diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a612c0c5a35974f06e09e2d4d36bfed37630805b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +tests/ export-ignore +vendor/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.git/ export-ignore +.gitlab-ci.yml export-ignore +.idea export-ignore +apigen.neon export-ignore +build-coverage/ export-ignore +docs/ export-ignore +LICENSE.md export-ignore +phpcs.xml.dist export-ignore +phpunit-integration.xml export-ignore +phpunit-unit.xml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4b86223d6cae8ddb86d7cc3d12e0fcca43717f54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +.idea +composer.lock +build-coverage +swagger \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..db521c16d8bda63ee8608498ded2b1e6665ea5d0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,6 @@ +variables: + DISABLE_ACCEPTANCE: "1" + DISABLE_FUNCTIONAL: "1" + +include: 'https://gitlab.com/wpdesk/gitlab-ci/raw/master/gitlab-ci-1.2.yml' + diff --git a/README.md b/README.md index b6395ced1ce86a8e1b996dc337afea525a73484c..a8ccb8632fa21d89aadcc1eafb762fc90de91f06 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ -# wp-api-client +[](https://gitlab.com/wpdesk/wp-api-client/commits/master) +Integration: [](https://gitlab.com/wpdesk/wp-api-client/commits/master) +Unit: [](https://gitlab.com/wpdesk/wp-api-client/commits/master) +API Client +========== diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000000000000000000000000000000000000..03dc5509a4b06623a6e810075e831cf3beb91021 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,2 @@ += 1.0 - 2019-01-31 = +* First release diff --git a/composer.json b/composer.json index fc3057c3e3e0044a591aa4914bffc82420a48959..653e02bb5769f845145b5175665e1cdf12e261bc 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,8 @@ "require": { "php": ">=5.5", "psr/log": "^1.0.1", - "wpdesk/wp-cache": "dev-master", - "wpdesk/wp-http-client": "dev-master", + "wpdesk/wp-cache": "dev-feature/first-release", + "wpdesk/wp-http-client": "dev-feature/first-release", "psr/simple-cache": "^1.0" }, "require-dev": { diff --git a/phpunit-integration.xml b/phpunit-integration.xml new file mode 100644 index 0000000000000000000000000000000000000000..4a342abf125d638d044bafa01cd9a75a771b4b3d --- /dev/null +++ b/phpunit-integration.xml @@ -0,0 +1,28 @@ +<phpunit bootstrap="tests/integration/bootstrap.php" + backupGlobals="false" + > + <testsuites> + <testsuite> + <directory prefix="Test" suffix=".php">./tests/integration</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory suffix=".php">src</directory> + </whitelist> + </filter> + + <logging> + <log type="junit" target="build-coverage/report.junit.xml"/> + <log type="coverage-html" target="build-coverage/coverage" charset="UTF-8" yui="true" highlight="true"/> + <log type="coverage-text" target="build-coverage/coverage.txt"/> + <log type="coverage-clover" target="build-coverage/clover.xml"/> + </logging> + + <php> + <env name="WP_DEVELOP_DIR" value="/tmp/wordpress-develop"/> + <env name="WC_DEVELOP_DIR" value="/tmp/woocommerce"/> + </php> + +</phpunit> \ No newline at end of file diff --git a/phpunit-unit.xml b/phpunit-unit.xml new file mode 100644 index 0000000000000000000000000000000000000000..31e5c9f9c202769cac9487ac192f331e827c9489 --- /dev/null +++ b/phpunit-unit.xml @@ -0,0 +1,21 @@ +<phpunit bootstrap="tests/unit/bootstrap.php"> + <testsuites> + <testsuite> + <directory prefix="Test" suffix=".php">./tests/unit/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory suffix=".php">src</directory> + </whitelist> + </filter> + + <logging> + <log type="junit" target="build-coverage/report.junit.xml"/> + <log type="coverage-html" target="build-coverage/coverage" charset="UTF-8" yui="true" highlight="true"/> + <log type="coverage-text" target="build-coverage/coverage.txt"/> + <log type="coverage-clover" target="build-coverage/clover.xml"/> + </logging> + +</phpunit> diff --git a/src/Authentication/JWTSaasToken.php b/src/Authentication/JWTSaasToken.php new file mode 100644 index 0000000000000000000000000000000000000000..3465b2149140ea0146d231d3fdc33e565224fe9f --- /dev/null +++ b/src/Authentication/JWTSaasToken.php @@ -0,0 +1,66 @@ +<?php + +namespace WPDesk\ApiClient\Authentication; + +class JWTSaasToken implements Token +{ + const SHOP_ID_PARAM = 'shop'; + + const ROLE_PARAM = 'ROLE_SHOP'; + + + /** @var JWTToken */ + private $token; + + /** + * JWTToken constructor. + * @param string $token + */ + public function __construct(JWTToken $token) + { + $this->token = $token; + } + + public function getAuthString() + { + return $this->token->getAuthString(); + } + + public function isExpired() + { + return $this->token->isExpired(); + } + + public function isSignatureValid() + { + return $this->token->isSignatureValid(); + } + + public function __toString() + { + return $this->token->__toString(); + } + + /** + * If there is shop id in the token + * + * @return bool + */ + public function hasShopId() + { + $info = $this->token->getDecodedPublicTokenInfo(); + return !empty($info[self::SHOP_ID_PARAM]) && in_array(self::ROLE_PARAM, $info['roles']); + } + + /** + * Get shop id from token + * + * @return int + */ + public function getShopId() + { + $info = $this->token->getDecodedPublicTokenInfo(); + return (int)$info[self::SHOP_ID_PARAM]; + } + +} \ No newline at end of file diff --git a/src/Authentication/JWTToken.php b/src/Authentication/JWTToken.php new file mode 100644 index 0000000000000000000000000000000000000000..c6eaccf93e15be47f5be0ff37e1ec27b8383f993 --- /dev/null +++ b/src/Authentication/JWTToken.php @@ -0,0 +1,81 @@ +<?php + +namespace WPDesk\ApiClient\Authentication; + +class JWTToken implements Token +{ + const CONSIDER_EXPIRED_WHEN_LESS = 2; + + const EXPIRED_IN_SECONDS_PARAM = 'exp'; + + /** @var string */ + private $token; + + /** + * JWTToken constructor. + * @param string $token + */ + public function __construct($token) + { + $this->token = $token; + } + + /** + * @return string + */ + public function __toString() + { + return $this->token; + } + + /** + * Get string to perform authentication + * + * @return string + */ + public function getAuthString() + { + return 'Bearer ' . $this->__toString(); + } + + /** + * Returns public data from token + * + * @return array + */ + public function getDecodedPublicTokenInfo() + { + $tokenParts = explode('.', $this->__toString()); + if (!empty($tokenParts[1])) { + $infoPart = base64_decode($tokenParts[1]); + return json_decode($infoPart, true); + } + return []; + } + + /** + * Is token expired or very soon to be expired? + * + * @return bool + */ + public function isExpired() + { + $tokenInfo = $this->getDecodedPublicTokenInfo(); + if (!empty($tokenInfo[self::EXPIRED_IN_SECONDS_PARAM])) { + return $tokenInfo[self::EXPIRED_IN_SECONDS_PARAM] - time() < self::CONSIDER_EXPIRED_WHEN_LESS; + } + return true; + } + + /** + * Validates token signature + * + * @return bool + */ + public function isSignatureValid() + { + // @TODO + return true; + } + +} \ No newline at end of file diff --git a/src/Authentication/NullToken.php b/src/Authentication/NullToken.php new file mode 100644 index 0000000000000000000000000000000000000000..d46c595e2cc99d1b8a6970eede5f04e277350c33 --- /dev/null +++ b/src/Authentication/NullToken.php @@ -0,0 +1,54 @@ +<?php + +namespace WPDesk\ApiClient\Authentication; + +/** + * Null object pattern + * + * @package WPDesk\SaasPlatformClient\Authentication + */ +class NullToken implements Token +{ + public function __construct() + { + } + + /** + * @return string + */ + public function __toString() + { + return ''; + } + + /** + * Get string to perform authentication + * + * @return string + */ + public function getAuthString() + { + return ''; + } + + /** + * Is token expired or very soon to be expired? + * + * @return bool + */ + public function isExpired() + { + return true; + } + + /** + * Validates token signature + * + * @return bool + */ + public function isSignatureValid() + { + return false; + } + +} \ No newline at end of file diff --git a/src/Authentication/Token.php b/src/Authentication/Token.php new file mode 100644 index 0000000000000000000000000000000000000000..bc5a98e6bd53d1a37f068eb051d6d0d0620438bc --- /dev/null +++ b/src/Authentication/Token.php @@ -0,0 +1,32 @@ +<?php + +namespace WPDesk\ApiClient\Authentication; + +interface Token +{ + /** + * Get string to perform authentication + * + * @return string + */ + public function getAuthString(); + + /** + * Is token expired or very soon to be expired? + * + * @return bool + */ + public function isExpired(); + + /** + * Validates token signature + * + * @return bool + */ + public function isSignatureValid(); + + /** + * @return string + */ + public function __toString(); +} \ No newline at end of file diff --git a/src/Client/ApiClientOptions.php b/src/Client/ApiClientOptions.php new file mode 100644 index 0000000000000000000000000000000000000000..808f292cc76300f17879b7ea4d9c7ff62d3b4da1 --- /dev/null +++ b/src/Client/ApiClientOptions.php @@ -0,0 +1,32 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use Psr\Log\LoggerInterface; +use WPDesk\ApiClient\Serializer\SerializerOptions; +use WPDesk\HttpClient\HttpClientOptions; + +interface ApiClientOptions extends HttpClientOptions, SerializerOptions +{ + + /** + * @return LoggerInterface + */ + public function getLogger(); + + /** + * @return string + */ + public function getApiUrl(); + + /** + * @return array + */ + public function getDefaultRequestHeaders(); + + /** + * @return bool + */ + public function isCachedClient(); + +} \ No newline at end of file diff --git a/src/Client/CachedClient.php b/src/Client/CachedClient.php new file mode 100644 index 0000000000000000000000000000000000000000..3c6ee45eaf5147791ff18ab96cf7957ea2b54497 --- /dev/null +++ b/src/Client/CachedClient.php @@ -0,0 +1,111 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use Psr\SimpleCache\CacheInterface; +use WPDesk\Cache\CacheDispatcher; +use WPDesk\Cache\CacheItemCreator; +use WPDesk\Cache\CacheItemVerifier; +use WPDesk\HttpClient\HttpClient; +use WPDesk\ApiClient\Request\Request; +use WPDesk\ApiClient\Response\Response; +use WPDesk\ApiClient\Serializer\Serializer; + +class CachedClient implements Client, CacheItemCreator, CacheItemVerifier +{ + + /** @var Client */ + private $client; + + /** @var CacheInterface */ + private $cache; + + /** + * @var CacheDispatcher + */ + private $cacheDispatcher; + + /** + * CachedClient constructor. + * @param Client $decorated Decorated client + * @param CacheInterface $cache + */ + public function __construct(Client $decorated, CacheInterface $cache) + { + $this->client = $decorated; + $this->cache = $cache; + $this->cacheDispatcher = new CacheDispatcher($cache, [new RequestCacheInfoResolver()]); + } + + /** + * Create item to cache. + * + * @param Request $request + * @return Response + */ + public function createCacheItem($request) + { + return $this->client->sendRequest($request); + } + + /** + * Verify cache item. + * + * @param $object + * @return Response; + */ + public function getVerifiedItemOrNull($object) + { + if ($object instanceof Response) { + return $object; + } + return null; + } + + /** + * Send request. + * + * @param Request $request + * @return mixed|Response + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function sendRequest(Request $request) + { + $response = $this->cacheDispatcher->dispatch($request, $this, $this); + return $response; + } + + /** + * @return HttpClient + */ + public function getHttpClient() + { + return $this->client->getHttpClient(); + } + + /** + * @param HttpClient $client + * @return mixed + */ + public function setHttpClient(HttpClient $client) + { + return $this->client->setHttpClient($client); + } + + /** + * @return Serializer + */ + public function getSerializer() + { + return $this->client->getSerializer(); + } + + /** + * @return string + */ + public function getApiUrl() + { + return $this->client->getApiUrl(); + } + +} \ No newline at end of file diff --git a/src/Client/Client.php b/src/Client/Client.php new file mode 100644 index 0000000000000000000000000000000000000000..c3372f9f39e47bf7a115e4915da46282c2de19d9 --- /dev/null +++ b/src/Client/Client.php @@ -0,0 +1,41 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use WPDesk\HttpClient\HttpClient; +use WPDesk\ApiClient\Request\Request; +use WPDesk\ApiClient\Response\Response; +use WPDesk\ApiClient\Serializer\Serializer; + +interface Client +{ + /** + * Send given request trough HttpClient + * + * @param Request $request + * @return Response + */ + public function sendRequest(Request $request); + + /** + * @return HttpClient + */ + public function getHttpClient(); + + /** + * @param HttpClient $client + */ + public function setHttpClient(HttpClient $client); + + /** + * @return Serializer + */ + public function getSerializer(); + + /** + * Returns api url. Always without ending / + * + * @return string + */ + public function getApiUrl(); +} \ No newline at end of file diff --git a/src/Client/ClientFactory.php b/src/Client/ClientFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..81ad8aa30ed2b52729e11cd02262e95c84277746 --- /dev/null +++ b/src/Client/ClientFactory.php @@ -0,0 +1,34 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use WPDesk\Cache\WordpressCache; +use WPDesk\HttpClient\HttpClientFactory; +use WPDesk\ApiClient\Serializer\SerializerFactory; + +class ClientFactory +{ + /** + * @param ApiClientOptions $options + * @return Client + */ + public function createClient(ApiClientOptions $options) + { + $httpClientFactory = new HttpClientFactory(); + $serializerFactory = new SerializerFactory(); + + $client = new ClientImplementation( + $httpClientFactory->createClient($options), + $serializerFactory->createSerializer($options), + $options->getLogger(), + $options->getApiUrl(), + $options->getDefaultRequestHeaders() + ); + + if ($options->isCachedClient()) { + $client = new CachedClient($client, new WordpressCache()); + } + + return $client; + } +} \ No newline at end of file diff --git a/src/Client/ClientImplementation.php b/src/Client/ClientImplementation.php new file mode 100644 index 0000000000000000000000000000000000000000..feaeb75b00947472676c99209b8aab814a9722f8 --- /dev/null +++ b/src/Client/ClientImplementation.php @@ -0,0 +1,214 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use WPDesk\HttpClient\HttpClient; +use WPDesk\HttpClient\HttpClientResponse; +use WPDesk\ApiClient\Request\Request; +use WPDesk\ApiClient\Response\RawResponse; +use WPDesk\ApiClient\Response\Response; +use WPDesk\ApiClient\Serializer\Serializer; +use WPDesk\HttpClient\HttpClientRequestException; + +class ClientImplementation implements Client, LoggerAwareInterface +{ + const CLIENT_VERSION = '1.6.5'; + + const DEFAULT_TIMEOUT = 10; + + /** @var HttpClient */ + private $client; + + /** @var Serializer */ + private $serializer; + + /** @var LoggerInterface */ + private $logger; + + /** @var string */ + private $apiUrl; + + /** @var array */ + private $defaultRequestHeaders; + + /** + * Client constructor. + * @param HttpClient $client + * @param Serializer $serializer + * @param LoggerInterface $logger + * @param string $apiUri + * @param array $defaultRequestHeaders + */ + public function __construct( + HttpClient $client, + Serializer $serializer, + LoggerInterface $logger, + $apiUri, + array $defaultRequestHeaders + ) { + $this->client = $client; + $this->serializer = $serializer; + $this->logger = $logger; + $this->apiUrl = $apiUri; + $this->defaultRequestHeaders = $defaultRequestHeaders; + } + + /** + * Send given request trough HttpClient + * + * @param Request $request + * @throws HttpClientRequestException + * @return Response + */ + public function sendRequest(Request $request) + { + $this->logger->debug("Sends request with METHOD: {$request->getMethod()}; to ENDPOINT {$request->getEndpoint()}", + $this->getLoggerContext()); + try { + $httpResponse = $this->client->send( + $fullUrl = $this->prepareFullUrl($request), + $method = $request->getMethod(), + $body = $this->prepareRequestBody($request), + $headers = $this->prepareRequestHeaders($request), self::DEFAULT_TIMEOUT + ); + + $this->logger->debug( + "Sent request with: URL: {$fullUrl};\n METHOD: {$method};\n BODY: {$body};\n" + . "HEADERS: " . json_encode($headers) . "\n\n and got response as CODE: {$httpResponse->getResponseCode()};\n" + . "with RESPONSE BODY {$httpResponse->getBody()}", + $this->getLoggerContext()); + + + return $this->mapHttpResponseToApiResponse($httpResponse); + } catch (HttpClientRequestException $e) { + $this->logger->error("Exception {$e->getMessage()}; {$e->getCode()} occurred while sending request"); + throw $e; + } + } + + /** + * Returns full request url with endpoint + * + * @param Request $request + * @return string + */ + private function prepareFullUrl(Request $request) + { + $endpoint = $request->getEndpoint(); + if (strpos('http', $endpoint) === 0) { + return $endpoint; + } + return $this->getApiUrl() . $endpoint; + } + + /** + * Map response from http client to api response using serializer + * + * @param HttpClientResponse $response + * @return RawResponse + */ + private function mapHttpResponseToApiResponse(HttpClientResponse $response) + { + $apiResponse = new RawResponse( + $this->serializer->unserialize($response->getBody()), + $response->getResponseCode(), + $response->getHeaders() + ); + + return $apiResponse; + } + + /** + * Prepare serialized request body + * + * @param Request $request + * @return string + */ + private function prepareRequestBody(Request $request) + { + return $this->serializer->serialize($request->getBody()); + } + + /** + * Prepares array of http headers + * + * @param Request $request + * @return array + */ + private function prepareRequestHeaders(Request $request) + { + $headers = array( + 'User-Agent' => 'saas-client-' . self::CLIENT_VERSION, + 'Accept-Encoding' => '*', + 'Content-Type' => $this->serializer->getMime() + ); + $headers = array_merge($this->defaultRequestHeaders, $headers); + return array_merge($headers, $request->getHeaders()); + } + + /** + * @return HttpClient + */ + public function getHttpClient() + { + return $this->client; + } + + /** + * @param HttpClient $client + */ + public function setHttpClient(HttpClient $client) + { + $this->client = $client; + } + + /** + * @return Serializer + */ + public function getSerializer() + { + return $this->serializer; + } + + /** + * Returns api url. Always without ending / + * + * @return string + */ + public function getApiUrl() + { + return trim($this->apiUrl, '/'); + } + + /** + * Sets logger + * + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Returns logger context for + * + * @param string $additional_context Optional additional context + * @return array + */ + protected function getLoggerContext($additional_context = '') + { + $context = [ + Platform::LIBARY_LOGIN_CONTEXT, + self::class + ]; + if ($additional_context !== '') { + $context[] = $additional_context; + } + return $context; + } + + +} \ No newline at end of file diff --git a/src/Client/RequestCacheInfoResolver.php b/src/Client/RequestCacheInfoResolver.php new file mode 100644 index 0000000000000000000000000000000000000000..43a0b7e3d013b754b1bc2149f22c063a19f9a4e1 --- /dev/null +++ b/src/Client/RequestCacheInfoResolver.php @@ -0,0 +1,139 @@ +<?php + +namespace WPDesk\ApiClient\Client; + +use WPDesk\Cache\CacheInfoResolver; +use WPDesk\Cache\HowToCache; +use WPDesk\SaasPlatformClient\Request\AuthRequest; +use WPDesk\SaasPlatformClient\Request\BasicRequest; +use WPDesk\SaasPlatformClient\Request\Request; +use WPDesk\SaasPlatformClient\Request\ShippingServicesSettings\PutSettingsRequest; +use WPDesk\SaasPlatformClient\Request\Status\GetStatusRequest; +use WPDesk\SaasPlatformClient\Response\ApiResponse; +use WPDesk\SaasPlatformClient\Response\RawResponse; + +class RequestCacheInfoResolver implements CacheInfoResolver +{ + + const DEFAULT_CACHE_TTL = 86400; //24 hours + const CACHE_TTL_ONE_MINUTE = 60; + + const OPTION_FS_SAAS_PLATFORM_VERSION_HASH = 'fs-saas-platform-version-hash'; + + /** + * + * @param Request $request + * + * @return bool + */ + private function prepareCacheKey($request) + { + return md5($request->getEndpoint()); + } + + /** + * + * @param Request $request + * + * @return bool + */ + public function isSupported($request) + { + if ($request instanceof BasicRequest) { + return true; + } + return false; + } + + /** + * + * @param Request $request + * + * @return bool + */ + public function shouldCache($request) + { + if ($request instanceof ConnectKeyInfoRequest) { + return false; + } + if ($request instanceof GetStatusRequest) { + return false; + } + if ($request instanceof BasicRequest) { + if ('GET' === $request->getMethod()) { + return true; + } + } + return false; + } + + /** + * + * @param Request $request + * + * @return HowToCache + */ + public function prepareHowToCache($request) + { + $howToCache = new HowToCache($this->prepareCacheKey($request), self::DEFAULT_CACHE_TTL); + return $howToCache; + } + + /** + * @param ApiResponse $response + * + * @return bool + */ + private function isPlatformVersionFromResponseChanged(ApiResponse $response) + { + $stored_hash = get_option(self::OPTION_FS_SAAS_PLATFORM_VERSION_HASH, ''); + if ($stored_hash !== $response->getPlatformVersionHash()) { + return true; + } + return false; + } + + /** + * @param ApiResponse $response + */ + private function storePlatformVersionHashFromResponse(ApiResponse $response) + { + update_option(self::OPTION_FS_SAAS_PLATFORM_VERSION_HASH, $response->getPlatformVersionHash()); + } + + /** + * + * @param Request $request + * @param mixed $item + * + * @return bool + */ + public function shouldClearCache($request, $item) + { + if ($request instanceof PutSettingsRequest) { + return true; + } + if ($item instanceof ApiResponse && $this->isPlatformVersionFromResponseChanged($item)) { + $this->storePlatformVersionHashFromResponse($item); + return true; + } + return false; + } + + /** + * + * @param Request $request + * @param mixed $item + * + * @return string[] + */ + public function shouldClearKeys($request, $item) + { + if ('GET' !== $request->getMethod()) { + return [$this->prepareCacheKey($request)]; + } + return []; + } + + +} \ No newline at end of file diff --git a/src/Request/BasicRequest.php b/src/Request/BasicRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..ae0f599f099ec06cd26b8d551e1bfa4b6d44e844 --- /dev/null +++ b/src/Request/BasicRequest.php @@ -0,0 +1,54 @@ +<?php + +namespace WPDesk\ApiClient\Request; + + +class BasicRequest implements Request +{ + /** @var string */ + protected $method; + + /** @var array */ + protected $data; + + /** @var string */ + protected $endPoint; + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Return endpoint in format /[^/]+/ + * + * @return string + */ + public function getEndpoint() + { + return '/' . trim($this->endPoint, '/'); + } + + /** + * Returns array of http headers + * + * @return array + */ + public function getHeaders() + { + return array(); + } + + /** + * Return unserialized request body as array + * + * @return array + */ + public function getBody() + { + return $this->data; + } +} \ No newline at end of file diff --git a/src/Request/Request.php b/src/Request/Request.php new file mode 100644 index 0000000000000000000000000000000000000000..4789c80ddcd0e7a46323f58d52ea681f45a481cf --- /dev/null +++ b/src/Request/Request.php @@ -0,0 +1,27 @@ +<?php + +namespace WPDesk\ApiClient\Request; + + +interface Request +{ + /** + * @return string + */ + public function getMethod(); + + /** + * @return array + */ + public function getHeaders(); + + /** + * @return array + */ + public function getBody(); + + /** + * @return string + */ + public function getEndpoint(); +} \ No newline at end of file diff --git a/src/Response/ApiResponse.php b/src/Response/ApiResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..b782720c20c4bc2af45fb93b975e83023def0b09 --- /dev/null +++ b/src/Response/ApiResponse.php @@ -0,0 +1,35 @@ +<?php + +namespace WPDesk\ApiClient\Response; + + +interface ApiResponse extends Response +{ + /** + * Get links structure to the other request + * + * @return array + */ + public function getLinks(); + + /** + * Is it a BAD REQUEST response + * + * @return bool + */ + public function isBadRequest(); + + /** + * Is it a FATAL ERROR response + * + * @return bool + */ + public function isServerFatalError(); + + /** + * Is requested resource exists + * + * @return bool + */ + public function isNotExists(); +} \ No newline at end of file diff --git a/src/Response/AuthApiResponse.php b/src/Response/AuthApiResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..86f34fa9faf916700eabbed43f0cf3b7aae233ce --- /dev/null +++ b/src/Response/AuthApiResponse.php @@ -0,0 +1,14 @@ +<?php + +namespace WPDesk\ApiClient\Response; + +use WPDesk\ApiClient\Response\Traits\AuthApiResponseDecorator; + +class AuthApiResponse implements ApiResponse +{ + const RESPONSE_CODE_BAD_CREDENTIALS = 401; + + const RESPONSE_CODE_NOT_EXISTS = 404; + + use AuthApiResponseDecorator; +} \ No newline at end of file diff --git a/src/Response/ProtectedResponse.php b/src/Response/ProtectedResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..c5195d0a4c45d59d592483ce12c15fabbb43ea1e --- /dev/null +++ b/src/Response/ProtectedResponse.php @@ -0,0 +1,27 @@ +<?php + +namespace WPDesk\ApiClient\Response; + +use WPDesk\ApiClient\Response\Exception\TriedExtractDataFromErrorResponse; +use WPDesk\ApiClient\Response\Traits\ApiResponseDecorator; + +/** + * Response is protected in a way so when you try to get body of the response when an error occured you will get an exception + * + * Class ProtectedResponse + * @package WPDesk\ApiClient\Response + */ +class ProtectedResponse implements Response +{ + use ApiResponseDecorator; + + public function getResponseBody() + { + if ($this->isError()) { + throw TriedExtractDataFromErrorResponse::createWithClassInfo(get_class($this->rawResponse), $this->getResponseCode()); + } + return $this->rawResponse->getResponseBody(); + } + + +} \ No newline at end of file diff --git a/src/Response/RawResponse.php b/src/Response/RawResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..3662483b47731dffc49f66e3a273e34aac6f38ae --- /dev/null +++ b/src/Response/RawResponse.php @@ -0,0 +1,113 @@ +<?php + +namespace WPDesk\ApiClient\Response; + +class RawResponse implements Response +{ + const RESPONSE_CODE_SUCCESS = 200; + const RESPONSE_CODE_CREATED = 201; + const RESPONSE_CODE_ERROR_BAD_REQUEST = 400; + const RESPONSE_CODE_DOMAIN_NOT_ALLOWED = 462; + const RESPONSE_CODE_ERROR_FATAL = 500; + const RESPONSE_CODE_MAINTENANCE = 503; + const HEADER_X_PLATFORM_VERSION_HASH = 'X-Platform-Version-Hash'; + + /** @var array */ + private $data; + + /** @var int */ + private $code; + + /** @var array */ + private $headers; + + /** + * RawResponse constructor. + * @param array $body + * @param int $code + * @param array $headers + */ + public function __construct(array $body, $code, array $headers) + { + $this->data = $body; + $this->code = (int)$code; + $this->headers = $headers; + } + + /** + * Returns response http code + * + * @return int + */ + public function getResponseCode() + { + return $this->code; + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseBody() + { + return $this->data; + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseErrorBody() + { + return $this->data; + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseHeaders() + { + return $this->headers; + } + + /** + * Is any error occured + * + * @return bool + */ + public function isError() + { + $code = $this->getResponseCode(); + return ( $code < 200 || $code >= 300 ) && !$this->isMaintenance(); + } + + /** + * Is maintenance. + * + * @return bool + */ + public function isMaintenance() + { + $code = $this->getResponseCode(); + return self::RESPONSE_CODE_MAINTENANCE === $code; + } + + /** + * Get platform version hash string. + * + * @return bool|string + */ + public function getPlatformVersionHash() + { + if (isset($this->headers[self::HEADER_X_PLATFORM_VERSION_HASH])) { + return $this->headers[self::HEADER_X_PLATFORM_VERSION_HASH]; + } + return false; + } + + +} \ No newline at end of file diff --git a/src/Response/Response.php b/src/Response/Response.php new file mode 100644 index 0000000000000000000000000000000000000000..296b6fdf29635a86bb95bacb769a826f939f3437 --- /dev/null +++ b/src/Response/Response.php @@ -0,0 +1,43 @@ +<?php + +namespace WPDesk\ApiClient\Response; + + +interface Response +{ + /** + * @return int + */ + public function getResponseCode(); + + /** @return array */ + public function getResponseBody(); + + /** @return array */ + public function getResponseHeaders(); + + /** @return array */ + public function getResponseErrorBody(); + + /** + * Is any error occured + * + * @return bool + */ + public function isError(); + + /** + * Is maintenance + * + * @return bool + */ + public function isMaintenance(); + + /** + * Get platform version hash string. + * + * @return bool|string + */ + public function getPlatformVersionHash(); + +} \ No newline at end of file diff --git a/src/Response/Traits/ApiResponseDecorator.php b/src/Response/Traits/ApiResponseDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..0960c902925df1af012dfb2daadc2548ddceb700 --- /dev/null +++ b/src/Response/Traits/ApiResponseDecorator.php @@ -0,0 +1,145 @@ +<?php + +namespace WPDesk\ApiClient\Response\Traits; + +use WPDesk\ApiClient\Response\AuthApiResponse; +use WPDesk\ApiClient\Response\RawResponse; +use WPDesk\ApiClient\Response\Response; + +trait ApiResponseDecorator +{ + + /** @var RawResponse */ + private $rawResponse; + + /** + * RawResponseDecorator constructor. + * @param Response $rawResponse + */ + public function __construct(Response $rawResponse) + { + $this->rawResponse = $rawResponse; + } + + /** + * Returns response http code + * + * @return int + */ + public function getResponseCode() + { + return $this->rawResponse->getResponseCode(); + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseBody() + { + return $this->rawResponse->getResponseBody(); + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseErrorBody() + { + return $this->rawResponse->getResponseErrorBody(); + } + + /** + * Returns response body as array + * + * @return array + */ + public function getResponseHeaders() + { + return $this->rawResponse->getResponseHeaders(); + } + + /** + * Get links structure to the other request + * + * @return array + */ + public function getLinks() + { + $body = $this->getResponseBody(); + return $body['_links']; + } + + /** + * Is it a BAD REQUEST response + * + * @return bool + */ + public function isBadRequest() + { + return $this->getResponseCode() === RawResponse::RESPONSE_CODE_ERROR_BAD_REQUEST; + } + + /** + * Is it a DOMAIN NOT ALLOWED response + * + * @return bool + */ + public function isDomainNotAllowed() + { + return $this->getResponseCode() === RawResponse::RESPONSE_CODE_DOMAIN_NOT_ALLOWED; + } + + /** + * Is it a FATAL ERROR response + * + * @return bool + */ + public function isServerFatalError() + { + return $this->getResponseCode() === RawResponse::RESPONSE_CODE_ERROR_FATAL; + } + + /** + * Is any error occured + * + * @return bool + */ + public function isError() + { + return $this->rawResponse->isError(); + } + + /** + * Is requested resource exists + * + * @return bool + */ + public function isNotExists() + { + return $this->getResponseCode() === AuthApiResponse::RESPONSE_CODE_NOT_EXISTS; + } + + /** + * Is maintenance. + * + * @return bool + */ + public function isMaintenance() + { + return $this->rawResponse->isMaintenance(); + } + + /** + * Get platform version hash string. + * + * @return bool|string + */ + public function getPlatformVersionHash() + { + return $this->rawResponse->getPlatformVersionHash(); + } + + } \ No newline at end of file diff --git a/src/Response/Traits/AuthApiResponseDecorator.php b/src/Response/Traits/AuthApiResponseDecorator.php new file mode 100644 index 0000000000000000000000000000000000000000..a052865b28b0031e961e128f372481266534868a --- /dev/null +++ b/src/Response/Traits/AuthApiResponseDecorator.php @@ -0,0 +1,38 @@ +<?php + +namespace WPDesk\ApiClient\Response\Traits; + +use WPDesk\ApiClient\Response\AuthApiResponse; + +trait AuthApiResponseDecorator +{ + use ApiResponseDecorator; + + /** + * @return bool + */ + public function isBadCredentials() + { + return $this->getResponseCode() === AuthApiResponse::RESPONSE_CODE_BAD_CREDENTIALS; + } + + /** + * Is bad credential because token expires + * + * @return bool + */ + public function isTokenExpired() + { + return $this->isBadCredentials(); + } + + /** + * Is bad credential because token is invalid + * + * @return bool + */ + public function isTokenInvalid() + { + return $this->isBadCredentials(); + } +} \ No newline at end of file diff --git a/src/Response/Traits/PagedListImplementation.php b/src/Response/Traits/PagedListImplementation.php new file mode 100644 index 0000000000000000000000000000000000000000..23286478d3ca85eac7b62a7653000c08f496b502 --- /dev/null +++ b/src/Response/Traits/PagedListImplementation.php @@ -0,0 +1,44 @@ +<?php + +namespace WPDesk\ApiClient\Response\Traits; + +trait PagedListImplementation +{ + /* + * @return array + */ + public function getRawPage() + { + $body = $this->getResponseBody(); + if ($body['_embedded'] !== null && $body['_embedded']['item'] !== null) { + return $body['_embedded']['item']; + } + return []; + } + + /** + * @return int + */ + public function getPageCount() + { + return (int)floor($this->getItemCount() / $this->getItemsPerPage()); + } + + /** + * @return int + */ + public function getItemsPerPage() + { + $body = $this->getResponseBody(); + return (int)$body['itemsPerPage']; + } + + /** + * @return int + */ + public function getItemCount() + { + $body = $this->getResponseBody(); + return (int)$body['totalItems']; + } +} \ No newline at end of file diff --git a/src/Serializer/Exception/CannotUnserializeException.php b/src/Serializer/Exception/CannotUnserializeException.php new file mode 100644 index 0000000000000000000000000000000000000000..0d603480969fdee82a65974bd80ed4799ef15cd3 --- /dev/null +++ b/src/Serializer/Exception/CannotUnserializeException.php @@ -0,0 +1,13 @@ +<?php + +namespace WPDesk\ApiClient\Serializer\Exception; + +/** + * Thrown when serializer cannot unserialize string data + * + * @package WPDesk\ApiClient\Serializer\Exception + */ +class CannotUnserializeException extends \RuntimeException +{ + +} \ No newline at end of file diff --git a/src/Serializer/JsonSerializer.php b/src/Serializer/JsonSerializer.php new file mode 100644 index 0000000000000000000000000000000000000000..55fa230cd7cc2b5c63d0edfadabe86787559cbc6 --- /dev/null +++ b/src/Serializer/JsonSerializer.php @@ -0,0 +1,46 @@ +<?php + +namespace WPDesk\ApiClient\Serializer; + +use WPDesk\ApiClient\Serializer\Exception\CannotUnserializeException; + +class JsonSerializer implements Serializer +{ + + /** + * Convert data to string + * + * @param mixed $data + * @return string + */ + public function serialize($data) + { + return json_encode($data, JSON_FORCE_OBJECT); + } + + /** + * Convert string to php data + * + * @param string $data + * @return mixed + */ + public function unserialize($data) + { + $unserializedResult = json_decode($data, true); + if ($unserializedResult === null) { + throw new CannotUnserializeException("Cannot unserialize data: {$data}"); + } + + return $unserializedResult; + } + + /** + * @return string + */ + public function getMime() + { + return 'application/json'; + } + + +} \ No newline at end of file diff --git a/src/Serializer/Serializer.php b/src/Serializer/Serializer.php new file mode 100644 index 0000000000000000000000000000000000000000..1faa8f943c7d2d844e8790be0bf430bb3ee68727 --- /dev/null +++ b/src/Serializer/Serializer.php @@ -0,0 +1,23 @@ +<?php + +namespace WPDesk\ApiClient\Serializer; + +interface Serializer +{ + /** + * @param mixed $data + * @return string + */ + public function serialize($data); + + /** + * @param string $data + * @return mixed + */ + public function unserialize($data); + + /** + * @return string + */ + public function getMime(); +} \ No newline at end of file diff --git a/src/Serializer/SerializerFactory.php b/src/Serializer/SerializerFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..532099ea97383175574556b87f8cd7974223a6b2 --- /dev/null +++ b/src/Serializer/SerializerFactory.php @@ -0,0 +1,16 @@ +<?php + +namespace WPDesk\ApiClient\Serializer; + +class SerializerFactory +{ + /** + * @param SerializerOptions $options + * @return Serializer + */ + public function createSerializer(SerializerOptions $options) + { + $className = $options->getSerializerClass(); + return new $className; + } +} \ No newline at end of file diff --git a/src/Serializer/SerializerOptions.php b/src/Serializer/SerializerOptions.php new file mode 100644 index 0000000000000000000000000000000000000000..d1441bc68ffd14fe5ac8a209f661d3b61823c7ae --- /dev/null +++ b/src/Serializer/SerializerOptions.php @@ -0,0 +1,11 @@ +<?php + +namespace WPDesk\ApiClient\Serializer; + +interface SerializerOptions +{ + /** + * @return string + */ + public function getSerializerClass(); +} \ No newline at end of file diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2a86b036f682ed318b27b028d2e96dfcd5979afd --- /dev/null +++ b/tests/docker-compose.yaml @@ -0,0 +1,172 @@ +version: '2.0' + +services: + + wordpress: + image: wpdesknet/phpunit-woocommerce:0-0 + volumes: + - .././:/opt/project + depends_on: + - mysql0 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql0 + + wordpress-0-1: + image: wpdesknet/phpunit-woocommerce:0-1 + volumes: + - .././:/opt/project + depends_on: + - mysql1 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql1 + + wordpress-0-2: + image: wpdesknet/phpunit-woocommerce:0-2 + volumes: + - .././:/opt/project + depends_on: + - mysql2 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql2 + + wordpress-0-3: + image: wpdesknet/phpunit-woocommerce:0-3 + volumes: + - .././:/opt/project + depends_on: + - mysql3 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql3 + + wordpress-0-4: + image: wpdesknet/phpunit-woocommerce:0-4 + volumes: + - .././:/opt/project + depends_on: + - mysql4 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql4 + + wordpress-0-5: + image: wpdesknet/phpunit-woocommerce:0-5 + volumes: + - .././:/opt/project + depends_on: + - mysql5 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql5 + + wordpress-1-0: + image: wpdesknet/phpunit-woocommerce:1-0 + volumes: + - .././:/opt/project + depends_on: + - mysql0 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql0 + + wordpress-2-0: + image: wpdesknet/phpunit-woocommerce:2-0 + volumes: + - .././:/opt/project + depends_on: + - mysql0 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql0 + + wordpress-3-0: + image: wpdesknet/phpunit-woocommerce:3-0 + volumes: + - .././:/opt/project + depends_on: + - mysql0 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql0 + + wordpress-4-0: + image: wpdesknet/phpunit-woocommerce:4-0 + volumes: + - .././:/opt/project + depends_on: + - mysql0 + environment: + WORDPRESS_DB_NAME: wptest + WORDPRESS_DB_USER: mysql + WORDPRESS_DB_PASSWORD: mysql + WORDPRESS_DB_HOST: mysql0 + + mysql0: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + + mysql1: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + + mysql2: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + + mysql3: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + + mysql4: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + + mysql5: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: wptest + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php new file mode 100644 index 0000000000000000000000000000000000000000..a422fd9c7fea652a15b2aced0b2e6134a5590201 --- /dev/null +++ b/tests/integration/bootstrap.php @@ -0,0 +1,28 @@ +<?php + +ini_set('error_reporting', E_ALL); // or error_reporting(E_ALL); +ini_set('display_errors', '1'); +ini_set('display_startup_errors', '1'); + +require_once __DIR__ . '/../../vendor/autoload.php'; + +// disable xdebug backtrace +if ( function_exists( 'xdebug_disable' ) ) { + xdebug_disable(); +} + +if ( getenv( 'PLUGIN_PATH' ) !== false ) { + define( 'PLUGIN_PATH', getenv( 'PLUGIN_PATH' ) ); +} else { + define( 'PLUGIN_PATH', __DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR ); +} + +require_once( getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit/includes/functions.php' ); + +tests_add_filter( 'muplugins_loaded', function () { +}, 100 ); + +putenv('WP_TESTS_DIR=' . getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit'); +require_once( getenv( 'WC_DEVELOP_DIR' ) . '/tests/bootstrap.php' ); + +do_action('plugins_loaded'); \ No newline at end of file diff --git a/tests/unit/Authentication/TestJWTSaasToken.php b/tests/unit/Authentication/TestJWTSaasToken.php new file mode 100644 index 0000000000000000000000000000000000000000..526253d91e94aec5020a0d1381071b34f4bb1952 --- /dev/null +++ b/tests/unit/Authentication/TestJWTSaasToken.php @@ -0,0 +1,64 @@ +<?php + +use WPDesk\ApiClient\Authentication\JWTSaasToken; +use WPDesk\ApiClient\Authentication\JWTToken; + +class TestJWTSaasToken extends \PHPUnit\Framework\TestCase +{ + /** + * Provides tokens with info about expected shop id encoded inside + * + * @return array + */ + public function provideTokensWithShopInfo() + { + return [ + [ + 'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDA2NjQsImV4cCI6MTUzNDk0NDI2NCwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjoxfQ.rG6_U67zKTinkqR4324wOhS0YP9P9_2DH-OMsfijTPt408wNGwh5YKqkA4kglP5wMQQ80UGo7qyYd9R3fc465JMB9DQTbALATz_UMk8fr_-LWl9Gj8cuzQvEMQL2Saya_nggAn0bmWlVG27k6-6ezfBW7Hhb1d1GtoydbiYMA_ntLVPzrRjicvcIoftroadX6rsuCSHy-lLvHd1pj6E6eOEX2IcZXoa9eqd_3-j4dgszNUoPDag8bYXQzmIDIqyseBQ3eaQr69Tj-npTaMDjSyHwFQqtr57leX9aVVo8xPeF2ulrT4u4VgCv6kQt1Fao5_G6WLJ9FpJE19k_e4uebSwmDwiPoSnqU30tmIoVpcqfzxoNDKxfaK6am4HvfiQ6l_A2Cb7Wi9Wh5Px1A2a1NjFNKq-hxI3bVb30cKvacL29tYcjT-315RKv57onnCjVJWaGfzPJ44ZibB2VaVF9HO8Rjci8PJ35lF0NOw7YYb7rTQB0JOsLhHrwe9cjgKBsxwIAOVx1fFt6fq1RRJzUpcHKAx0f7kwGmHzXNwpNIexneBYg1Btz5UfW3yiztxggBPYvDfFAdLc5hfFURFhLtqaQzCklomRYBkbEkBGSt_5iz7HLYiiWuM51TzXgUMa631KwCuyBA675fwEMnwQ01YM51Et7NQgRcja737kSliM', + 'shop' => 1 + ], + [ + 'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDEyMDAsImV4cCI6MTUzNDk0NDgwMCwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjo1fQ.NAh6p40uSgyhcRkXxLSJhsGSn-mRAIYGsbtuMqSrplHk7LxrOQvvLBKfW0cZpl4y683ECGYqykJYmGb3nnWPKOQv5Pl2GExLItvzWuVju6KvkTi-_RD1xVXv6tCxkI6umcrJaI0bUWQhjLogrjNgIAR81DRbfG81lctIRzhP-CtADD9J7uEFrfWIGIbBgVOMdiw4nsAvqB9ECTDbnv5TRsxmOnyhm42mWxfpbrzd7WaRugIMWbysL2_5HXwqLV7jaaPlBHFpGs3cpgpRF0fsZ1VZeM7LAiiwvNuBwVsZbmD_ZKKdXzwL183RbsuSP4C_7gjH4UXEflRWRR6nzom_AyCFhqGrdu1obSNLiIeIt94p_VavOmwicwQKmuhOHGnYHVguTLYW2FcQIS2nMosswnXoZzTJcL3YwQQsMT6Xg-Da43bWZMIWvcK7d-nSvOGHW0OrHzWQRZOcH3RKANzPUEG5L0-KL55yacMbAPq6ykGc-I-_q1IDBxfxE2rSaxGKa72O60uwjX2Mafdhp9YWuESNm2Gvi09hYo3_dBUAHjaSdXOWqJhb8h1ND8c2IJNxltTIukz1fDdLkFvTttZi4g5rjsJoDN94Y3f-19qokY8lzhx8z3Zc4hgbNQ9p8CYrmjB6FDa59Psuxbd-ODwzNm_uLsn1z4U5uo68QHa5g50', + 'shop' => 5 + ], + [ // case with wpdesk and shop role + 'token' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDE0MzYsImV4cCI6MTUzNDk0NTAzNiwicm9sZXMiOlsiUk9MRV9XUERFU0siLCJST0xFX1NIT1AiXSwidXNlcm5hbWUiOjEsImlwIjoiMTcyLjE5LjAuMSIsInNob3AiOjJ9.pD9qQQJB_Mn6hSBcK5unEyrQ2n-3BgnzCRtk1V_u7BL0SKqgqILQN5NGJYpG2dO1Rb54pCNgAVzr6axu9f6ijmGetpHX_V1HSy7HOM4xdI8H7BaPHaK-2KUKTrVFM7lEp_sRc4KBkfSW4Ju2EH0e0Oty42EkIWeTb3J3npTHK_RjXI47xX5e_gZW8inRfJx24EhiSnlkMX77lIpXsqqCyRKqW64niLrQzKMNeSOJ_5HgBvzk9OyR9H5O8_s8EAliD24LvynoxgQOp9r0A78EIBRoqdZKJSS7kSgl_8eRFTGgxZBPGmyBKNWvxrd2XnRp19QPgymMi2kR0HJDWRemtSFHxYsjTrVVnpeiHjZ7WTKNawZPB-7ELPIHm_C_-GuJ4I1fnnM_uUna2rh6mr3KeRQF4LJGmDjesdHIpy8FDmepqsIcAAxpmAi6kJRtDr6QAxoh20zi0jCni1pPmLQyoDsau7U7l3Nev_STFbFiHBk-mLYbhNqV_MCcFY1-GeWynfehehWoVeGTShVoxXrDSWklPb58DaQKuwXJ8-kvmvWVhVqWGqF5l7qUj3D2CIxr4tXsQ6FUPkntvFw3S4GrLqAl-P7XoR5alRxSyC6KypYeY7pHRZBDDeoFWgCOJhEymWtKrcTt-Yzd2EL3vQYnwhYOp0hw1n-eXnE1WkaB4Wg', + 'shop' => 2 + ] + ]; + } + + /** + * Provides tokens when can't get info about shop + * + * @return array + */ + public function provideTokensWithInvalidShopInfo() + { + return [ + ['invalid' => 'siUk9MRV9XUERFU0siLCJST0xFX1NIT1AiXSwidXNlcm5hbWUiOjEsImlwIjoiMTcyLjE5LjAuMSIsInNob3AiOjJ9.qeroBbMHVgQWypDaF6FPmaZadu7D1u1JObCn9rXy44pGL1mWESdVCI6pU_MajeCJsQ-V3emWMOfqHl5AIiH7BZb2iW04ahGJjJTtRGaEfGH5ztyG-00TVzf51V0mtX3i3DuEEJd8F6LDHK8C51oqm6WkHVv__GhsqOlMz_WTDdst-IRUK-kgcUBhbYy-XzVxt4LADQhs4SsJqd-0wYPaobTFRQaHel69oGF_ymwJ4snla4uJxlfIHLtmYRArOIcDNKcPIOH8cMo-UJP9B15IxuOGO0M2hR74LNSmDUpIHmeQ24rEiwp73q2yhXYIHhIBdExDk7GAHFhOILIqa05D48V8fkPLr0TFVVCACvylJHwj3UsYPzI1WvhdrEHcGr1MC8bWB2KAC_RpB5Vvo7kPp0YXcUa7nbszL90v1njKpVrcSrpYG69a4Ym8ZXSdAS7jJmoSH-Xf3JXXi_6vtxtSsm-DG_zwd7VoWupBu1rG1h_ARW-Lambj_Ql2XZEAWAvdR6LqHkL5SyIZ7CnscbNY_VbYoyRHsQIwhzbaQdBSwBa4qu0VsEHtwdV2acbDZhojm-DMb4b7vr3znyw_WnHPSvjOGRXmW2KhD0bwwfrAJcquc0rL5N04FRVYXR7AnJjN-j1j0bfhvDN9a5W-yHidmp9_OZ0HVCqK5c0IIsiv-Ms'], + ['no shop id' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDE5MjMsImV4cCI6MTUzNDk0NTUyMywicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEifQ.S8Dkz9C7d0zHRn5_uRzjdBHH437JJZ4CdHTRc-Bwn1bnP9rj0SGn3AbCA53TlZjBl2EX0cigRlEMYsKEr0eQdQK8dellP0EjtPSaX5_5nEuU_H0X_6oJT8wYiS9N2jVm6W3UiKHMJBCR9JQfhjHYmMRx-NKhAi5xUgiGIWC7IWhNvLzHu52sRozkG2jTssnHGid087FdnQ1aG9c4dYY9piSw-5xBf8FruP9sDoWhTvFzESHIlwzgL5MNHQx-9HUH6caWoblpas959YqbD29h49YuCHLbifR1sMjSUWnkw69WLGDAFs8aKPCR5FsfKg3Rac0s957Cl3ES60eL7kWJ2mk1kRjxpFa6me1pWvh7Pt6lpDsJPLvTIbcZ_aDX5taOOiucL8YO-PbCB37Ffi2L4Am0-b4EGrqarBprjUWe2CJA9tCfjjFm0McOotDNVjLDmU7wAdHv8hXEOq4Kr2wd1z7suRqZii3VjMotoWFz9Cq4BTu1cs_Z_OLq5aC_uhnWq-SsKx3pdfL5tDXZ_CPqDCW0_3692FT8Z_puk1UUtMI_I_GkY_gp-npH72D8VcYdI-0j4LJoEtviGk6-0Lk47CiygGw79TaFjcEhwxPoYD_7MwMEw7vJiYYO8PVptlSuQy7j2-cPI0XPHlu19tVzA97ilqOlRAxt_SihXMfSoyo'], + ['no shop role' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIwNjEsImV4cCI6MTUzNDk0NTY2MSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjo1fQ.PS5SQixw9k9_-Waxho4GKJ1QudCSzee6gy9_cFjWnVcBpMGH-PkHf5iDO5rWwqQSsKpBgMi5m-FFCYoelHa5q1-XAn6FkRlj0aXNcQZCY6YGfqIaDeCP-0DEVoSqfaceFXLZbTyPz4aKrurONjWdZwuw9uull6Ho4Nq1B_E5QTpJBYehmebgWnS5OjHDjJVW0UrknwKHiRSohP-vCA5P2HQGZpZXrUCkLFcL2PqCCchORuHUQr3xjn2OleE4Jt1RsD5Lymo3m8s6WkRw9GepWV7-vJ0WDh3Mu_irkstZlCyujfh0PNnRSEyr9jshW2OXnDX_l0dLzsd5n6pPX1OVa58FpkZNb3s7hAsVxQ2Dd9GUg2o-qMZDCP9LbXn_CGUqiyXo4iIdal4balF0DpUGNEfggwqnL4leTsHqUirsN1bhLLhRmkAYooCfFuEMA9F5G66zF8Vwl5AuZSz8Nrj-T0krM54H5ykhzVttpWyRA9TcVywKAC9E2DFDOMfajtJA_LcMv5xg-m0uhhxlGk25HAY8yVWQdPfBp5KIq2jO3vLPzVmBDPFjtDgwiUXvXVakFS7AmtI8DRWh8dq92F2dGDV67uZ_WK5kqBQEtO1abfM5EF0zcQ9r_E4KqjLJfXyCyLMucw8CUdsE8MEcCfjpE06_mSuRMW1dpoSPJWGXE5Y'], + ['no whole shop info' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIwMjksImV4cCI6MTUzNDk0NTYyOSwicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEifQ.XTvcWiKWLda4TnHhWnqrOKCBseiHmjnDYgmPCKhX6NrEYMu4mN0puKgnoeOWJVZHBjFQ5X--khMFKNKU4e7Zgc6MWgnSc5niKyZpr73D-PVfUkOG81we2-DSteik2i3F2R2cTSIUbYMJKeqe7fQ0qTZ3-7s6kNgKAkIhGraQrbbxi-FTp4uNbUrDwcDEyBZ_T9zmcR9jXsKLVJszHdmc8K_eVYwj2jOR8bouGOqpCHwlGz0bEpcSPqXJ_DsotVeM91QIBls4JzImTFbYr06N1GdG4oFzMNWqVhO9iJ4-ElN_COPLp1ou0i1Ha0ZedYajI2CJrhailbF5ybZd5x08tiDoMa26aVpH09wOe_P4zYno0ksMnZ66WpPcnBgnPA_dlgcqUUkc93igicD8XCKEBokEyF16IzI3A8Y85FEuwkIj-JadM8ZTJqHErxNbhZyuEGqWvQfwhawade-3CxOUz3krbG8LxhNPBYrm2t8NQpRwwFoQPa0lvEdMiGo4JL0J7hW02shT2iGPOUwJ0_KfpXCU5ensd60Mou0GQhnUPhKvnA0CtvQn5JgX_TM5OKKCSvtcIEaW2pqq96--S8atnQp2GwZU0Ccs8R6HAfH61d8cwL8abwKvr6A5wQoskn28YPwWu8g9Tvt556i2oIvFWEjrFOnfQyhmQj1GCVMafFc'], + ]; + } + + /** + * @dataProvider provideTokensWithShopInfo + */ + public function testValidShopInfoFromToken($token, $shop) + { + $saasToken = new JWTSaasToken(new JWTToken($token)); + $this->assertTrue($saasToken->hasShopId(), 'Token should have shop id info'); + $this->assertEquals($shop, $saasToken->getShopId(), 'Invalid shop id provided in token'); + } + + /** + * @dataProvider provideTokensWithInvalidShopInfo + */ + public function testTokensWithInvalidShopInfo($token) + { + $saasToken = new JWTSaasToken(new JWTToken($token)); + $this->assertFalse($saasToken->hasShopId(), 'Token should NOT have shop id info'); + } +} \ No newline at end of file diff --git a/tests/unit/Authentication/TestJWToken.php b/tests/unit/Authentication/TestJWToken.php new file mode 100644 index 0000000000000000000000000000000000000000..492f1159ff070a0d8ef0f9031118fe82c70dfc60 --- /dev/null +++ b/tests/unit/Authentication/TestJWToken.php @@ -0,0 +1,33 @@ +<?php + +use WPDesk\ApiClient\Authentication\JWTToken; + +class TestJWToken extends \PHPUnit\Framework\TestCase +{ + + public function testExpiredToken() + { + $expiredToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ4NTc2OTEsImV4cCI6MTUzNDg2MTI5MSwicm9sZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9TSE9QIl0sInVzZXJuYW1lIjozLCJpcCI6IjE3Mi4xOS4wLjEiLCJzaG9wIjoxfQ.iB9u8f3uBdq1KhkkYPAVpPFSwvkK4CQypfFQIiM8N5acQSFxv-jyzD2guGs4HtLOdMvu4Dt5zkd4ZfQFJNT2b6k7i33FyU4AsEDAVtwHL-TcqsnomXn70CjB5Rhgd2LteFl6wPp3XonE4SZ6Oo3vtBZzSoNNkR6-7T3OseJMwoJ7qzlEFBixNXG6UTlXPJky_b-rbfhFORlInxVzvs4GJgkJM3F3Ugy4bLSSPtBYxWcwKnKkFE8L4Z87Pezp4v35aXqJFpvJ3zll0gJ1F32Z2vx1oCmu0jOHkzzmu3wA2u6gK0iNgp591M7MKH2_3HaLfFY06cLoLDN6TR6wmzSvZYmzSw8C68MUH7uGWyGRU9j4YtdL8Bom3v8D1J-IC4Jx6-QPE665nd1VgzHZb1TFkHseUx3kLF5Jhgq7095NJ79QTC-6XTW2bN-T-dbbFkvjCU-B-9Ti09uMUEn4Rtlt_lThbr_lA9Wyc4qXAecqCz6dAC2UTy-_KxLwvfNrQ4sSS4y3B8bPh8qJrI0EsIbi5nY20sgBT71abUG9DHmIzj4rA5YW_DOu7Cez0WhNoCHjhMEymATrD2cWYxFCPWQfTTBuoD1HZgVfwYI9B-aZPw669orNRGkBtP8Bm5ghmCZoRHpISiY7UoIzgOnVKhB-Qv43g1uRprk7nYnfVEglqXg'; + + $jwtToken = new JWTToken($expiredToken); + $this->assertTrue($jwtToken->isSignatureValid(), 'Signature should be valid'); + $this->assertTrue($jwtToken->isExpired(), 'Token should be expired long ago'); + } + + public function testImmortalToken() + { + $tokenWithExpiredDateInFuture = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIyODgsImV4cCI6MzI1MDM2ODAwMDAsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfU0hPUCJdLCJ1c2VybmFtZSI6MywiaXAiOiIxNzIuMTkuMC4xIiwic2hvcCI6NX0.gWsHsCGm8iUc13lGc9PgUtwv-qIYWAl98-jdzOdzuQ_PwreLuEFonP-hutp0_IXZtHeE6e1XLID-Cgn0YaahLrxybVGBTbcVgTJVOMB9Fv-gP9lHYNqvBdfQRcdXiO6PYCBbfpezunhcmiML_ebFfprdoMn8kG3K-XbEkyRB7MOQg0-dZ35tncVCZDfzLh5fucFzteQmCcddIosKpqr-rjYjRRCAB-aTE1vbAulZa1_VmP17l6m64__QBjHW9r07OTq7QviayXpOB_4mBdxI26XjgDXANCzlejcka5Uh7AS03cP3zDP8Mc0VaVZxqUhwotQ93s0VtyMNyUn_nkMebAuwXkDOmgsQzJHhlPD8hs9vzmNbN6ymKwsSZ-E3q1yPTQwXzu-rERdCaNifAIKlUyTETlvE3aoN7M0JjdCIVHBkf81ygppMITYJ0eytjyqBg0wjdRlMCpHEtGVosqLe8sNZo7NeZ9tPexhZxWXQNRA6VyE1NwDYd9yPsKBSPrulFcvkgoGOuL98rfE9iizMDYD60G0eTgafnR_99-IH4yoBMIjf4UNVzAcw8revbSE-3bgmxpdjVTJtVXw9LH13BHdB6GN7KJDwH4NRoz_MQJMSBELU18zY2yjmkNLXYI3YArTE3OQ-qBzD7rgPrcJBmoWTswbNVnTl2csFVHdQZOo'; + + $jwtToken = new JWTToken($tokenWithExpiredDateInFuture); + $this->assertTrue($jwtToken->isSignatureValid(), 'Signature should be valid'); + $this->assertFalse($jwtToken->isExpired(), 'Token should NOT be expired. Have 3000 years of live'); + } + + public function testStringValueAndBearer() { + $someToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MzQ5NDIyODgsImV4cCI6MzI1MDM2ODAwMDAsInJvbGVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfU0hPUCJdLCJ1c2VybmFtZSI6MywiaXAiOiIxNzIuMTkuMC4xIiwic2hvcCI6NX0.gWsHsCGm8iUc13lGc9PgUtwv-qIYWAl98-jdzOdzuQ_PwreLuEFonP-hutp0_IXZtHeE6e1XLID-Cgn0YaahLrxybVGBTbcVgTJVOMB9Fv-gP9lHYNqvBdfQRcdXiO6PYCBbfpezunhcmiML_ebFfprdoMn8kG3K-XbEkyRB7MOQg0-dZ35tncVCZDfzLh5fucFzteQmCcddIosKpqr-rjYjRRCAB-aTE1vbAulZa1_VmP17l6m64__QBjHW9r07OTq7QviayXpOB_4mBdxI26XjgDXANCzlejcka5Uh7AS03cP3zDP8Mc0VaVZxqUhwotQ93s0VtyMNyUn_nkMebAuwXkDOmgsQzJHhlPD8hs9vzmNbN6ymKwsSZ-E3q1yPTQwXzu-rERdCaNifAIKlUyTETlvE3aoN7M0JjdCIVHBkf81ygppMITYJ0eytjyqBg0wjdRlMCpHEtGVosqLe8sNZo7NeZ9tPexhZxWXQNRA6VyE1NwDYd9yPsKBSPrulFcvkgoGOuL98rfE9iizMDYD60G0eTgafnR_99-IH4yoBMIjf4UNVzAcw8revbSE-3bgmxpdjVTJtVXw9LH13BHdB6GN7KJDwH4NRoz_MQJMSBELU18zY2yjmkNLXYI3YArTE3OQ-qBzD7rgPrcJBmoWTswbNVnTl2csFVHdQZOo'; + + $jwtToken = new JWTToken($someToken); + $this->assertEquals($jwtToken->__toString(), $someToken); + $this->assertEquals($jwtToken->getAuthString(), 'Bearer ' . $someToken); + } +} \ No newline at end of file diff --git a/tests/unit/Client/TestClientFactory.php b/tests/unit/Client/TestClientFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..83dc5a9c6ed6a4417111e4b2b3f8d6b55c6277e5 --- /dev/null +++ b/tests/unit/Client/TestClientFactory.php @@ -0,0 +1,67 @@ +<?php + +use WPDesk\ApiClient\Client\ClientFactory; + +class TestClientFactory extends \PHPUnit\Framework\TestCase +{ + + /** + * Prepare client options. + * + * @param bool $isCachedClient + * + * @return \Mockery\MockInterface|\WPDesk\ApiClient\Client\ApiClientOptions + */ + private function prepareOptions($isCachedClient = false) + { + $options = Mockery::mock(\WPDesk\ApiClient\Client\ApiClientOptions::class); + $options->shouldReceive('getHttpClientClass') + ->withAnyArgs() + ->andReturn(\WPDesk\HttpClient\Curl\CurlClient::class); + + $options->shouldReceive('getSerializerClass') + ->withAnyArgs() + ->andReturn(\WPDesk\ApiClient\Serializer\JsonSerializer::class); + + $options->shouldReceive('getLogger') + ->withAnyArgs() + ->andReturn(new \Psr\Log\NullLogger()); + + $options->shouldReceive('getApiUrl') + ->withAnyArgs() + ->andReturn('https://app.flexibleshipping.com/api/v1'); + + $options->shouldReceive('getDefaultRequestHeaders') + ->withAnyArgs() + ->andReturn(array()); + + $options->shouldReceive('isCachedClient') + ->withAnyArgs() + ->andReturn($isCachedClient); + + return $options; + } + + /** + * Test createClient method. + */ + public function testCreateClient() + { + + $clientFactory = new ClientFactory(); + $client = $clientFactory->createClient($this->prepareOptions()); + $this->assertInstanceOf(\WPDesk\ApiClient\Client\ClientImplementation::class, $client); + } + + /** + * Test createClient method. + */ + public function testCreateClientCached() + { + + $clientFactory = new ClientFactory(); + $client = $clientFactory->createClient($this->prepareOptions(true)); + $this->assertInstanceOf(\WPDesk\ApiClient\Client\CachedClient::class, $client); + } + +} \ No newline at end of file diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php new file mode 100644 index 0000000000000000000000000000000000000000..76b8109582ae17560b77a6e0499b232e09047810 --- /dev/null +++ b/tests/unit/bootstrap.php @@ -0,0 +1,9 @@ +<?php +/** + * PHPUnit bootstrap file + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +WP_Mock::setUsePatchwork( true ); +WP_Mock::bootstrap();