diff --git a/.env.dist b/.env.dist index 0bcd1a4..bd2e8b5 100644 --- a/.env.dist +++ b/.env.dist @@ -1,4 +1,5 @@ ENDPOINT=https://api.taobao.com/router/rest APP_KEY=00000000 APP_SECRET=d784fa8b6d98d27699781bd9a7cf19f0 +REDIRECT_URI=https://example.com SESSION=test diff --git a/.travis.yml b/.travis.yml index d60a5ce..f94e5e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ php: - '7.4' before_script: + - cp .env.dist .env - flags="-o" - composer install $flags diff --git a/src/Builder/AuthorizationUriBuilder.php b/src/Builder/AuthorizationUriBuilder.php new file mode 100644 index 0000000..aeb4be7 --- /dev/null +++ b/src/Builder/AuthorizationUriBuilder.php @@ -0,0 +1,91 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Builder; + +use BadMethodCallException; +use RetailCrm\Interfaces\BuilderInterface; + +/** + * Class AuthorizationUriBuilder + * + * @category AuthorizationUriBuilder + * @package RetailCrm\Builder + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class AuthorizationUriBuilder implements BuilderInterface +{ + private const AUTHORIZE_URI = 'https://oauth.aliexpress.com/authorize'; + + /** + * @var string $appKey + */ + private $appKey; + + /** + * @var string $redirectUri + */ + private $redirectUri; + + /** + * @var bool $withState + */ + private $withState; + + /** + * AuthorizationUriBuilder constructor. + * + * @param string $appKey + * @param string $redirectUri + * @param bool $withState Set to true if state should be present in the URI + * + * It doesn't violate SRP because this class doesn't do anything besides URI generation. + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function __construct(string $appKey, string $redirectUri, bool $withState = false) + { + $this->appKey = $appKey; + $this->redirectUri = $redirectUri; + $this->withState = $withState; + } + + /** + * @inheritDoc + */ + public function build(): string + { + return self::AUTHORIZE_URI . '?' . http_build_query($this->getParams()); + } + + /** + * @return array + * @throws BadMethodCallException + */ + private function getParams(): array + { + if (empty($this->redirectUri)) { + throw new BadMethodCallException('Redirect URI should not be empty'); + } + + return array_filter([ + 'client_id' => $this->appKey, + 'response_type' => 'code', + 'redirect_uri' => $this->redirectUri, + 'sp' => 'ae', + 'state' => $this->withState ? uniqid('aeauth', true) : null, + 'view' => 'web' + ]); + } +} diff --git a/src/Builder/ClientBuilder.php b/src/Builder/ClientBuilder.php index f90be1f..4c6524b 100644 --- a/src/Builder/ClientBuilder.php +++ b/src/Builder/ClientBuilder.php @@ -15,6 +15,7 @@ namespace RetailCrm\Builder; use RetailCrm\Component\Constants; use RetailCrm\Component\ServiceLocator; use RetailCrm\Interfaces\AppDataInterface; +use RetailCrm\Interfaces\AuthenticatorInterface; use RetailCrm\Interfaces\BuilderInterface; use RetailCrm\Interfaces\ContainerAwareInterface; use RetailCrm\Interfaces\TopRequestFactoryInterface; @@ -39,6 +40,9 @@ class ClientBuilder implements ContainerAwareInterface, BuilderInterface /** @var \RetailCrm\Interfaces\AppDataInterface $appData */ private $appData; + /** @var \RetailCrm\Interfaces\AuthenticatorInterface $authenticator */ + private $authenticator; + /** * @return static */ @@ -58,6 +62,17 @@ class ClientBuilder implements ContainerAwareInterface, BuilderInterface return $this; } + /** + * @param \RetailCrm\Interfaces\AuthenticatorInterface $authenticator + * + * @return ClientBuilder + */ + public function setAuthenticator(AuthenticatorInterface $authenticator): ClientBuilder + { + $this->authenticator = $authenticator; + return $this; + } + /** * @return \RetailCrm\TopClient\Client * @throws \RetailCrm\Component\Exception\ValidationException @@ -71,6 +86,11 @@ class ClientBuilder implements ContainerAwareInterface, BuilderInterface $client->setRequestFactory($this->container->get(TopRequestFactoryInterface::class)); $client->setServiceLocator($this->container->get(ServiceLocator::class)); $client->setProcessor($this->container->get(TopRequestProcessorInterface::class)); + + if (null !== $this->authenticator) { + $client->setAuthenticator($this->authenticator); + } + $client->validateSelf(); return $client; diff --git a/src/Component/AppData.php b/src/Component/AppData.php index ed183dd..cdbb4fc 100644 --- a/src/Component/AppData.php +++ b/src/Component/AppData.php @@ -50,6 +50,11 @@ class AppData implements AppDataInterface */ private $appSecret; + /** + * @var string $redirectUri + */ + private $redirectUri; + /** * AppData constructor. * @@ -57,11 +62,12 @@ class AppData implements AppDataInterface * @param string $appKey * @param string $appSecret */ - public function __construct(string $serviceUrl, string $appKey, string $appSecret) + public function __construct(string $serviceUrl, string $appKey, string $appSecret, string $redirectUri = '') { - $this->serviceUrl = $serviceUrl; - $this->appKey = $appKey; - $this->appSecret = $appSecret; + $this->serviceUrl = $serviceUrl; + $this->appKey = $appKey; + $this->appSecret = $appSecret; + $this->redirectUri = $redirectUri; } /** @@ -87,4 +93,12 @@ class AppData implements AppDataInterface { return $this->appSecret; } + + /** + * @return string + */ + public function getRedirectUri(): string + { + return $this->redirectUri; + } } diff --git a/src/Component/Authenticator/TokenAuthenticator.php b/src/Component/Authenticator/TokenAuthenticator.php new file mode 100644 index 0000000..0cb3efe --- /dev/null +++ b/src/Component/Authenticator/TokenAuthenticator.php @@ -0,0 +1,52 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Component\Authenticator; + +use RetailCrm\Interfaces\AuthenticatorInterface; +use RetailCrm\Model\Request\BaseRequest; + +/** + * Class TokenAuthenticator + * + * @category TokenAuthenticator + * @package RetailCrm\Component\Authenticator + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class TokenAuthenticator implements AuthenticatorInterface +{ + /** + * @var string $token + */ + private $token; + + /** + * TokenAuthenticator constructor. + * + * @param string $token + */ + public function __construct(string $token) + { + $this->token = $token; + } + + /** + * @inheritDoc + */ + public function authenticate(BaseRequest $request): void + { + $request->session = $this->token; + } +} diff --git a/src/Interfaces/AppDataInterface.php b/src/Interfaces/AppDataInterface.php index dd034ae..6deb430 100644 --- a/src/Interfaces/AppDataInterface.php +++ b/src/Interfaces/AppDataInterface.php @@ -39,4 +39,9 @@ interface AppDataInterface * @return string */ public function getAppSecret(): string; + + /** + * @return string + */ + public function getRedirectUri(): string; } diff --git a/src/Interfaces/AuthenticatorInterface.php b/src/Interfaces/AuthenticatorInterface.php new file mode 100644 index 0000000..6218829 --- /dev/null +++ b/src/Interfaces/AuthenticatorInterface.php @@ -0,0 +1,36 @@ + + * @license MIT https://mit-license.org + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Interfaces; + +use RetailCrm\Model\Request\BaseRequest; + +/** + * Interface AuthenticatorInterface + * + * @category AuthenticatorInterface + * @package RetailCrm\Interfaces + * @author RetailDriver LLC + * @license MIT https://mit-license.org + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +interface AuthenticatorInterface +{ + /** + * Authenticate provided request + * + * @param \RetailCrm\Model\Request\BaseRequest $request + */ + public function authenticate(BaseRequest $request): void; +} diff --git a/src/TopClient/Client.php b/src/TopClient/Client.php index 0502490..3c3d8fc 100644 --- a/src/TopClient/Client.php +++ b/src/TopClient/Client.php @@ -14,12 +14,15 @@ namespace RetailCrm\TopClient; use DateTime; use JMS\Serializer\SerializerInterface; +use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\StreamInterface; +use RetailCrm\Builder\AuthorizationUriBuilder; use RetailCrm\Component\Exception\TopApiException; use RetailCrm\Component\Exception\TopClientException; use RetailCrm\Component\ServiceLocator; use RetailCrm\Interfaces\AppDataInterface; +use RetailCrm\Interfaces\AuthenticatorInterface; use RetailCrm\Interfaces\TopRequestFactoryInterface; use RetailCrm\Interfaces\TopRequestProcessorInterface; use RetailCrm\Model\Request\BaseRequest; @@ -80,6 +83,11 @@ class Client */ protected $processor; + /** + * @var \RetailCrm\Interfaces\AuthenticatorInterface $authenticator + */ + protected $authenticator; + /** * Client constructor. * @@ -130,14 +138,6 @@ class Client $this->serviceLocator = $serviceLocator; } - /** - * @return \RetailCrm\Component\ServiceLocator - */ - public function getServiceLocator(): ServiceLocator - { - return $this->serviceLocator; - } - /** * @param \RetailCrm\Interfaces\TopRequestProcessorInterface $processor * @@ -150,10 +150,45 @@ class Client } /** + * @param \RetailCrm\Interfaces\AuthenticatorInterface $authenticator + * + * @return Client + */ + public function setAuthenticator(AuthenticatorInterface $authenticator): Client + { + $this->authenticator = $authenticator; + return $this; + } + + /** + * @return \RetailCrm\Component\ServiceLocator + */ + public function getServiceLocator(): ServiceLocator + { + return $this->serviceLocator; + } + + /** + * @param bool $withState + * + * @return string + * + * $withState is passed to AuthorizationUriBuilder. + * @see AuthorizationUriBuilder::__construct + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function getAuthorizationUri(bool $withState = false): string + { + $builder = new AuthorizationUriBuilder($this->appData->getAppKey(), $this->appData->getAppSecret(), $withState); + return $builder->build(); + } + + /** + * Send TOP request + * * @param \RetailCrm\Model\Request\BaseRequest $request * * @return TopResponseInterface - * @throws \Psr\Http\Client\ClientExceptionInterface * @throws \RetailCrm\Component\Exception\ValidationException * @throws \RetailCrm\Component\Exception\FactoryException * @throws \RetailCrm\Component\Exception\TopClientException @@ -168,7 +203,12 @@ class Client $this->processor->process($request, $this->appData); $httpRequest = $this->requestFactory->fromModel($request, $this->appData); - $httpResponse = $this->httpClient->sendRequest($httpRequest); + + try { + $httpResponse = $this->httpClient->sendRequest($httpRequest); + } catch (ClientExceptionInterface $exception) { + throw new TopClientException(sprintf('Error sending request: %s', $exception->getMessage()), $exception); + } /** @var BaseResponse $response */ $response = $this->serializer->deserialize( @@ -188,6 +228,28 @@ class Client return $response; } + /** + * Send authenticated TOP request + * + * @param \RetailCrm\Model\Request\BaseRequest $request + * + * @return \RetailCrm\Model\Response\TopResponseInterface + * @throws \RetailCrm\Component\Exception\FactoryException + * @throws \RetailCrm\Component\Exception\TopApiException + * @throws \RetailCrm\Component\Exception\TopClientException + * @throws \RetailCrm\Component\Exception\ValidationException + */ + public function sendAuthenticatedRequest(BaseRequest $request): TopResponseInterface + { + if (null === $this->authenticator) { + throw new TopClientException('Authenticator is not provided'); + } + + $this->authenticator->authenticate($request); + + return $this->sendRequest($request); + } + /** * Returns body stream data (it should work like that in order to keep compatibility with some implementations). * diff --git a/tests/RetailCrm/Test/MatcherException.php b/tests/RetailCrm/Test/MatcherException.php new file mode 100644 index 0000000..180a2c9 --- /dev/null +++ b/tests/RetailCrm/Test/MatcherException.php @@ -0,0 +1,41 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Test; + +use Exception; +use Throwable; + +/** + * Class MatcherException + * + * @category MatcherException + * @package RetailCrm\Test + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class MatcherException extends Exception +{ + /** + * MatcherException constructor. + * + * @param string $message + * @param int $code + * @param \Throwable|null $previous + */ + public function __construct($message = "Cannot match any request", $code = 0, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/tests/RetailCrm/Test/TestCase.php b/tests/RetailCrm/Test/TestCase.php index 7decc76..8a85a9c 100644 --- a/tests/RetailCrm/Test/TestCase.php +++ b/tests/RetailCrm/Test/TestCase.php @@ -14,11 +14,13 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use RetailCrm\Builder\ContainerBuilder; use RetailCrm\Component\AppData; +use RetailCrm\Component\Authenticator\TokenAuthenticator; use RetailCrm\Component\Constants; use RetailCrm\Component\Environment; use RetailCrm\Component\Logger\StdoutLogger; use RetailCrm\Factory\FileItemFactory; use RetailCrm\Interfaces\AppDataInterface; +use RetailCrm\Interfaces\AuthenticatorInterface; use RetailCrm\Interfaces\FileItemFactoryInterface; /** @@ -65,8 +67,9 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase { return $this->getAppData( self::getenv('ENDPOINT', AppData::OVERSEAS_ENDPOINT), - self::getenv('APP_KEY', 'appKey'), - self::getenv('APP_SECRET', 'helloworld') + self::getEnvAppKey(), + self::getenv('APP_SECRET', 'helloworld'), + self::getenv('REDIRECT_URI', 'https://example.com'), ); } @@ -75,14 +78,32 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase * @param string $appKey * @param string $appSecret * + * @param string $redirectUri + * * @return \RetailCrm\Interfaces\AppDataInterface */ protected function getAppData( string $endpoint = AppData::OVERSEAS_ENDPOINT, string $appKey = 'appKey', - string $appSecret = 'helloworld' + string $appSecret = 'helloworld', + string $redirectUri = 'https://example.com' ): AppDataInterface{ - return new AppData($endpoint, $appKey, $appSecret); + return new AppData($endpoint, $appKey, $appSecret, $redirectUri); + } + + protected function getEnvTokenAuthenticator(): AuthenticatorInterface + { + return $this->getTokenAuthenticator(self::getenv('SESSION', 'test')); + } + + /** + * @param string $token + * + * @return \RetailCrm\Interfaces\AuthenticatorInterface + */ + protected function getTokenAuthenticator(string $token): AuthenticatorInterface + { + return new TokenAuthenticator($token); } /** @@ -156,6 +177,14 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase ->withBody($data); } + /** + * @return string + */ + protected static function getEnvAppKey(): string + { + return self::getenv('APP_KEY', 'appKey'); + } + /** * @param string $variable * @param mixed $default @@ -176,7 +205,9 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase */ protected static function getMockClient(): MockClient { - return new MockClient(); + $client = new MockClient(); + $client->setDefaultException(new MatcherException()); + return $client; } /** diff --git a/tests/RetailCrm/Tests/TopClient/ClientTest.php b/tests/RetailCrm/Tests/TopClient/ClientTest.php index 9bf6615..7d27366 100644 --- a/tests/RetailCrm/Tests/TopClient/ClientTest.php +++ b/tests/RetailCrm/Tests/TopClient/ClientTest.php @@ -59,7 +59,7 @@ class ClientTest extends TestCase $client = ClientBuilder::create() ->setContainer($this->getContainer($mockClient)) - ->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret')) + ->setAppData($this->getEnvAppData()) ->build(); $this->expectExceptionMessage($errorBody->msg); @@ -71,7 +71,7 @@ class ClientTest extends TestCase { $client = ClientBuilder::create() ->setContainer($this->getContainer(self::getMockClient())) - ->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret')) + ->setAppData($this->getEnvAppData()) ->build(); $request = new HttpDnsGetRequest(); @@ -118,7 +118,7 @@ EOF; RequestMatcher::createMatcher('api.taobao.com') ->setPath('/router/rest') ->setOptionalQueryParams([ - 'app_key' => 'appKey', + 'app_key' => self::getEnvAppKey(), 'method' => 'aliexpress.solution.seller.category.tree.query', 'category_id' => '5090300', 'filter_no_permission' => 1 @@ -127,7 +127,8 @@ EOF; ); $client = ClientBuilder::create() ->setContainer($this->getContainer($mock)) - ->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret')) + ->setAppData($this->getEnvAppData()) + ->setAuthenticator($this->getEnvTokenAuthenticator()) ->build(); $request = new SolutionSellerCategoryTreeQuery(); @@ -135,7 +136,7 @@ EOF; $request->filterNoPermission = true; /** @var SolutionSellerCategoryTreeQueryResponse $response */ - $result = $client->sendRequest($request); + $result = $client->sendAuthenticatedRequest($request); self::assertInstanceOf(SolutionSellerCategoryTreeQueryResponseData::class, $result->responseData); self::assertInstanceOf( @@ -184,14 +185,15 @@ EOF; RequestMatcher::createMatcher('api.taobao.com') ->setPath('/router/rest') ->setOptionalQueryParams([ - 'app_key' => 'appKey', + 'app_key' => self::getEnvAppKey(), 'method' => 'aliexpress.postproduct.redefining.categoryforecast' ]), $this->responseJson(200, $json) ); $client = ClientBuilder::create() ->setContainer($this->getContainer($mock)) - ->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret')) + ->setAppData($this->getEnvAppData()) + ->setAuthenticator($this->getEnvTokenAuthenticator()) ->build(); $request = new PostproductRedefiningCategoryForecast(); @@ -199,7 +201,7 @@ EOF; $request->locale = 'en'; /** @var PostproductRedefiningCategoryForecastResponse $response */ - $response = $client->sendRequest($request); + $response = $client->sendAuthenticatedRequest($request); self::assertInstanceOf(PostproductRedefiningCategoryForecastResponse::class, $response); self::assertEquals(