From 6314d2ef968f1dfd8e3adab934841520d51f2a55 Mon Sep 17 00:00:00 2001 From: jordyvanderhaegen Date: Wed, 28 Feb 2024 17:06:39 +0100 Subject: [PATCH] Add: basic plytix setup --- .gitignore | 2 +- composer.json | 13 ++- config/plytix.php | 41 +++++++++ phpunit.xml | 48 +++++----- src/Facades/PlytixFacade.php | 13 --- src/Plytix.php | 57 +++++++++++- src/PlytixAuth.php | 16 ++++ src/PlytixTokenAuthenticator.php | 26 ++++++ src/Requests/CreateProductRequest.php | 29 ++++++ src/Requests/TokenRequest.php | 34 +++++++ src/Requests/UpdateProductRequest.php | 28 ++++++ tests/Feature/PlytixTest.php | 91 +++++++++++++++++++ .../Requests/CreateProductRequestTest.php | 31 +++++++ .../Requests/UpdateProductRequestTest.php | 33 +++++++ tests/Fixtures/Saloon/create-product.json | 49 ++++++++++ tests/Fixtures/Saloon/token.json | 7 ++ .../Saloon/update-product-not-found.json | 11 +++ tests/Fixtures/Saloon/update-product.json | 44 +++++++++ tests/PlytixTest.php | 12 --- tests/Support/MockResponseFixture.php | 17 ++++ tests/TestCase.php | 9 ++ 21 files changed, 553 insertions(+), 58 deletions(-) delete mode 100644 src/Facades/PlytixFacade.php create mode 100644 src/PlytixAuth.php create mode 100644 src/PlytixTokenAuthenticator.php create mode 100644 src/Requests/CreateProductRequest.php create mode 100644 src/Requests/TokenRequest.php create mode 100644 src/Requests/UpdateProductRequest.php create mode 100644 tests/Feature/PlytixTest.php create mode 100644 tests/Feature/Requests/CreateProductRequestTest.php create mode 100644 tests/Feature/Requests/UpdateProductRequestTest.php create mode 100644 tests/Fixtures/Saloon/create-product.json create mode 100644 tests/Fixtures/Saloon/token.json create mode 100644 tests/Fixtures/Saloon/update-product-not-found.json create mode 100644 tests/Fixtures/Saloon/update-product.json delete mode 100644 tests/PlytixTest.php create mode 100644 tests/Support/MockResponseFixture.php diff --git a/.gitignore b/.gitignore index 5e5f3b0..f3df55c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ vendor composer.lock -.phpunit.result.cache +.phpunit.cache .php-cs-fixer.cache \ No newline at end of file diff --git a/composer.json b/composer.json index 9816a60..eceb03f 100644 --- a/composer.json +++ b/composer.json @@ -17,12 +17,14 @@ ], "require": { "php": "^8.0", - "illuminate/support": "^8.0" + "illuminate/support": "^10.0", + "saloonphp/rate-limit-plugin": "^2.0", + "saloonphp/saloon": "^3.6" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.5", - "orchestra/testbench": "^6.0", - "phpunit/phpunit": "^9.0" + "orchestra/testbench": "^8.0", + "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { @@ -45,10 +47,7 @@ "laravel": { "providers": [ "Esign\\Plytix\\PlytixServiceProvider" - ], - "aliases": { - "Plytix": "Esign\\Plytix\\Facades\\PlytixFacade" - } + ] } }, "minimum-stability": "dev", diff --git a/config/plytix.php b/config/plytix.php index 25058db..621b9bf 100644 --- a/config/plytix.php +++ b/config/plytix.php @@ -1,5 +1,46 @@ env('PLYTIX_API_KEY'), + + /** + * The API password to be used for authenticating with the Plytix API. + */ + 'api_password' => env('PLYTIX_API_PASSWORD'), + + 'authenticator_cache' => [ + /** + * The key that will be used to cache the Plytix access token. + */ + 'key' => 'esign.plytix.authenticator', + + /** + * The cache store to be used for the Plytix access token. + * Use null to utilize the default cache store from the cache.php config file. + * To disable caching, you can use the 'array' store. + */ + 'store' => null, + ], + + 'rate_limiting' => [ + /** + * The rate limits to be used for the Plytix API. + */ + 'limits' => [ + Limit::allow(20)->everySeconds(10), + Limit::allow(2000)->everyHour(), + ], + /** + * The cache store to be used for the Plytix rate limits. + * Use null to utilize the default cache store from the cache.php config file. + * To disable caching, you can use the 'array' store. + */ + 'cache_store' => null, + ], ]; \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 6341576..e24cc15 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,28 +1,28 @@ - - - src/ - - - - - tests - - - - - + + + tests + + + + + + + + + + src/ + + diff --git a/src/Facades/PlytixFacade.php b/src/Facades/PlytixFacade.php deleted file mode 100644 index 40a2730..0000000 --- a/src/Facades/PlytixFacade.php +++ /dev/null @@ -1,13 +0,0 @@ -get(config('plytix.authenticator_cache.key')); + + if ($cachedAuthenticator instanceof PlytixTokenAuthenticator && ! $cachedAuthenticator->hasExpired()) { + return $cachedAuthenticator; + } + + $tokenResponse = (new PlytixAuth())->send(new TokenRequest( + config('plytix.api_key'), + config('plytix.api_password') + )); + + $authenticator = new PlytixTokenAuthenticator($tokenResponse->json('data.0.access_token')); + + $cacheStore->put( + key: config('plytix.authenticator_cache.key'), + value: $authenticator, + ttl: $authenticator->expiresAt + ); + + return $authenticator; + } + + protected function resolveRateLimitStore(): RateLimitStore + { + return new LaravelCacheStore( + Cache::store(config('plytix.rate_limiting.cache_store')) + ); + } + + protected function resolveLimits(): array + { + return config('plytix.rate_limiting.limits'); + } } diff --git a/src/PlytixAuth.php b/src/PlytixAuth.php new file mode 100644 index 0000000..cf1199f --- /dev/null +++ b/src/PlytixAuth.php @@ -0,0 +1,16 @@ +headers()->add('Authorization', 'Bearer ' . $this->token); + } + + public function hasExpired(): bool + { + return $this->expiresAt->getTimestamp() <= (new DateTimeImmutable)->getTimestamp(); + } +} diff --git a/src/Requests/CreateProductRequest.php b/src/Requests/CreateProductRequest.php new file mode 100644 index 0000000..3fb3aac --- /dev/null +++ b/src/Requests/CreateProductRequest.php @@ -0,0 +1,29 @@ +payload; + } +} diff --git a/src/Requests/TokenRequest.php b/src/Requests/TokenRequest.php new file mode 100644 index 0000000..33917bd --- /dev/null +++ b/src/Requests/TokenRequest.php @@ -0,0 +1,34 @@ + $this->apiKey, + 'api_password' => $this->apiPassword, + ]; + } +} diff --git a/src/Requests/UpdateProductRequest.php b/src/Requests/UpdateProductRequest.php new file mode 100644 index 0000000..0c4677b --- /dev/null +++ b/src/Requests/UpdateProductRequest.php @@ -0,0 +1,28 @@ +productId; + } + + public function defaultBody(): array + { + return $this->payload; + } +} diff --git a/tests/Feature/PlytixTest.php b/tests/Feature/PlytixTest.php new file mode 100644 index 0000000..8c6a5a4 --- /dev/null +++ b/tests/Feature/PlytixTest.php @@ -0,0 +1,91 @@ +storeAccessTokenInCache(new DateTimeImmutable('+1 hour')); + $plytix = new Plytix(); + $mockClient = MockClient::global([ + MockResponseFixture::make(fixtureName: 'create-product.json', status: 201), + ]); + + $plytix->send(new CreateProductRequest(['sku' => '12345'])); + + $mockClient->assertNotSent(TokenRequest::class); + $mockClient->assertSent(CreateProductRequest::class); + } + + /** @test */ + public function it_can_request_a_new_token_when_it_has_expired() + { + $this->storeAccessTokenInCache(new DateTimeImmutable('-1 minute')); + $plytix = new Plytix(); + $mockClient = MockClient::global([ + MockResponseFixture::make(fixtureName: 'token.json', status: 200), + MockResponseFixture::make(fixtureName: 'create-product.json', status: 201), + ]); + + $plytix->send(new CreateProductRequest(['sku' => '12345'])); + + $mockClient->assertSentCount(1, TokenRequest::class); + $mockClient->assertSentCount(1, CreateProductRequest::class); + } + + /** @test */ + public function it_can_use_a_cached_token_when_performing_multiple_requests() + { + $this->storeAccessTokenInCache(new DateTimeImmutable('+1 hour')); + $plytix = new Plytix(); + $mockClient = MockClient::global([ + MockResponseFixture::make(fixtureName: 'create-product.json', status: 201), + MockResponseFixture::make(fixtureName: 'create-product.json', status: 201), + ]); + + $plytix->send(new CreateProductRequest(['sku' => '12345'])); + $plytix->send(new CreateProductRequest(['sku' => '12345'])); + + $mockClient->assertNotSent(TokenRequest::class); + $mockClient->assertSentCount(2, CreateProductRequest::class); + } + + /** @test */ + public function it_can_throw_an_exception_when_an_http_error_is_encoutered() + { + $plytix = new Plytix(); + MockClient::global([ + MockResponseFixture::make(fixtureName: 'token.json', status: 200), + MockResponseFixture::make(fixtureName: 'update-product-not-found.json', status: 404), + ]); + + $this->expectException(RequestException::class); + + $plytix->send(new UpdateProductRequest( + productId: '5c4ed8002f0985001e233279', + payload: [] + )); + } + + protected function storeAccessTokenInCache(DateTimeImmutable $expiresAt): void + { + Cache::store(config('plytix.authenticator_cache.store'))->put( + config('plytix.authenticator_cache.key'), + new PlytixTokenAuthenticator('fake-token', $expiresAt), + ); + } +} diff --git a/tests/Feature/Requests/CreateProductRequestTest.php b/tests/Feature/Requests/CreateProductRequestTest.php new file mode 100644 index 0000000..f538916 --- /dev/null +++ b/tests/Feature/Requests/CreateProductRequestTest.php @@ -0,0 +1,31 @@ +send(new CreateProductRequest([ + 'sku' => '12345', + 'label' => 'Black Kettle', + ])); + + $mockClient->assertSent(CreateProductRequest::class); + $this->assertEquals('12345', $response->json('data.0.sku')); + $this->assertEquals('Black Kettle', $response->json('data.0.label')); + } +} diff --git a/tests/Feature/Requests/UpdateProductRequestTest.php b/tests/Feature/Requests/UpdateProductRequestTest.php new file mode 100644 index 0000000..e2ba079 --- /dev/null +++ b/tests/Feature/Requests/UpdateProductRequestTest.php @@ -0,0 +1,33 @@ +send(new UpdateProductRequest( + productId: '5c4ed8002f0985001e233279', + payload: [ + 'sku' => '12345', + 'label' => 'Black Kettle', + ])); + + $mockClient->assertSent(UpdateProductRequest::class); + $this->assertEquals('12345', $response->json('data.0.sku')); + $this->assertEquals('Black Kettle', $response->json('data.0.label')); + } +} diff --git a/tests/Fixtures/Saloon/create-product.json b/tests/Fixtures/Saloon/create-product.json new file mode 100644 index 0000000..e1aacd7 --- /dev/null +++ b/tests/Fixtures/Saloon/create-product.json @@ -0,0 +1,49 @@ +{ + "data": [ + { + "_parent_id": null, + "assets": [], + "attributes": { + "color_create_a_multiselect_type_attribute": [ + "Blue" + ], + "description_create_a_rich_text_type_attribute": "Try making the description a Rich Text attribute so that you can format the text however you'd like." + }, + "categories": [ + { + "id": "5c4ed7f62f0985001c233276", + "name": "Ground Coffee", + "path": [ + "Sample Categories", + "Coffee & Tea", + "Ground Coffee" + ] + }, + { + "id": "5c4ed7f62f0985001c233278", + "name": "Drinkware", + "path": [ + "Sample Categories", + "Drinkware" + ] + }, + { + "id": "5c4ed7f62f0985001c233279", + "name": "Kettles & Teapots", + "path": [ + "Sample Categories", + "Kettles & Teapots" + ] + } + ], + "created": "2019-01-28T12:20:57.341000+00:00", + "id": "5c4ef3a9bedb5e000189befc", + "label": "Black Kettle", + "modified": "2019-01-28T12:20:57.437000+00:00", + "num_variations": 0, + "sku": "12345", + "status": "Completed", + "thumbnail": null + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/Saloon/token.json b/tests/Fixtures/Saloon/token.json new file mode 100644 index 0000000..e745a1c --- /dev/null +++ b/tests/Fixtures/Saloon/token.json @@ -0,0 +1,7 @@ +{ + "data": [ + { + "access_token": "fake-acccess-token" + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/Saloon/update-product-not-found.json b/tests/Fixtures/Saloon/update-product-not-found.json new file mode 100644 index 0000000..b785db4 --- /dev/null +++ b/tests/Fixtures/Saloon/update-product-not-found.json @@ -0,0 +1,11 @@ +{ + "error": { + "errors": [ + { + "field": "id", + "msg": "product does not exist" + } + ], + "msg": " No results found for 5c4ed8002f0985001e233279" + } +} \ No newline at end of file diff --git a/tests/Fixtures/Saloon/update-product.json b/tests/Fixtures/Saloon/update-product.json new file mode 100644 index 0000000..7f9cc20 --- /dev/null +++ b/tests/Fixtures/Saloon/update-product.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "_parent_id": null, + "assets": [ + { + "filename": "Kettle handle - White.png", + "id": "5c4ed8002f0985001e233276", + "thumbnail": "https://files.plytix.com/api/v1.1/thumb/public_files/pim/assets/d1/6d/8e/5b/5b8e6dd17f7f46000c7e9629/images/00/d8/4e/5c/5c4ed8002f0985001e233276/Kettle handle - White.png", + "url": "https://files.plytix.com/api/v1.1/file/public_files/pim/assets/d1/6d/8e/5b/5b8e6dd17f7f46000c7e9629/images/00/d8/4e/5c/5c4ed8002f0985001e233276/Kettle handle - White.png" + } + ], + "attributes": { + "color_create_a_multiselect_type_attribute": [ + "Blue" + ], + "description_create_a_rich_text_type_attribute": "Try making the description a Rich Text attribute so that you can format the text however you'd like." + }, + "categories": [ + { + "id": "5c4ed7f62f0985001c233279", + "name": "Kettles & Teapots", + "path": [ + "Sample Categories", + "Kettles & Teapots" + ] + } + ], + "created": "2019-01-28T10:22:56.817000+00:00", + "id": "5c4ed8002f0985001e233279", + "label": "Black Kettle", + "modified": "2019-01-28T10:24:44.792000+00:00", + "num_variations": 0, + "sku": "12345", + "status": "Completed", + "thumbnail": { + "filename": "assets/d1/6d/8e/5b/5b8e6dd17f7f46000c7e9629/images/00/d8/4e/5c/5c4ed8002f0985001e233276/Kettle handle - White.png", + "id": "5c4ed8002f0985001e233276", + "thumbnail": "https://files.plytix.com/api/v1.1/thumb/public_files/pim/assets/d1/6d/8e/5b/5b8e6dd17f7f46000c7e9629/images/00/d8/4e/5c/5c4ed8002f0985001e233276/Kettle handle - White.png", + "url": "https://files.plytix.com/api/v1.1/thumb/public_files/pim/assets/d1/6d/8e/5b/5b8e6dd17f7f46000c7e9629/images/00/d8/4e/5c/5c4ed8002f0985001e233276/Kettle handle - White.png" + } + } + ] +} \ No newline at end of file diff --git a/tests/PlytixTest.php b/tests/PlytixTest.php deleted file mode 100644 index d0112fa..0000000 --- a/tests/PlytixTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue(true); - } -} \ No newline at end of file diff --git a/tests/Support/MockResponseFixture.php b/tests/Support/MockResponseFixture.php new file mode 100644 index 0000000..df8a55c --- /dev/null +++ b/tests/Support/MockResponseFixture.php @@ -0,0 +1,17 @@ +