From 54cce225f25d633ef68beb4f4fc44cdd0126ac10 Mon Sep 17 00:00:00 2001 From: Valentin Ursuleac Date: Wed, 16 Mar 2016 09:50:13 -0400 Subject: [PATCH 1/5] Init --- .gitignore | 4 + CHANGELOG.md | 7 + CONTRIBUTING.md | 38 +++ LICENSE | 21 ++ README.md | 113 +++++++- composer.json | 46 ++++ phpunit.xml | 18 ++ src/Grant/LsExchangeToken.php | 23 ++ .../Exception/LightspeedProviderException.php | 7 + src/Provider/Lightspeed.php | 251 ++++++++++++++++++ 10 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Grant/LsExchangeToken.php create mode 100644 src/Provider/Exception/LightspeedProviderException.php create mode 100644 src/Provider/Lightspeed.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8c7cb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/build +/vendor +composer.phar +composer.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0eb4e74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# CHANGELOG + +## 1.0.0 - March 14, 2016 + +- Updated `composer.json` +- Getting AccountId +- Getting Sale details diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ed1f47b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +Contributions are **welcome**. + +We accept contributions via Pull Requests on [Github](https://github.com/ursuleacv/oauth2-lightspeed). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow SemVer. Randomly breaking public APIs is not an option. + +- **Create topic branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. + +- **Ensure no coding standards violations** - Please run PHP Code Sniffer using the PSR-2 standard (see below) before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails. + + +## Running Tests + +``` bash +$ ./vendor/bin/phpunit +``` + + +## Running PHP Code Sniffer + +``` bash +$ ./vendor/bin/phpcs src --standard=psr2 -sp +``` + +**Happy coding**! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1add835 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Valentin Ursuleac + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 6df01e0..c49c524 100644 --- a/README.md +++ b/README.md @@ -1 +1,112 @@ -# oauth2-lightspeed \ No newline at end of file +# LightSpeed Provider for OAuth 2.0 Client + +This package provides LightSpeed OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/ursuleacv/oauth2-client). + +This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. If you notice compliance oversights, please send a patch via pull request. + +[PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md +[PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md +[PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md +[PSR-7]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md + + +## Requirements + +The following versions of PHP are supported. + +* PHP 5.5 +* PHP 5.6 +* PHP 7.0 +* HHVM + +## Installation + +Add the following to your `composer.json` file. + +```json +{ + "require": { + "ursuleacv/oauth2-lightspeed": "~1.0" + } +} +``` + +## Usage + +### Authorization Code Flow + +```php +session_start(); + +$provider = new League\OAuth2\Client\Provider\Lightspeed([ + 'clientId' => LIGHTSPEED_CLIENT_ID, + 'clientSecret' => LIGHTSPEED_CLIENT_SECRET, + 'redirectUri' => LIGHTSPEED_REDIRECT_URI, + 'accountId' => LIGHTSPEED_ACCOUNT_ID, +]); + +if (!isset($_GET['code'])) { + + // If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl([ + 'scope' => ['employee:all', '...', '...'], + ]); + $_SESSION['oauth2state'] = $provider->getState(); + + echo 'Log in with LightSpeed!'; + exit; + +// Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + + unset($_SESSION['oauth2state']); + echo 'Invalid state.'; + exit; + +} + +// Try to get an access token (using the authorization code grant) +$token = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'] +]); + +try { + + // We got an access token, let's now get the Account ID and sale details + $client = $ls->getAccountId($token); + $sale = $ls->getSale($token, 1); + + echo '
';
+    print_r($client); echo '
'; + print_r($sale); echo '
'; + echo '
'; + +} catch (Exception $e) { + exit($e->getMessage()); +} + +echo '
';
+// Use this to interact with an API on the client behalf
+var_dump($token->getToken());
+
+echo '
'; +``` + +## Testing + +``` bash +$ ./vendor/bin/phpunit +``` + +## Contributing + +Please see [CONTRIBUTING](https://github.com/ursuleacv/oauth2-lightspeed/blob/master/CONTRIBUTING.md) for details. + + +## Credits + +- [Valentin Ursuleac](https://github.com/ursuleacv) + +## License + +The MIT License (MIT). Please see [License File](https://github.com/ursuleacv/oauth2-lightspeed/blob/master/LICENSE) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..bf95a43 --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "ursuleacv/oauth2-lightspeed", + "description": "LightSpeed OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "license": "MIT", + "version": "1.0.0", + "authors": [ + { + "name": "Valentin Ursuleac", + "email": "ursuleacv@gmail.com", + "homepage": "http://valnet.ca", + "role": "Developer" + } + ], + "keywords": [ + "oauth", + "oauth2", + "client", + "authorization", + "authentication", + "LightSpeed" + ], + "require": { + "php": ">=5.5.0", + "league/oauth2-client": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "mockery/mockery": "~0.9", + "squizlabs/php_codesniffer": "~2.0" + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "League\\OAuth2\\Client\\Test\\": "tests/src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Grant/LsExchangeToken.php b/src/Grant/LsExchangeToken.php new file mode 100644 index 0000000..089b015 --- /dev/null +++ b/src/Grant/LsExchangeToken.php @@ -0,0 +1,23 @@ +false, 'apiCall'=>'']; + + /** + * @param array $options + * @param array $collaborators + * + * @throws \InvalidArgumentException + */ + public function __construct($options = [], array $collaborators = []) + { + parent::__construct($options, $collaborators); + + $this->accountId = $options['accountId']; + } + + public function getBaseAuthorizationUrl() + { + return static::LIGHTSPEED_AUTHORIZATION_ENDPOINT; + } + + public function getBaseAccessTokenUrl(array $params) + { + return static::LIGHTSPEED_TOKEN_ENDPOINT; + } + + public function getDefaultScopes() + { + return ['employee:all']; + } + + public function getAccessToken($grant = 'authorization_code', array $params = []) + { + if (isset($params['refresh_token'])) { + throw new LightspeedProviderException('Lightspeed does not support token refreshing.'); + } + + return parent::getAccessToken($grant, $params); + } + + /** + * Exchanges a short-lived access token with a long-lived access-token. + * + * @param string $accessToken + * + * @return \League\OAuth2\Client\Token\AccessToken + * + * @throws LightspeedProviderException + */ + public function getLongLivedAccessToken($accessToken) + { + $params = [ + 'ls_exchange_token' => (string) $accessToken, + ]; + + return $this->getAccessToken('ls_exchange_token', $params); + } + + public function getAccountId(AccessToken $token) + { + $url = $this->prepareApiUrl('Account', $this->accountId, NULL).'?oauth_token='.$token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + + $response = $this->getResponse($request); + + if( isset( $response['Account'] ) && $response['Account']['accountID'] ) + return (int) $response['Account']['accountID']; + + if( isset( $response['httpCode'] ) && $response['httpCode'] != '200' ) + throw new IdentityProviderException($response['message'], $response['httpCode'], $response); + } + + public function getSale(AccessToken $token, $saleId) + { + $apiResource = 'Account.Sale'; + $this->context['apiCall'] = $apiResource; + + $url = $this->prepareApiUrl($apiResource, $this->accountId, $saleId).'?oauth_token='.$token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + if( isset( $response['Sale'] ) && $this->itemsCount($response)>0 ) // should only return 1 sale. + return $response['Sale']; + + return []; + } + + public function getShops(AccessToken $token) + { + $apiResource = 'Account.Shop'; + $this->context['apiCall'] = $apiResource; + + //get url + $url = $this->prepareApiUrl($apiResource, $this->accountId, NULL).'?oauth_token='.$token; + //make API call + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + //get response + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + //validate the response + if( isset( $response['Shop'] ) && $this->itemsCount($response)==1 ) + return [$response['Shop']]; + elseif( isset( $response['Shop'] ) && $this->itemsCount($response)>1 ) + return $response['Shop']; + + return []; + } + + private function prepareApiUrl($controlName, $accountId, $uniqueId=NULL) + { + $controlUrl = $this->getBaseLightspeedApiUrl() . str_replace( '.', '/', str_replace('Account.', 'Account.' . $accountId . '.', $controlName ) ); + + if ( $uniqueId ) { + $controlUrl .= '/' . $uniqueId; + } + + $controlUrl .=self::LS_FORMAT; + + return $controlUrl; + } + + private function checkApiResponse( $response ) + { + if (empty($this->accountId)) { + $message = 'The "accountId" not set. In order to query Shop endpoint an accountId is required.'; + throw new \Exception($message); + } + + // must be an error + if( isset( $response['httpCode'] ) && $response['httpCode'] != '200' ){ + $message = $response['httpMessage'].': '.$response['message'] . ' ('.$response['errorClass'].')'; + throw new IdentityProviderException($message, $response['httpCode'], $response); + } + } + + private function itemsCount( $response ) + { + $attributes = '@attributes'; + + if( isset( $response[$attributes] ) ) + return $response[$attributes]['count']; + + return 0; + } + + public function getResourceOwnerDetailsUrl(AccessToken $token) + { + return $this->getBaseLightspeedApiUrl().'/Account/'.$this->accountId.'/Item?oauth_token='.$token; + } + + protected function createResourceOwner(array $response, AccessToken $token) + { + return new LightspeedUser($response); + } + + /** + * Returns all options that are required. + * + * @return array + */ + protected function getRequiredOptions() + { + return [ + 'urlAuthorize', + 'urlAccessToken', + ]; + } + + protected function checkResponse(ResponseInterface $response, $data) + { + if (!empty($data['error'])) { + $message = $data['error'].': '.$data['error_description']; + throw new IdentityProviderException($message, $response->getStatusCode(), $data); + } + } + + /** + * @inheritdoc + */ + // protected function getContentType(ResponseInterface $response) + // { + // $type = parent::getContentType($response); + + // // Fix for Lightspeed's pseudo-JSONP support + // if (strpos($type, 'javascript') !== false) { + // return 'application/json'; + // } + + // // Fix for Lightspeed's pseudo-urlencoded support + // if (strpos($type, 'plain') !== false) { + // return 'application/x-www-form-urlencoded'; + // } + + // return $type; + // } + + /** + * Get the Lightspeed api URL. + * + * @return string + */ + private function getBaseLightspeedApiUrl() + { + return static::LIGHTSPEED_API_URL; + } + + /** + * Verifies that all required options have been passed. + * + * @param array $options + * @return void + * @throws InvalidArgumentException + */ + private function assertRequiredOptions(array $options) + { + $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options); + if (!empty($missing)) { + throw new InvalidArgumentException( + 'Required options not defined: ' . implode(', ', array_keys($missing)) + ); + } + } +} From 41e984678a9149fa1dbbbaf839f89b8b761e6b67 Mon Sep 17 00:00:00 2001 From: Valentin Ursuleac Date: Wed, 16 Mar 2016 09:54:15 -0400 Subject: [PATCH 2/5] Adding travis --- .travis.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ed15fe2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - hhvm + +before_script: + - travis_retry composer self-update + - travis_retry composer install --no-interaction --prefer-source --dev + - travis_retry phpenv rehash + +script: + - ./vendor/bin/phpcs --standard=psr2 src/ + - ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi \ No newline at end of file From ce83d975b494e3efc9dee2eae7094058d4e907a7 Mon Sep 17 00:00:00 2001 From: Valentin Ursuleac Date: Wed, 16 Mar 2016 10:18:29 -0400 Subject: [PATCH 3/5] Formatting --- .../Exception/LightspeedProviderException.php | 3 +- src/Provider/Lightspeed.php | 499 +++++++++--------- 2 files changed, 257 insertions(+), 245 deletions(-) diff --git a/src/Provider/Exception/LightspeedProviderException.php b/src/Provider/Exception/LightspeedProviderException.php index bdc7128..b67e299 100644 --- a/src/Provider/Exception/LightspeedProviderException.php +++ b/src/Provider/Exception/LightspeedProviderException.php @@ -2,6 +2,5 @@ namespace League\OAuth2\Client\Provider\Exception; -class LightspeedProviderException extends \Exception -{ +class LightspeedProviderException extends \Exception { } diff --git a/src/Provider/Lightspeed.php b/src/Provider/Lightspeed.php index 5c9d825..7fb6ce6 100644 --- a/src/Provider/Lightspeed.php +++ b/src/Provider/Lightspeed.php @@ -2,250 +2,263 @@ namespace League\OAuth2\Client\Provider; -use League\OAuth2\Client\Token\AccessToken; -use League\OAuth2\Client\Provider\Exception\LightspeedProviderException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Provider\Exception\LightspeedProviderException; +use League\OAuth2\Client\Token\AccessToken; use Psr\Http\Message\ResponseInterface; -class Lightspeed extends AbstractProvider -{ - - const LIGHTSPEED_API_URL = 'https://api.merchantos.com/API/'; - const LIGHTSPEED_REGISTRATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/register.php'; - const LIGHTSPEED_AUTHORIZATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/authorize.php'; - const LIGHTSPEED_TOKEN_ENDPOINT = 'https://cloud.merchantos.com/oauth/access_token.php'; - const LS_FORMAT = '.json'; - - /** - * Lightspeed account ID - * - * @const string - */ - protected $accountId; - - private $context = ['error'=>false, 'apiCall'=>'']; - - /** - * @param array $options - * @param array $collaborators - * - * @throws \InvalidArgumentException - */ - public function __construct($options = [], array $collaborators = []) - { - parent::__construct($options, $collaborators); - - $this->accountId = $options['accountId']; - } - - public function getBaseAuthorizationUrl() - { - return static::LIGHTSPEED_AUTHORIZATION_ENDPOINT; - } - - public function getBaseAccessTokenUrl(array $params) - { - return static::LIGHTSPEED_TOKEN_ENDPOINT; - } - - public function getDefaultScopes() - { - return ['employee:all']; - } - - public function getAccessToken($grant = 'authorization_code', array $params = []) - { - if (isset($params['refresh_token'])) { - throw new LightspeedProviderException('Lightspeed does not support token refreshing.'); - } - - return parent::getAccessToken($grant, $params); - } - - /** - * Exchanges a short-lived access token with a long-lived access-token. - * - * @param string $accessToken - * - * @return \League\OAuth2\Client\Token\AccessToken - * - * @throws LightspeedProviderException - */ - public function getLongLivedAccessToken($accessToken) - { - $params = [ - 'ls_exchange_token' => (string) $accessToken, - ]; - - return $this->getAccessToken('ls_exchange_token', $params); - } - - public function getAccountId(AccessToken $token) - { - $url = $this->prepareApiUrl('Account', $this->accountId, NULL).'?oauth_token='.$token; - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - - $response = $this->getResponse($request); - - if( isset( $response['Account'] ) && $response['Account']['accountID'] ) - return (int) $response['Account']['accountID']; - - if( isset( $response['httpCode'] ) && $response['httpCode'] != '200' ) - throw new IdentityProviderException($response['message'], $response['httpCode'], $response); - } - - public function getSale(AccessToken $token, $saleId) - { - $apiResource = 'Account.Sale'; - $this->context['apiCall'] = $apiResource; - - $url = $this->prepareApiUrl($apiResource, $this->accountId, $saleId).'?oauth_token='.$token; - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - $response = $this->getResponse($request); - - $this->checkApiResponse($response); - - if( isset( $response['Sale'] ) && $this->itemsCount($response)>0 ) // should only return 1 sale. - return $response['Sale']; - - return []; - } - - public function getShops(AccessToken $token) - { - $apiResource = 'Account.Shop'; - $this->context['apiCall'] = $apiResource; - - //get url - $url = $this->prepareApiUrl($apiResource, $this->accountId, NULL).'?oauth_token='.$token; - //make API call - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - //get response - $response = $this->getResponse($request); - - $this->checkApiResponse($response); - - //validate the response - if( isset( $response['Shop'] ) && $this->itemsCount($response)==1 ) - return [$response['Shop']]; - elseif( isset( $response['Shop'] ) && $this->itemsCount($response)>1 ) - return $response['Shop']; - - return []; - } - - private function prepareApiUrl($controlName, $accountId, $uniqueId=NULL) - { - $controlUrl = $this->getBaseLightspeedApiUrl() . str_replace( '.', '/', str_replace('Account.', 'Account.' . $accountId . '.', $controlName ) ); - - if ( $uniqueId ) { - $controlUrl .= '/' . $uniqueId; - } - - $controlUrl .=self::LS_FORMAT; - - return $controlUrl; - } - - private function checkApiResponse( $response ) - { - if (empty($this->accountId)) { - $message = 'The "accountId" not set. In order to query Shop endpoint an accountId is required.'; - throw new \Exception($message); - } - - // must be an error - if( isset( $response['httpCode'] ) && $response['httpCode'] != '200' ){ - $message = $response['httpMessage'].': '.$response['message'] . ' ('.$response['errorClass'].')'; - throw new IdentityProviderException($message, $response['httpCode'], $response); - } - } - - private function itemsCount( $response ) - { - $attributes = '@attributes'; - - if( isset( $response[$attributes] ) ) - return $response[$attributes]['count']; - - return 0; - } - - public function getResourceOwnerDetailsUrl(AccessToken $token) - { - return $this->getBaseLightspeedApiUrl().'/Account/'.$this->accountId.'/Item?oauth_token='.$token; - } - - protected function createResourceOwner(array $response, AccessToken $token) - { - return new LightspeedUser($response); - } - - /** - * Returns all options that are required. - * - * @return array - */ - protected function getRequiredOptions() - { - return [ - 'urlAuthorize', - 'urlAccessToken', - ]; - } - - protected function checkResponse(ResponseInterface $response, $data) - { - if (!empty($data['error'])) { - $message = $data['error'].': '.$data['error_description']; - throw new IdentityProviderException($message, $response->getStatusCode(), $data); - } - } - - /** - * @inheritdoc - */ - // protected function getContentType(ResponseInterface $response) - // { - // $type = parent::getContentType($response); - - // // Fix for Lightspeed's pseudo-JSONP support - // if (strpos($type, 'javascript') !== false) { - // return 'application/json'; - // } - - // // Fix for Lightspeed's pseudo-urlencoded support - // if (strpos($type, 'plain') !== false) { - // return 'application/x-www-form-urlencoded'; - // } - - // return $type; - // } - - /** - * Get the Lightspeed api URL. - * - * @return string - */ - private function getBaseLightspeedApiUrl() - { - return static::LIGHTSPEED_API_URL; - } - - /** - * Verifies that all required options have been passed. - * - * @param array $options - * @return void - * @throws InvalidArgumentException - */ - private function assertRequiredOptions(array $options) - { - $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options); - if (!empty($missing)) { - throw new InvalidArgumentException( - 'Required options not defined: ' . implode(', ', array_keys($missing)) - ); - } - } +class Lightspeed extends AbstractProvider { + + const LIGHTSPEED_API_URL = 'https://api.merchantos.com/API/'; + const LIGHTSPEED_REGISTRATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/register.php'; + const LIGHTSPEED_AUTHORIZATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/authorize.php'; + const LIGHTSPEED_TOKEN_ENDPOINT = 'https://cloud.merchantos.com/oauth/access_token.php'; + const LS_FORMAT = '.json'; + + /** + * @var mixed + */ + protected $accountId; + + /** + * @var array + */ + private $context = ['error' => false, 'apiCall' => '']; + + /** + * @param array $options + * @param array $collaborators + * + * @throws \InvalidArgumentException + */ + public function __construct($options = [], array $collaborators = []) { + parent::__construct($options, $collaborators); + + $this->accountId = $options['accountId']; + } + + public function getBaseAuthorizationUrl() { + return static::LIGHTSPEED_AUTHORIZATION_ENDPOINT; + } + + /** + * @param array $params + */ + public function getBaseAccessTokenUrl(array $params) { + return static::LIGHTSPEED_TOKEN_ENDPOINT; + } + + public function getDefaultScopes() { + return ['employee:all']; + } + + /** + * @param $grant + * @param array $params + */ + public function getAccessToken($grant = 'authorization_code', array $params = []) { + if (isset($params['refresh_token'])) { + throw new LightspeedProviderException('Lightspeed does not support token refreshing.'); + } + + return parent::getAccessToken($grant, $params); + } + + /** + * Exchanges a short-lived access token with a long-lived access-token. + * + * @param string $accessToken + * + * @return \League\OAuth2\Client\Token\AccessToken + * + * @throws LightspeedProviderException + */ + public function getLongLivedAccessToken($accessToken) { + $params = [ + 'ls_exchange_token' => (string) $accessToken, + ]; + + return $this->getAccessToken('ls_exchange_token', $params); + } + + /** + * @param AccessToken $token + */ + public function getAccountId(AccessToken $token) { + $url = $this->prepareApiUrl('Account', $this->accountId, null) . '?oauth_token=' . $token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + + $response = $this->getResponse($request); + + if (isset($response['Account']) && $response['Account']['accountID']) { + return (int) $response['Account']['accountID']; + } + + if (isset($response['httpCode']) && $response['httpCode'] != '200') { + throw new IdentityProviderException($response['message'], $response['httpCode'], $response); + } + + } + + /** + * @param AccessToken $token + * @param $saleId + * @return mixed + */ + public function getSale(AccessToken $token, $saleId) { + $apiResource = 'Account.Sale'; + $this->context['apiCall'] = $apiResource; + + $url = $this->prepareApiUrl($apiResource, $this->accountId, $saleId) . '?oauth_token=' . $token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + if (isset($response['Sale']) && $this->itemsCount($response) > 0) // should only return 1 sale. + { + return $response['Sale']; + } + + return []; + } + + /** + * @param AccessToken $token + * @return mixed + */ + public function getShops(AccessToken $token) { + $apiResource = 'Account.Shop'; + $this->context['apiCall'] = $apiResource; + + //get url + $url = $this->prepareApiUrl($apiResource, $this->accountId, null) . '?oauth_token=' . $token; + //make API call + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + //get response + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + //validate the response + if (isset($response['Shop']) && $this->itemsCount($response) == 1) { + return [$response['Shop']]; + } elseif (isset($response['Shop']) && $this->itemsCount($response) > 1) { + return $response['Shop']; + } + + return []; + } + + /** + * @param $controlName + * @param $accountId + * @param $uniqueId + * @return mixed + */ + private function prepareApiUrl($controlName, $accountId, $uniqueId = null) { + $controlUrl = $this->getBaseLightspeedApiUrl() . str_replace('.', '/', str_replace('Account.', 'Account.' . $accountId . '.', $controlName)); + + if ($uniqueId) { + $controlUrl .= '/' . $uniqueId; + } + + $controlUrl .= self::LS_FORMAT; + + return $controlUrl; + } + + /** + * @param $response + */ + private function checkApiResponse($response) { + if (empty($this->accountId)) { + $message = 'The "accountId" not set. In order to query Shop endpoint an accountId is required.'; + throw new \Exception($message); + } + + // must be an error + if (isset($response['httpCode']) && $response['httpCode'] != '200') { + $message = $response['httpMessage'] . ': ' . $response['message'] . ' (' . $response['errorClass'] . ')'; + throw new IdentityProviderException($message, $response['httpCode'], $response); + } + } + + /** + * @param $response + * @return int + */ + private function itemsCount($response) { + $attributes = '@attributes'; + + if (isset($response[$attributes])) { + return $response[$attributes]['count']; + } + + return 0; + } + + /** + * @param AccessToken $token + * @return mixed + */ + public function getResourceOwnerDetailsUrl(AccessToken $token) { + return $this->getBaseLightspeedApiUrl() . '/Account/' . $this->accountId . '/Item?oauth_token=' . $token; + } + + /** + * @param array $response + * @param AccessToken $token + */ + protected function createResourceOwner(array $response, AccessToken $token) { + return new LightspeedUser($response); + } + + /** + * Returns all options that are required. + * + * @return array + */ + protected function getRequiredOptions() { + return [ + 'urlAuthorize', + 'urlAccessToken', + ]; + } + + /** + * @param ResponseInterface $response + * @param $data + */ + protected function checkResponse(ResponseInterface $response, $data) { + if (!empty($data['error'])) { + $message = $data['error'] . ': ' . $data['error_description']; + throw new IdentityProviderException($message, $response->getStatusCode(), $data); + } + } + + /** + * Get the Lightspeed api URL. + * + * @return string + */ + private function getBaseLightspeedApiUrl() { + return static::LIGHTSPEED_API_URL; + } + + /** + * Verifies that all required options have been passed. + * + * @param array $options + * @return void + * @throws InvalidArgumentException + */ + private function assertRequiredOptions(array $options) { + $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options); + if (!empty($missing)) { + throw new InvalidArgumentException( + 'Required options not defined: ' . implode(', ', array_keys($missing)) + ); + } + } } From 29b91cd5bd23eea76655713fb6ab9c8fa90c70b4 Mon Sep 17 00:00:00 2001 From: Valentin Ursuleac Date: Wed, 16 Mar 2016 10:23:33 -0400 Subject: [PATCH 4/5] Formatting psr2 --- src/Provider/Lightspeed.php | 527 +++++++++++++++++++----------------- 1 file changed, 273 insertions(+), 254 deletions(-) diff --git a/src/Provider/Lightspeed.php b/src/Provider/Lightspeed.php index 7fb6ce6..07e9024 100644 --- a/src/Provider/Lightspeed.php +++ b/src/Provider/Lightspeed.php @@ -7,258 +7,277 @@ use League\OAuth2\Client\Token\AccessToken; use Psr\Http\Message\ResponseInterface; -class Lightspeed extends AbstractProvider { - - const LIGHTSPEED_API_URL = 'https://api.merchantos.com/API/'; - const LIGHTSPEED_REGISTRATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/register.php'; - const LIGHTSPEED_AUTHORIZATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/authorize.php'; - const LIGHTSPEED_TOKEN_ENDPOINT = 'https://cloud.merchantos.com/oauth/access_token.php'; - const LS_FORMAT = '.json'; - - /** - * @var mixed - */ - protected $accountId; - - /** - * @var array - */ - private $context = ['error' => false, 'apiCall' => '']; - - /** - * @param array $options - * @param array $collaborators - * - * @throws \InvalidArgumentException - */ - public function __construct($options = [], array $collaborators = []) { - parent::__construct($options, $collaborators); - - $this->accountId = $options['accountId']; - } - - public function getBaseAuthorizationUrl() { - return static::LIGHTSPEED_AUTHORIZATION_ENDPOINT; - } - - /** - * @param array $params - */ - public function getBaseAccessTokenUrl(array $params) { - return static::LIGHTSPEED_TOKEN_ENDPOINT; - } - - public function getDefaultScopes() { - return ['employee:all']; - } - - /** - * @param $grant - * @param array $params - */ - public function getAccessToken($grant = 'authorization_code', array $params = []) { - if (isset($params['refresh_token'])) { - throw new LightspeedProviderException('Lightspeed does not support token refreshing.'); - } - - return parent::getAccessToken($grant, $params); - } - - /** - * Exchanges a short-lived access token with a long-lived access-token. - * - * @param string $accessToken - * - * @return \League\OAuth2\Client\Token\AccessToken - * - * @throws LightspeedProviderException - */ - public function getLongLivedAccessToken($accessToken) { - $params = [ - 'ls_exchange_token' => (string) $accessToken, - ]; - - return $this->getAccessToken('ls_exchange_token', $params); - } - - /** - * @param AccessToken $token - */ - public function getAccountId(AccessToken $token) { - $url = $this->prepareApiUrl('Account', $this->accountId, null) . '?oauth_token=' . $token; - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - - $response = $this->getResponse($request); - - if (isset($response['Account']) && $response['Account']['accountID']) { - return (int) $response['Account']['accountID']; - } - - if (isset($response['httpCode']) && $response['httpCode'] != '200') { - throw new IdentityProviderException($response['message'], $response['httpCode'], $response); - } - - } - - /** - * @param AccessToken $token - * @param $saleId - * @return mixed - */ - public function getSale(AccessToken $token, $saleId) { - $apiResource = 'Account.Sale'; - $this->context['apiCall'] = $apiResource; - - $url = $this->prepareApiUrl($apiResource, $this->accountId, $saleId) . '?oauth_token=' . $token; - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - $response = $this->getResponse($request); - - $this->checkApiResponse($response); - - if (isset($response['Sale']) && $this->itemsCount($response) > 0) // should only return 1 sale. - { - return $response['Sale']; - } - - return []; - } - - /** - * @param AccessToken $token - * @return mixed - */ - public function getShops(AccessToken $token) { - $apiResource = 'Account.Shop'; - $this->context['apiCall'] = $apiResource; - - //get url - $url = $this->prepareApiUrl($apiResource, $this->accountId, null) . '?oauth_token=' . $token; - //make API call - $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); - //get response - $response = $this->getResponse($request); - - $this->checkApiResponse($response); - - //validate the response - if (isset($response['Shop']) && $this->itemsCount($response) == 1) { - return [$response['Shop']]; - } elseif (isset($response['Shop']) && $this->itemsCount($response) > 1) { - return $response['Shop']; - } - - return []; - } - - /** - * @param $controlName - * @param $accountId - * @param $uniqueId - * @return mixed - */ - private function prepareApiUrl($controlName, $accountId, $uniqueId = null) { - $controlUrl = $this->getBaseLightspeedApiUrl() . str_replace('.', '/', str_replace('Account.', 'Account.' . $accountId . '.', $controlName)); - - if ($uniqueId) { - $controlUrl .= '/' . $uniqueId; - } - - $controlUrl .= self::LS_FORMAT; - - return $controlUrl; - } - - /** - * @param $response - */ - private function checkApiResponse($response) { - if (empty($this->accountId)) { - $message = 'The "accountId" not set. In order to query Shop endpoint an accountId is required.'; - throw new \Exception($message); - } - - // must be an error - if (isset($response['httpCode']) && $response['httpCode'] != '200') { - $message = $response['httpMessage'] . ': ' . $response['message'] . ' (' . $response['errorClass'] . ')'; - throw new IdentityProviderException($message, $response['httpCode'], $response); - } - } - - /** - * @param $response - * @return int - */ - private function itemsCount($response) { - $attributes = '@attributes'; - - if (isset($response[$attributes])) { - return $response[$attributes]['count']; - } - - return 0; - } - - /** - * @param AccessToken $token - * @return mixed - */ - public function getResourceOwnerDetailsUrl(AccessToken $token) { - return $this->getBaseLightspeedApiUrl() . '/Account/' . $this->accountId . '/Item?oauth_token=' . $token; - } - - /** - * @param array $response - * @param AccessToken $token - */ - protected function createResourceOwner(array $response, AccessToken $token) { - return new LightspeedUser($response); - } - - /** - * Returns all options that are required. - * - * @return array - */ - protected function getRequiredOptions() { - return [ - 'urlAuthorize', - 'urlAccessToken', - ]; - } - - /** - * @param ResponseInterface $response - * @param $data - */ - protected function checkResponse(ResponseInterface $response, $data) { - if (!empty($data['error'])) { - $message = $data['error'] . ': ' . $data['error_description']; - throw new IdentityProviderException($message, $response->getStatusCode(), $data); - } - } - - /** - * Get the Lightspeed api URL. - * - * @return string - */ - private function getBaseLightspeedApiUrl() { - return static::LIGHTSPEED_API_URL; - } - - /** - * Verifies that all required options have been passed. - * - * @param array $options - * @return void - * @throws InvalidArgumentException - */ - private function assertRequiredOptions(array $options) { - $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options); - if (!empty($missing)) { - throw new InvalidArgumentException( - 'Required options not defined: ' . implode(', ', array_keys($missing)) - ); - } - } +class Lightspeed extends AbstractProvider +{ + + const LIGHTSPEED_API_URL = 'https://api.merchantos.com/API/'; + const LIGHTSPEED_REGISTRATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/register.php'; + const LIGHTSPEED_AUTHORIZATION_ENDPOINT = 'https://cloud.merchantos.com/oauth/authorize.php'; + const LIGHTSPEED_TOKEN_ENDPOINT = 'https://cloud.merchantos.com/oauth/access_token.php'; + const LS_FORMAT = '.json'; + + /** + * @var mixed + */ + protected $accountId; + + /** + * @var array + */ + private $context = ['error' => false, 'apiCall' => '']; + + /** + * @param array $options + * @param array $collaborators + * + * @throws \InvalidArgumentException + */ + public function __construct($options = [], array $collaborators = []) + { + parent::__construct($options, $collaborators); + + $this->accountId = $options['accountId']; + } + + public function getBaseAuthorizationUrl() + { + return static::LIGHTSPEED_AUTHORIZATION_ENDPOINT; + } + + /** + * @param array $params + */ + public function getBaseAccessTokenUrl(array $params) + { + return static::LIGHTSPEED_TOKEN_ENDPOINT; + } + + public function getDefaultScopes() + { + return ['employee:all']; + } + + /** + * @param $grant + * @param array $params + */ + public function getAccessToken($grant = 'authorization_code', array $params = []) + { + if (isset($params['refresh_token'])) { + throw new LightspeedProviderException('Lightspeed does not support token refreshing.'); + } + + return parent::getAccessToken($grant, $params); + } + + /** + * Exchanges a short-lived access token with a long-lived access-token. + * + * @param string $accessToken + * + * @return \League\OAuth2\Client\Token\AccessToken + * + * @throws LightspeedProviderException + */ + public function getLongLivedAccessToken($accessToken) + { + $params = [ + 'ls_exchange_token' => (string) $accessToken, + ]; + + return $this->getAccessToken('ls_exchange_token', $params); + } + + /** + * @param AccessToken $token + */ + public function getAccountId(AccessToken $token) + { + $url = $this->prepareApiUrl('Account', $this->accountId, null) . '?oauth_token=' . $token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + + $response = $this->getResponse($request); + + if (isset($response['Account']) && $response['Account']['accountID']) { + return (int) $response['Account']['accountID']; + } + + if (isset($response['httpCode']) && $response['httpCode'] != '200') { + throw new IdentityProviderException($response['message'], $response['httpCode'], $response); + } + + } + + /** + * @param AccessToken $token + * @param $saleId + * @return mixed + */ + public function getSale(AccessToken $token, $saleId) + { + $apiResource = 'Account.Sale'; + $this->context['apiCall'] = $apiResource; + + $url = $this->prepareApiUrl($apiResource, $this->accountId, $saleId) . '?oauth_token=' . $token; + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + if (isset($response['Sale']) && $this->itemsCount($response) > 0) // should only return 1 sale. + { + return $response['Sale']; + } + + return []; + } + + /** + * @param AccessToken $token + * @return mixed + */ + public function getShops(AccessToken $token) + { + $apiResource = 'Account.Shop'; + $this->context['apiCall'] = $apiResource; + + //get url + $url = $this->prepareApiUrl($apiResource, $this->accountId, null) . '?oauth_token=' . $token; + //make API call + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + //get response + $response = $this->getResponse($request); + + $this->checkApiResponse($response); + + //validate the response + if (isset($response['Shop']) && $this->itemsCount($response) == 1) { + return [$response['Shop']]; + } elseif (isset($response['Shop']) && $this->itemsCount($response) > 1) { + return $response['Shop']; + } + + return []; + } + + /** + * @param $controlName + * @param $accountId + * @param $uniqueId + * @return mixed + */ + private function prepareApiUrl($controlName, $accountId, $uniqueId = null) + { + $controlUrl = $this->getBaseLightspeedApiUrl() . str_replace('.', '/', str_replace('Account.', 'Account.' . $accountId . '.', $controlName)); + + if ($uniqueId) { + $controlUrl .= '/' . $uniqueId; + } + + $controlUrl .= self::LS_FORMAT; + + return $controlUrl; + } + + /** + * @param $response + */ + private function checkApiResponse($response) + { + if (empty($this->accountId)) { + $message = 'The "accountId" not set. In order to query Shop endpoint an accountId is required.'; + throw new \Exception($message); + } + + // must be an error + if (isset($response['httpCode']) && $response['httpCode'] != '200') { + $message = $response['httpMessage'] . ': ' . $response['message'] . ' (' . $response['errorClass'] . ')'; + throw new IdentityProviderException($message, $response['httpCode'], $response); + } + } + + /** + * @param $response + * @return int + */ + private function itemsCount($response) + { + $attributes = '@attributes'; + + if (isset($response[$attributes])) { + return $response[$attributes]['count']; + } + + return 0; + } + + /** + * @param AccessToken $token + * @return mixed + */ + public function getResourceOwnerDetailsUrl(AccessToken $token) + { + return $this->getBaseLightspeedApiUrl() . '/Account/' . $this->accountId . '/Item?oauth_token=' . $token; + } + + /** + * @param array $response + * @param AccessToken $token + */ + protected function createResourceOwner(array $response, AccessToken $token) + { + return new LightspeedUser($response); + } + + /** + * Returns all options that are required. + * + * @return array + */ + protected function getRequiredOptions() + { + return [ + 'urlAuthorize', + 'urlAccessToken', + ]; + } + + /** + * @param ResponseInterface $response + * @param $data + */ + protected function checkResponse(ResponseInterface $response, $data) + { + if (!empty($data['error'])) { + $message = $data['error'] . ': ' . $data['error_description']; + throw new IdentityProviderException($message, $response->getStatusCode(), $data); + } + } + + /** + * Get the Lightspeed api URL. + * + * @return string + */ + private function getBaseLightspeedApiUrl() + { + return static::LIGHTSPEED_API_URL; + } + + /** + * Verifies that all required options have been passed. + * + * @param array $options + * @return void + * @throws InvalidArgumentException + */ + private function assertRequiredOptions(array $options) + { + $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options); + if (!empty($missing)) { + throw new InvalidArgumentException( + 'Required options not defined: ' . implode(', ', array_keys($missing)) + ); + } + } } From b5d2a84d64e175758e0e8210cd188c5cf3e230a6 Mon Sep 17 00:00:00 2001 From: Valentin Ursuleac Date: Wed, 16 Mar 2016 11:20:06 -0400 Subject: [PATCH 5/5] Adding tests --- phpunit.xml | 4 + .../Exception/LightspeedProviderException.php | 3 +- src/Provider/Lightspeed.php | 3 +- tests/src/Provider/LightspeedTest.php | 175 ++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 tests/src/Provider/LightspeedTest.php diff --git a/phpunit.xml b/phpunit.xml index 3347b75..5d0b9f8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,4 +15,8 @@ ./tests/ + + + + diff --git a/src/Provider/Exception/LightspeedProviderException.php b/src/Provider/Exception/LightspeedProviderException.php index b67e299..bdc7128 100644 --- a/src/Provider/Exception/LightspeedProviderException.php +++ b/src/Provider/Exception/LightspeedProviderException.php @@ -2,5 +2,6 @@ namespace League\OAuth2\Client\Provider\Exception; -class LightspeedProviderException extends \Exception { +class LightspeedProviderException extends \Exception +{ } diff --git a/src/Provider/Lightspeed.php b/src/Provider/Lightspeed.php index 07e9024..446ba58 100644 --- a/src/Provider/Lightspeed.php +++ b/src/Provider/Lightspeed.php @@ -124,8 +124,7 @@ public function getSale(AccessToken $token, $saleId) $this->checkApiResponse($response); - if (isset($response['Sale']) && $this->itemsCount($response) > 0) // should only return 1 sale. - { + if (isset($response['Sale']) && $this->itemsCount($response) > 0) { return $response['Sale']; } diff --git a/tests/src/Provider/LightspeedTest.php b/tests/src/Provider/LightspeedTest.php new file mode 100644 index 0000000..c3c0260 --- /dev/null +++ b/tests/src/Provider/LightspeedTest.php @@ -0,0 +1,175 @@ +provider = new Lightspeed([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'accountId' => static::ACCOUNT_ID, + ]); + } + + public function tearDown() + { + m::close(); + parent::tearDown(); + } + + public function testAuthorizationUrl() + { + $url = $this->provider->getAuthorizationUrl(); + $uri = parse_url($url); + parse_str($uri['query'], $query); + + $this->assertArrayHasKey('client_id', $query); + $this->assertArrayHasKey('redirect_uri', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('scope', $query); + $this->assertArrayHasKey('response_type', $query); + $this->assertArrayHasKey('approval_prompt', $query); + $this->assertNotNull($this->provider->getState()); + } + + public function testGetBaseAccessTokenUrl() + { + $url = $this->provider->getBaseAccessTokenUrl([]); + $uri = parse_url($url); + $accountId = static::ACCOUNT_ID; + + $this->assertEquals('/oauth/access_token.php', $uri['path']); + } + + public function testGetAccessToken() + { + $response = m::mock('Psr\Http\Message\ResponseInterface'); + $response->shouldReceive('getHeader') + ->times(1) + ->andReturn('application/json'); + $response->shouldReceive('getBody') + ->times(1) + ->andReturn('{"access_token":"mock_access_token","token_type":"bearer","expires_in":3600}'); + + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send')->times(1)->andReturn($response); + $this->provider->setHttpClient($client); + + $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + + $this->assertEquals('mock_access_token', $token->getToken()); + $this->assertLessThanOrEqual(time() + 3600, $token->getExpires()); + $this->assertGreaterThanOrEqual(time(), $token->getExpires()); + $this->assertNull($token->getRefreshToken(), 'Lightspeed does not support refresh tokens. Expected null.'); + $this->assertNull($token->getResourceOwnerId(), 'Lightspeed does not return user ID with access token. Expected null.'); + } + + public function testCanGetALongLivedAccessTokenFromShortLivedOne() + { + $response = m::mock('Psr\Http\Message\ResponseInterface'); + $response->shouldReceive('getHeader') + ->times(1) + ->andReturn('application/json'); + $response->shouldReceive('getBody') + ->times(1) + ->andReturn('{"access_token":"long-lived-token","token_type":"bearer","expires_in":3600}'); + + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send')->times(1)->andReturn($response); + $this->provider->setHttpClient($client); + + $token = $this->provider->getLongLivedAccessToken('short-lived-token'); + + $this->assertEquals('long-lived-token', $token->getToken()); + } + + /** + * @expectedException \League\OAuth2\Client\Provider\Exception\LightspeedProviderException + */ + public function testTryingToRefreshAnAccessTokenWillThrow() + { + $this->provider->getAccessToken('foo', ['refresh_token' => 'foo_token']); + } + + public function testScopes() + { + $this->assertEquals(['employee:all'], $this->provider->getDefaultScopes()); + } + + // public function testUserData() + // { + // $provider = new FooLightspeedProvider([ + // 'accountId' => static::ACCOUNT_ID, + // ]); + + // $token = m::mock('League\OAuth2\Client\Token\AccessToken'); + // $user = $provider->getResourceOwner($token); + + // $this->assertEquals(12345, $user->getAccountId($token)); + // } + + /** + * @expectedException \InvalidArgumentException + */ + // public function testNotSettingADefaultAccountIdWillThrow() + // { + // new Lightspeed([ + // 'clientId' => 'mock_client_id', + // 'clientSecret' => 'mock_secret', + // 'redirectUri' => 'none', + // 'accountId' => , + // ]); + // } + + public function testProperlyHandlesErrorResponses() + { + $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getHeader') + ->times(1) + ->andReturn('application/json'); + $postResponse->shouldReceive('getBody') + ->times(1) + ->andReturn('{"error":{"message":"Foo auth error","type":"OAuthException","code":191}}'); + + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send')->times(1)->andReturn($postResponse); + $this->provider->setHttpClient($client); + + $errorMessage = ''; + $errorCode = 0; + + try { + $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + } catch (IdentityProviderException $e) { + $errorMessage = $e->getMessage(); + $errorCode = $e->getCode(); + } + + $this->assertEquals('OAuthException: Foo auth error', $errorMessage); + $this->assertEquals(191, $errorCode); + } +}