diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3452648 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +composer.lock +.php_cs.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..f0a0059 --- /dev/null +++ b/.php_cs @@ -0,0 +1,37 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests') +; +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => ['spacing' => 'one'], + 'new_with_braces' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_empty_comment' => true, + 'no_leading_import_slash' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_unused_imports' => true, + 'ordered_imports' => ['importsOrder' => null, 'sortAlgorithm' => 'alpha'], + 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], + 'phpdoc_align' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_to_comment' => true, + 'psr0' => false, + 'psr4' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'whitespace_after_comma_in_array' => true, + ]) + ->setFinder($finder) +; diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..0116232 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,17 @@ +filter: + paths: [src/*] + excluded_paths: [tests/*] +checks: + php: + code_rating: true +tools: + external_code_coverage: + timeout: 600 + runs: 2 + php_code_coverage: false + php_loc: + enabled: true + excluded_dirs: [tests, vendor] + php_cpd: + enabled: true + excluded_dirs: [tests, vendor] diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eae56d3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: php + +sudo: false + +matrix: + include: + - php: 7.0 + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false + - php: 7.1 + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true + - php: master + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false + allow_failures: + - php: master + fast_finish: true + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - travis_retry composer self-update + +install: + - travis_retry composer update --no-interaction --prefer-source + +script: + - composer phpunit + +after_script: + - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi + - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..64df1cf --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +Softonic OAuth2 Provider +===== + +[![Latest Version](https://img.shields.io/github/release/softonic/oauth2-provider.svg?style=flat-square)](https://github.com/softonic/oauth2-provider/releases) +[![Software License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/softonic/oauth2-provider/master.svg?style=flat-square)](https://travis-ci.org/softonic/oauth2-provider) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/softonic/oauth2-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/softonic/oauth2-provider/code-structure) +[![Quality Score](https://img.shields.io/scrutinizer/g/softonic/oauth2-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/softonic/oauth2-provider) +[![Total Downloads](https://img.shields.io/packagist/dt/softonic/oauth2-provider.svg?style=flat-square)](https://packagist.org/packages/softonic/oauth2-provider) + +This package provides Softonic OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). + +Installation +------- + +To install, use composer: + +``` +composer require softonic/oauth2-provider +``` + +Usage +------- + +``` php + 'myClient', + 'clientSecret' => 'mySecret' +]; + +$client = new Softonic\OAuth2\Client\Provider\Softonic($options); + +$token = $client->getAccessToken('client_credentials', ['scope' => 'myscope']); +``` + + +Testing +------- + +`softonic/oauth2-provider` has a [PHPUnit](https://phpunit.de) test suite and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/). + +To run the tests, run the following command from the project folder. + +``` bash +$ docker-compose run test +``` + +To run interactively using [PsySH](http://psysh.org/): +``` bash +$ docker-compose run interactive +``` + +License +------- + +The Apache 2.0 license. Please see [LICENSE](LICENSE) for more information. + +[PSR-2]: http://www.php-fig.org/psr/psr-2/ +[PSR-4]: http://www.php-fig.org/psr/psr-4/ diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..675ee79 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "softonic/oauth2-provider", + "type": "library", + "description" : "Softonic OAuth2 provider", + "keywords": ["softonic", "oauth2", "provider"], + "license": "Apache-2.0", + "homepage": "https://github.com/softonic/oauth2-provider", + "support": { + "issues": "https://github.com/softonic/oauth2-provider/issues" + }, + "require": { + "php": ">=7.0", + "league/oauth2-client": "^2.2", + "psr/http-message": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0", + "friendsofphp/php-cs-fixer": "^2.4" + }, + "autoload": { + "psr-4": { + "Softonic\\OAuth2\\Client\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Softonic\\OAuth2\\Client\\Test\\": "tests/" + } + }, + "scripts": { + "test": "phpunit --coverage-text; php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", + "phpunit": "phpunit --coverage-text", + "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", + "fix-cs": "php-cs-fixer fix -v --diff --allow-risky=yes;" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5285eb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.2' + +services: + test: + volumes: + - ./:/srv + image: ypereirareis/prestissimo:latest + command: composer run test + + interactive: + working_dir: /srv + volumes: + - ./:/srv + entrypoint: /psysh/psysh + image: habitissimo/psysh:latest diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5059bcf --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + + + tests + + + + + + src + + + + + + + + + + + diff --git a/src/Provider/Softonic.php b/src/Provider/Softonic.php new file mode 100644 index 0000000..2d2e941 --- /dev/null +++ b/src/Provider/Softonic.php @@ -0,0 +1,130 @@ +getErrorMessage($data); + + if (!empty($message)) { + throw new IdentityProviderException( + $message, + $response->getStatusCode(), + $response + ); + } + } + + /** + * Returns error message if any, otherwise null. + * + * @param array $parsedResponse + * + * @return string|null + */ + private function getErrorMessage(array $parsedResponse) + { + if ($this->responseHasError($parsedResponse)) { + return !empty($parsedResponse['exception']) ? + $parsedResponse['exception'] + : $parsedResponse['error_description']; + } + + return null; + } + + /** + * Returns true if response has any error. + * + * @param array $parsedResponse + * + * @return bool + */ + private function responseHasError(array $parsedResponse) + { + return !empty($parsedResponse['exception']) || + !empty($parsedResponse['error_description']); + } + + /** + * Generates a resource owner object from a successful resource owner + * details request. + * + * @param array $response + * @param AccessToken $token + * + * @return ResourceOwnerInterface + */ + protected function createResourceOwner(array $response, AccessToken $token) + { + return new SoftonicResourceOwner($response); + } +} diff --git a/src/Provider/SoftonicResourceOwner.php b/src/Provider/SoftonicResourceOwner.php new file mode 100644 index 0000000..e31f437 --- /dev/null +++ b/src/Provider/SoftonicResourceOwner.php @@ -0,0 +1,28 @@ +expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method not implemented yet.'); + + $resourceOwner = new SoftonicResourceOwner(); + $resourceOwner->getId(); + } + + public function testToArayIsNotImplemented() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method not implemented yet.'); + + $resourceOwner = new SoftonicResourceOwner(); + $resourceOwner->toArray(); + } +} diff --git a/tests/Provider/SoftonicTest.php b/tests/Provider/SoftonicTest.php new file mode 100644 index 0000000..a184c0f --- /dev/null +++ b/tests/Provider/SoftonicTest.php @@ -0,0 +1,130 @@ +provider = new Softonic(); + } + + public function testGetBaseAuthorizationUrl() + { + $expectedUrl = 'https://oauth-v2.softonic.com/authorize'; + $this->assertSame($expectedUrl, $this->provider->getBaseAuthorizationUrl()); + } + + public function testGetBaseAccessTokenUrl() + { + $expectedUrl = 'https://oauth-v2.softonic.com/token'; + $this->assertSame($expectedUrl, $this->provider->getBaseAccessTokenUrl([])); + } + + public function testGetResourceOwnerDetailsUrl() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method not implemented.'); + + $accessToken = new AccessToken(['access_token' => 'foobar']); + $this->provider->getResourceOwnerDetailsUrl($accessToken); + } + + public function testGetDefaultScopes() + { + $this->provider = new class() extends Softonic { + public function getDefaultScopes() + { + return parent::getDefaultScopes(); + } + }; + + $this->assertSame([], $this->provider->getDefaultScopes()); + } + + public function testCheckResponseWhenTheResponseIsValidShouldNotThrowAnException() + { + $this->provider = new class() extends Softonic { + public function checkResponse(ResponseInterface $response, $data) + { + parent::checkResponse($response, $data); + } + }; + + $response = $this->createMock(ResponseInterface::class); + $this->assertNull($this->provider->checkResponse($response, [])); + } + + public function testCheckResponseWhenTheResponseContainsErrorsShouldThrowAIdentityProviderException() + { + $parsedResponse = [ + 'error' => 'invalid_client', + 'error_description' => 'The client credentials are invalid', + ]; + $this->provider = new class() extends Softonic { + public function checkResponse(ResponseInterface $response, $data) + { + parent::checkResponse($response, $data); + } + }; + + $this->expectException(IdentityProviderException::class); + $this->expectExceptionMessage('The client credentials are invalid'); + $this->expectExceptionCode(500); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->once()) + ->method('getStatusCode') + ->willReturn(500); + $this->provider->checkResponse($response, $parsedResponse); + } + + public function testCheckResponseWhenTheResponseScopeMissingShouldThrowAIdentityProviderException() + { + $parsedResponse = [ + 'status' => false, + 'message' => 'We are sorry, but something went terribly wrong.', + 'exception' => 'Missing request scopes', + ]; + $this->provider = new class() extends Softonic { + public function checkResponse(ResponseInterface $response, $data) + { + parent::checkResponse($response, $data); + } + }; + + $this->expectException(IdentityProviderException::class); + $this->expectExceptionMessage('Missing request scopes'); + $this->expectExceptionCode(500); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->once()) + ->method('getStatusCode') + ->willReturn(500); + $this->provider->checkResponse($response, $parsedResponse); + } + + public function testCreateResourceOwner() + { + $this->provider = new class() extends Softonic { + public function createResourceOwner(array $response, AccessToken $token) + { + return parent::createResourceOwner($response, $token); + } + }; + $response = []; + $accessToken = new AccessToken(['access_token' => 'foobar']); + $this->assertInstanceOf( + \League\OAuth2\Client\Provider\ResourceOwnerInterface::class, + $this->provider->createResourceOwner($response, $accessToken) + ); + } +}