From c57e9b809ff4a1276554b387a24db9b9bc3f2b84 Mon Sep 17 00:00:00 2001 From: LevFlavien Date: Tue, 15 Feb 2022 10:21:14 +0100 Subject: [PATCH] refact: Merge EE client into this project --- phpunit.xml.dist | 1 + spec/AkeneoPimClientSpec.php | 145 +++++++++- spec/Api/AssetApiSpec.php | 177 ++++++++++++ spec/Api/AssetCategoryApiSpec.php | 184 +++++++++++++ spec/Api/AssetReferenceFileApiSpec.php | 213 +++++++++++++++ spec/Api/AssetTagApiSpec.php | 111 ++++++++ spec/Api/AssetVariationFileApiSpec.php | 138 ++++++++++ spec/Api/LocaleApiSpec.php | 2 +- spec/Api/MeasurementFamilyApiSpec.php | 7 - spec/Api/ProductApiSpec.php | 2 +- spec/Api/ProductDraftApiSpec.php | 59 ++++ spec/Api/ProductMediaFileApiSpec.php | 4 +- spec/Api/ProductModelDraftApiSpec.php | 56 ++++ spec/Api/PublishedProductApiSpec.php | 118 ++++++++ spec/Api/ReferenceEntityApiSpec.php | 84 ++++++ spec/Api/ReferenceEntityAttributeApiSpec.php | 126 +++++++++ .../ReferenceEntityAttributeOptionApiSpec.php | 91 +++++++ spec/Api/ReferenceEntityMediaFileApiSpec.php | 63 +++++ spec/Api/ReferenceEntityRecordApiSpec.php | 137 ++++++++++ spec/Client/AuthenticatedHttpClientSpec.php | 2 +- spec/Client/HttpClientSpec.php | 2 +- spec/Client/HttpExceptionHandlerSpec.php | 2 +- spec/Client/ResourceClientSpec.php | 4 +- spec/Exception/HttpExceptionSpec.php | 1 - src/AkeneoPimClient.php | 254 +++++++++++++++++- src/AkeneoPimClientBuilder.php | 40 ++- src/AkeneoPimClientInterface.php | 54 ++++ src/Api/AssetApi.php | 109 ++++++++ src/Api/AssetApiInterface.php | 27 ++ src/Api/AssetCategoryApi.php | 108 ++++++++ src/Api/AssetCategoryApiInterface.php | 27 ++ src/Api/AssetManager/AssetApi.php | 85 ++++++ src/Api/AssetManager/AssetApiInterface.php | 70 +++++ src/Api/AssetManager/AssetAttributeApi.php | 45 ++++ .../AssetAttributeApiInterface.php | 34 +++ .../AssetManager/AssetAttributeOptionApi.php | 55 ++++ .../AssetAttributeOptionApiInterface.php | 34 +++ src/Api/AssetManager/AssetFamilyApi.php | 69 +++++ .../AssetManager/AssetFamilyApiInterface.php | 35 +++ src/Api/AssetManager/AssetMediaFileApi.php | 76 ++++++ .../AssetMediaFileApiInterface.php | 34 +++ src/Api/AssetReferenceFileApi.php | 130 +++++++++ src/Api/AssetReferenceFileApiInterface.php | 100 +++++++ src/Api/AssetTagApi.php | 85 ++++++ src/Api/AssetTagApiInterface.php | 20 ++ src/Api/AssetVariationFileApi.php | 122 +++++++++ src/Api/AssetVariationFileApiInterface.php | 88 ++++++ .../DownloadableResourceInterface.php | 1 - src/Api/ProductDraftApi.php | 57 ++++ src/Api/ProductDraftApiInterface.php | 26 ++ src/Api/ProductMediaFileApi.php | 1 - src/Api/ProductModelDraftApi.php | 57 ++++ src/Api/ProductModelDraftApiInterface.php | 26 ++ src/Api/PublishedProductApi.php | 79 ++++++ src/Api/PublishedProductApiInterface.php | 21 ++ src/Api/ReferenceEntityApi.php | 74 +++++ src/Api/ReferenceEntityApiInterface.php | 53 ++++ src/Api/ReferenceEntityAttributeApi.php | 50 ++++ .../ReferenceEntityAttributeApiInterface.php | 53 ++++ src/Api/ReferenceEntityAttributeOptionApi.php | 60 +++++ ...renceEntityAttributeOptionApiInterface.php | 55 ++++ src/Api/ReferenceEntityMediaFileApi.php | 82 ++++++ .../ReferenceEntityMediaFileApiInterface.php | 40 +++ src/Api/ReferenceEntityRecordApi.php | 82 ++++++ src/Api/ReferenceEntityRecordApiInterface.php | 68 +++++ ...UploadAssetReferenceFileErrorException.php | 35 +++ tests/Api/Asset/GetAssetIntegration.php | 70 +++++ tests/Api/Asset/ListAllAssetsIntegration.php | 129 +++++++++ tests/Api/Asset/UpsertAssetIntegration.php | 51 ++++ .../Api/Asset/UpsertListAssetIntegration.php | 96 +++++++ .../GetAssetFamilyAttributeIntegration.php | 68 +++++ ...istAllAssetFamilyAttributesIntegration.php | 80 ++++++ .../UpsertAssetFamilyAttributeIntegration.php | 45 ++++ ...tAssetFamilyAttributeOptionIntegration.php | 61 +++++ ...AssetFamilyAttributeOptionsIntegration.php | 64 +++++ ...ilyAttributeAttributeOptionIntegration.php | 44 +++ .../AssetFamily/GetAssetFamilyIntegration.php | 60 +++++ .../ListAllAssetFamiliesIntegration.php | 111 ++++++++ .../UpsertAssetFamilyIntegration.php | 36 +++ .../CreateAssetMediaFileIntegration.php | 54 ++++ .../DownloadAssetReferenceFileIntegration.php | 87 ++++++ .../UploadAssetReferenceFileIntegration.php | 176 ++++++++++++ ...wnloadAssetVariationFileApiIntegration.php | 88 ++++++ ...UploadAssetVariationFileApiIntegration.php | 164 +++++++++++ tests/Api/DownloadProductMediaFileTest.php | 1 - .../GetReferenceEntityIntegration.php | 65 +++++ .../ListAllReferenceEntitiesIntegration.php | 125 +++++++++ .../UpsertReferenceEntityIntegration.php | 36 +++ ...ateReferenceEntityMediaFileIntegration.php | 60 +++++ .../GetReferenceEntityRecordIntegration.php | 66 +++++ ...stAllReferenceEntityRecordsIntegration.php | 131 +++++++++ ...rtListReferenceEntityRecordIntegration.php | 80 ++++++ ...UpsertReferenceEntityRecordIntegration.php | 42 +++ tests/Api/UpsertListProductTest.php | 1 - tests/fixtures/akeneo-logo.pdf | Bin 0 -> 21595 bytes tests/fixtures/unicorn.png | Bin 0 -> 11255 bytes tests/fixtures/ziggy-certification.jpg | Bin 0 -> 10513 bytes tests/fixtures/ziggy.png | Bin 0 -> 25685 bytes 98 files changed, 6412 insertions(+), 29 deletions(-) create mode 100644 spec/Api/AssetApiSpec.php create mode 100644 spec/Api/AssetCategoryApiSpec.php create mode 100644 spec/Api/AssetReferenceFileApiSpec.php create mode 100644 spec/Api/AssetTagApiSpec.php create mode 100644 spec/Api/AssetVariationFileApiSpec.php create mode 100644 spec/Api/ProductDraftApiSpec.php create mode 100644 spec/Api/ProductModelDraftApiSpec.php create mode 100644 spec/Api/PublishedProductApiSpec.php create mode 100644 spec/Api/ReferenceEntityApiSpec.php create mode 100644 spec/Api/ReferenceEntityAttributeApiSpec.php create mode 100644 spec/Api/ReferenceEntityAttributeOptionApiSpec.php create mode 100644 spec/Api/ReferenceEntityMediaFileApiSpec.php create mode 100644 spec/Api/ReferenceEntityRecordApiSpec.php create mode 100644 src/Api/AssetApi.php create mode 100644 src/Api/AssetApiInterface.php create mode 100644 src/Api/AssetCategoryApi.php create mode 100644 src/Api/AssetCategoryApiInterface.php create mode 100644 src/Api/AssetManager/AssetApi.php create mode 100644 src/Api/AssetManager/AssetApiInterface.php create mode 100644 src/Api/AssetManager/AssetAttributeApi.php create mode 100644 src/Api/AssetManager/AssetAttributeApiInterface.php create mode 100644 src/Api/AssetManager/AssetAttributeOptionApi.php create mode 100644 src/Api/AssetManager/AssetAttributeOptionApiInterface.php create mode 100644 src/Api/AssetManager/AssetFamilyApi.php create mode 100644 src/Api/AssetManager/AssetFamilyApiInterface.php create mode 100644 src/Api/AssetManager/AssetMediaFileApi.php create mode 100644 src/Api/AssetManager/AssetMediaFileApiInterface.php create mode 100644 src/Api/AssetReferenceFileApi.php create mode 100644 src/Api/AssetReferenceFileApiInterface.php create mode 100644 src/Api/AssetTagApi.php create mode 100644 src/Api/AssetTagApiInterface.php create mode 100644 src/Api/AssetVariationFileApi.php create mode 100644 src/Api/AssetVariationFileApiInterface.php create mode 100644 src/Api/ProductDraftApi.php create mode 100644 src/Api/ProductDraftApiInterface.php create mode 100644 src/Api/ProductModelDraftApi.php create mode 100644 src/Api/ProductModelDraftApiInterface.php create mode 100644 src/Api/PublishedProductApi.php create mode 100644 src/Api/PublishedProductApiInterface.php create mode 100644 src/Api/ReferenceEntityApi.php create mode 100644 src/Api/ReferenceEntityApiInterface.php create mode 100644 src/Api/ReferenceEntityAttributeApi.php create mode 100644 src/Api/ReferenceEntityAttributeApiInterface.php create mode 100644 src/Api/ReferenceEntityAttributeOptionApi.php create mode 100644 src/Api/ReferenceEntityAttributeOptionApiInterface.php create mode 100644 src/Api/ReferenceEntityMediaFileApi.php create mode 100644 src/Api/ReferenceEntityMediaFileApiInterface.php create mode 100644 src/Api/ReferenceEntityRecordApi.php create mode 100644 src/Api/ReferenceEntityRecordApiInterface.php create mode 100644 src/Exception/UploadAssetReferenceFileErrorException.php create mode 100644 tests/Api/Asset/GetAssetIntegration.php create mode 100644 tests/Api/Asset/ListAllAssetsIntegration.php create mode 100644 tests/Api/Asset/UpsertAssetIntegration.php create mode 100644 tests/Api/Asset/UpsertListAssetIntegration.php create mode 100644 tests/Api/AssetAttribute/GetAssetFamilyAttributeIntegration.php create mode 100644 tests/Api/AssetAttribute/ListAllAssetFamilyAttributesIntegration.php create mode 100644 tests/Api/AssetAttribute/UpsertAssetFamilyAttributeIntegration.php create mode 100644 tests/Api/AssetAttributeOption/GetAssetFamilyAttributeOptionIntegration.php create mode 100644 tests/Api/AssetAttributeOption/ListAllAssetFamilyAttributeOptionsIntegration.php create mode 100644 tests/Api/AssetAttributeOption/UpsertAssetFamilyAttributeAttributeOptionIntegration.php create mode 100644 tests/Api/AssetFamily/GetAssetFamilyIntegration.php create mode 100644 tests/Api/AssetFamily/ListAllAssetFamiliesIntegration.php create mode 100644 tests/Api/AssetFamily/UpsertAssetFamilyIntegration.php create mode 100644 tests/Api/AssetMediaFile/CreateAssetMediaFileIntegration.php create mode 100644 tests/Api/AssetReferenceFile/DownloadAssetReferenceFileIntegration.php create mode 100644 tests/Api/AssetReferenceFile/UploadAssetReferenceFileIntegration.php create mode 100644 tests/Api/AssetVariationFile/DownloadAssetVariationFileApiIntegration.php create mode 100644 tests/Api/AssetVariationFile/UploadAssetVariationFileApiIntegration.php create mode 100644 tests/Api/ReferenceEntity/GetReferenceEntityIntegration.php create mode 100644 tests/Api/ReferenceEntity/ListAllReferenceEntitiesIntegration.php create mode 100644 tests/Api/ReferenceEntity/UpsertReferenceEntityIntegration.php create mode 100644 tests/Api/ReferenceEntityMediaFile/CreateReferenceEntityMediaFileIntegration.php create mode 100644 tests/Api/ReferenceEntityRecord/GetReferenceEntityRecordIntegration.php create mode 100644 tests/Api/ReferenceEntityRecord/ListAllReferenceEntityRecordsIntegration.php create mode 100644 tests/Api/ReferenceEntityRecord/UpsertListReferenceEntityRecordIntegration.php create mode 100644 tests/Api/ReferenceEntityRecord/UpsertReferenceEntityRecordIntegration.php create mode 100644 tests/fixtures/akeneo-logo.pdf create mode 100644 tests/fixtures/unicorn.png create mode 100644 tests/fixtures/ziggy-certification.jpg create mode 100644 tests/fixtures/ziggy.png diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9b6d45a8..027b1ff6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,6 +12,7 @@ tests/ + tests/ diff --git a/spec/AkeneoPimClientSpec.php b/spec/AkeneoPimClientSpec.php index dd7e844e..53b0c04c 100644 --- a/spec/AkeneoPimClientSpec.php +++ b/spec/AkeneoPimClientSpec.php @@ -4,9 +4,19 @@ use Akeneo\Pim\ApiClient\AkeneoPimClient; use Akeneo\Pim\ApiClient\AkeneoPimClientInterface; +use Akeneo\Pim\ApiClient\Api\AssetApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetCategoryApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetApiInterface as AssetManagerApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetFamilyApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetReferenceFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetTagApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetVariationFileApiInterface; use Akeneo\Pim\ApiClient\Api\AssociationTypeApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeApiInterface; -use Akeneo\Pim\ApiClient\Api\AttributeGroupApi; +use Akeneo\Pim\ApiClient\Api\AttributeGroupApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeOptionApiInterface; use Akeneo\Pim\ApiClient\Api\CategoryApiInterface; use Akeneo\Pim\ApiClient\Api\ChannelApiInterface; @@ -18,7 +28,15 @@ use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApiInterface; use Akeneo\Pim\ApiClient\Api\MediaFileApiInterface; use Akeneo\Pim\ApiClient\Api\ProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductDraftApiInterface; use Akeneo\Pim\ApiClient\Api\ProductModelApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductModelDraftApiInterface; +use Akeneo\Pim\ApiClient\Api\PublishedProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityRecordApiInterface; use Akeneo\Pim\ApiClient\Security\Authentication; use PhpSpec\ObjectBehavior; @@ -30,7 +48,7 @@ function let( CategoryApiInterface $categoryApi, AttributeApiInterface $attributeApi, AttributeOptionApiInterface $attributeOptionApi, - AttributeGroupApi $attributeGroupApi, + AttributeGroupApiInterface $attributeGroupApi, FamilyApiInterface $familyApi, MediaFileApiInterface $productMediaFileApi, LocaleApiInterface $localeApi, @@ -40,7 +58,25 @@ function let( MeasurementFamilyApiInterface $measurementFamilyApi, AssociationTypeApiInterface $associationTypeApi, FamilyVariantApiInterface $familyVariantApi, - ProductModelApiInterface $productModelApi + ProductModelApiInterface $productModelApi, + ProductModelDraftApiInterface $productModelDraftApi, + PublishedProductApiInterface $publishedProductApi, + ProductDraftApiInterface $productDraftApi, + AssetApiInterface $assetApi, + AssetCategoryApiInterface $assetCategoryApi, + AssetTagApiInterface $assetTagApi, + AssetReferenceFileApiInterface $assetReferenceFileApi, + AssetVariationFileApiInterface $assetVariationFileApi, + ReferenceEntityRecordApiInterface $referenceEntityRecordApi, + ReferenceEntityMediaFileApiInterface $referenceEntityMediaFileApi, + ReferenceEntityAttributeApiInterface $referenceEntityAttributeApi, + ReferenceEntityAttributeOptionApiInterface $referenceEntityAttributeOptionApi, + ReferenceEntityApiInterface $referenceEntityApi, + AssetManagerApiInterface $assetManagerApi, + AssetFamilyApiInterface $assetFamilyApi, + AssetAttributeApiInterface $assetAttributeApi, + AssetAttributeOptionApiInterface $assetAttributeOptionApi, + AssetMediaFileApiInterface $assetMediaFileApi ) { $this->beConstructedWith( $authentication, @@ -48,7 +84,8 @@ function let( $categoryApi, $attributeApi, $attributeOptionApi, - $attributeGroupApi, $familyApi, + $attributeGroupApi, + $familyApi, $productMediaFileApi, $localeApi, $channelApi, @@ -57,7 +94,25 @@ function let( $measurementFamilyApi, $associationTypeApi, $familyVariantApi, - $productModelApi + $productModelApi, + $productModelDraftApi, + $publishedProductApi, + $productDraftApi, + $assetApi, + $assetCategoryApi, + $assetTagApi, + $assetReferenceFileApi, + $assetVariationFileApi, + $referenceEntityRecordApi, + $referenceEntityMediaFileApi, + $referenceEntityAttributeApi, + $referenceEntityAttributeOptionApi, + $referenceEntityApi, + $assetManagerApi, + $assetFamilyApi, + $assetAttributeApi, + $assetAttributeOptionApi, + $assetMediaFileApi ); } @@ -150,4 +205,84 @@ function it_gets_product_model_api($productModelApi) { $this->getProductModelApi()->shouldReturn($productModelApi); } + + function it_gets_published_product_api($publishedProductApi) + { + $this->getPublishedProductApi()->shouldReturn($publishedProductApi); + } + + function it_gets_draft_product_api($productDraftApi) + { + $this->getProductDraftApi()->shouldReturn($productDraftApi); + } + + function it_gets_draft_product_model_api($productModelDraftApi) + { + $this->getProductModelDraftApi()->shouldReturn($productModelDraftApi); + } + + function it_gets_asset_api($assetApi) + { + $this->getAssetApi()->shouldReturn($assetApi); + } + + function it_gets_asset_category_api($assetCategoryApi) + { + $this->getAssetCategoryApi()->shouldReturn($assetCategoryApi); + } + + function it_gets_asset_tags_api($assetTagApi) + { + $this->getAssetTagApi()->shouldReturn($assetTagApi); + } + + function it_gets_asset_reference_file_api($assetReferenceFileApi) + { + $this->getAssetReferenceFileApi()->shouldReturn($assetReferenceFileApi); + } + + function it_gets_reference_entity_record_api($referenceEntityRecordApi) + { + $this->getReferenceEntityRecordApi()->shouldReturn($referenceEntityRecordApi); + } + + function it_gets_reference_entity_media_file_api($referenceEntityMediaFileApi) + { + $this->getReferenceEntityMediaFileApi()->shouldReturn($referenceEntityMediaFileApi); + } + + function it_gets_reference_entity_attribute_api($referenceEntityAttributeApi) + { + $this->getReferenceEntityAttributeApi()->shouldReturn($referenceEntityAttributeApi); + } + + function it_gets_reference_entity_api($referenceEntityApi) + { + $this->getReferenceEntityApi()->shouldReturn($referenceEntityApi); + } + + function it_gets_asset_manager_api($assetManagerApi) + { + $this->getAssetManagerApi()->shouldReturn($assetManagerApi); + } + + function it_gets_asset_family_api($assetFamilyApi) + { + $this->getAssetFamilyApi()->shouldReturn($assetFamilyApi); + } + + function it_gets_asset_attribute_api($assetAttributeApi) + { + $this->getAssetAttributeApi()->shouldReturn($assetAttributeApi); + } + + function it_gets_asset_attribute_option_api($assetAttributeOptionApi) + { + $this->getAssetAttributeOptionApi()->shouldReturn($assetAttributeOptionApi); + } + + function it_gets_asset_media_file_api($assetMediaFileApi) + { + $this->getAssetMediaFileApi()->shouldReturn($assetMediaFileApi); + } } diff --git a/spec/Api/AssetApiSpec.php b/spec/Api/AssetApiSpec.php new file mode 100644 index 00000000..b2ea9db8 --- /dev/null +++ b/spec/Api/AssetApiSpec.php @@ -0,0 +1,177 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_initializable() + { + $this->shouldHaveType(AssetApi::class); + } + + function it_gets_an_asset($resourceClient) + { + $asset = [ + 'code' => 'akeneo_logo', + 'localized' => false, + 'description' => 'Akeneo logo', + 'end_of_use' => null, + 'tags' => [], + 'categories' => ['asset_main_catalog'], + 'variation_files' => [], + 'reference_files' => [], + ]; + + $resourceClient->getResource(AssetApi::ASSET_URI, ['akeneo_logo'])->willReturn($asset); + + $this->get('akeneo_logo')->shouldReturn($asset); + } + + function it_returns_a_list_of_assets_with_default_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetApi::ASSETS_URI, [], 10, false, []) + ->willReturn([]); + $pageFactory->createPage([])->willReturn($page); + $this->listPerPage()->shouldReturn($page); + } + + function it_returns_a_list_of_assets_with_limit_and_count( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetApi::ASSETS_URI, [], 10, true, []) + ->willReturn([]); + $pageFactory->createPage([])->willReturn($page); + $this->listPerPage(10, true)->shouldReturn($page); + } + + function it_returns_a_cursor_on_the_list_of_assets( + $resourceClient, + $pageFactory, + $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources( + AssetApi::ASSETS_URI, + [], + 10, + false, + ['pagination_type' => 'search_after'] + ) + ->willReturn([]); + $pageFactory->createPage([])->willReturn($page); + $cursorFactory->createCursor(10, $page)->willReturn($cursor); + $this->all(10, [])->shouldReturn($cursor); + } + + function it_returns_a_list_of_assets_with_additional_query_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetApi::ASSETS_URI, [], 10, true, ['foo' => 'bar']) + ->willReturn([]); + $pageFactory->createPage([])->willReturn($page); + $this->listPerPage(10, true, ['foo' => 'bar'])->shouldReturn($page); + } + + function it_creates_an_asset($resourceClient) + { + $resourceClient->createResource(AssetApi::ASSETS_URI, [], [ + 'code' => 'unicorn', + 'localized' => false, + 'description' => 'The wonderful unicorn', + 'end_of_use' => null, + 'tags' => [], + 'categories' => ['asset_main_catalog'], + 'variation_files' => [], + 'reference_files' => [], + ])->willReturn(201); + + $this->create('unicorn', [ + 'localized' => false, + 'description' => 'The wonderful unicorn', + 'end_of_use' => null, + 'tags' => [], + 'categories' => ['asset_main_catalog'], + 'variation_files' => [], + 'reference_files' => [], + ])->shouldReturn(201); + } + + function it_throws_an_exception_if_code_is_provided_in_data_when_creating_an_asset() + { + $this + ->shouldThrow(new InvalidArgumentException('The parameter "code" should not be defined in the data parameter')) + ->during('create', ['unicorn', ['code' => 'unicorn', 'localized' => false]]); + } + + function it_upserts_an_asset($resourceClient) + { + $resourceClient + ->upsertResource(AssetApi::ASSET_URI, ['akeneo_logo'], [ + 'localized' => false, + 'description' => 'Akeneo logo updated', + 'categories' => ['asset_main_catalog'], + ]) + ->willReturn(204); + + $this->upsert('akeneo_logo', [ + 'localized' => false, + 'description' => 'Akeneo logo updated', + 'categories' => ['asset_main_catalog'], + ])->shouldReturn(204); + } + + function it_upserts_a_list_of_assets($resourceClient, UpsertResourceListResponse $response) + { + $resourceClient->upsertStreamResourceList(AssetApi::ASSETS_URI, [], [ + [ + 'code' => 'akeneo_logo', + 'description' => 'Akeneo logo updated', + ], + [ + 'code' => 'unicorn', + 'description' => 'Created asset', + ] + ])->willReturn($response); + + $this->upsertList([ + [ + 'code' => 'akeneo_logo', + 'description' => 'Akeneo logo updated', + ], + [ + 'code' => 'unicorn', + 'description' => 'Created asset', + ] + ])->shouldReturn($response); + } +} diff --git a/spec/Api/AssetCategoryApiSpec.php b/spec/Api/AssetCategoryApiSpec.php new file mode 100644 index 00000000..2f97dc17 --- /dev/null +++ b/spec/Api/AssetCategoryApiSpec.php @@ -0,0 +1,184 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_initializable() + { + $this->shouldHaveType(AssetCategoryApi::class); + $this->shouldImplement(AssetCategoryApiInterface::class); + $this->shouldImplement(GettableResourceInterface::class); + $this->shouldImplement(ListableResourceInterface::class); + } + + public function it_gets_an_asset_category($resourceClient) + { + $assetCategory = [ + 'code' => 'asset_main_catalog', + 'parent' => null, + 'labels' => [ + 'en_US' => 'dolor sed perferendis', + ], + ]; + + $resourceClient->getResource(AssetCategoryApi::ASSET_CATEGORY_URI, ['asset_main_catalog'])->willReturn($assetCategory); + + $this->get('asset_main_catalog')->shouldReturn($assetCategory); + } + + function it_returns_a_list_of_asset_categories_with_default_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetCategoryApi::ASSET_CATEGORIES_URI, [], 10, false, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage()->shouldReturn($page); + } + + function it_returns_a_list_of_asset_categories_with_limit_and_count( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetCategoryApi::ASSET_CATEGORIES_URI, [], 10, true, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true)->shouldReturn($page); + } + + function it_returns_a_cursor_on_the_list_of_asset_categories( + $resourceClient, + $pageFactory, + $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources( + AssetCategoryApi::ASSET_CATEGORIES_URI, + [], + 10, + false, + [] + ) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $cursorFactory->createCursor(10, $page)->willReturn($cursor); + + $this->all(10, [])->shouldReturn($cursor); + } + + function it_returns_a_list_of_asset_categories_with_additional_query_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetCategoryApi::ASSET_CATEGORIES_URI, [], 10, true, ['foo' => 'bar']) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true, ['foo' => 'bar'])->shouldReturn($page); + } + + function it_upserts_an_asset_category($resourceClient) + { + $resourceClient + ->upsertResource(AssetCategoryApi::ASSET_CATEGORY_URI, ['asset_main_catalog'], [ + 'labels' => [ + 'en_US' => 'Nullam ullamcorper', + ] + ]) + ->willReturn(204); + + $this->upsert('asset_main_catalog', [ + 'labels' => [ + 'en_US' => 'Nullam ullamcorper', + ] + ])->shouldReturn(204); + } + + function it_upserts_a_list_of_asset_categories($resourceClient, UpsertResourceListResponse $response) + { + $resourceClient + ->upsertStreamResourceList( + AssetCategoryApi::ASSET_CATEGORIES_URI, + [], + [ + ['code' => 'asset_1'], + ['code' => 'asset_2'], + ] + ) + ->willReturn($response); + + $this + ->upsertList([ + ['code' => 'asset_1'], + ['code' => 'asset_2'], + ])->shouldReturn($response); + } + + function it_creates_an_asset_category($resourceClient) + { + $resourceClient->createResource(AssetCategoryApi::ASSET_CATEGORIES_URI, [], [ + 'code' => 'asset_spring', + 'parent' => null, + 'labels' => [ + 'en_US' => 'Nullam ullamcorper', + ], + ])->willReturn(201); + + $this->create('asset_spring', [ + 'parent' => null, + 'labels' => [ + 'en_US' => 'Nullam ullamcorper', + ], + ])->shouldReturn(201); + } + + function it_throws_an_exception_if_code_is_provided_in_data_when_creating_an_asset_category() + { + $this + ->shouldThrow(new InvalidArgumentException('The parameter "code" should not be defined in the data parameter')) + ->during('create', ['asset_spring', [ + 'code' => 'asset_spring', + 'parent' => null, + 'labels' => [ + 'en_US' => 'Nullam ullamcorper', + ], + ]]); + } +} diff --git a/spec/Api/AssetReferenceFileApiSpec.php b/spec/Api/AssetReferenceFileApiSpec.php new file mode 100644 index 00000000..f5ac58a9 --- /dev/null +++ b/spec/Api/AssetReferenceFileApiSpec.php @@ -0,0 +1,213 @@ +beConstructedWith($resourceClient, $fileSystem); + } + + function it_is_initializable() + { + $this->shouldHaveType(AssetReferenceFileApi::class); + $this->shouldImplement(AssetReferenceFileApiInterface::class); + } + + function it_gets_a_localizable_asset_reference_file($resourceClient) + { + $assetReferenceFile = [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_Ziggy.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => 'http://akeneo-ped-master.local/api/rest/v1/assets/ziggy/reference-files/en_US/download' + ] + ], + ]; + + $resourceClient + ->getResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', 'en_US']) + ->willReturn($assetReferenceFile); + + $this->getFromLocalizableAsset('ziggy', 'en_US')->shouldReturn($assetReferenceFile); + } + + function it_gets_a_not_localizable_asset_reference_file($resourceClient) + { + $assetReferenceFile = [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_Ziggy.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => 'http://akeneo-ped-master.local/api/rest/v1/assets/ziggy/reference-files/no-locale/download' + ] + ], + ]; + + $resourceClient + ->getResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE]) + ->willReturn($assetReferenceFile); + + $this->getFromNotLocalizableAsset('ziggy')->shouldReturn($assetReferenceFile); + } + + function it_uploads_a_localizable_asset_reference_file( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath('images/ziggy.png')->willReturn('fileResource'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => 'fileResource', + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', 'en_US'], $requestParts) + ->willReturn($response); + + $this->uploadForLocalizableAsset('images/ziggy.png', 'ziggy', 'en_US')->shouldReturn(201); + } + + function it_uploads_a_not_localizable_asset_reference_file( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath('images/ziggy.png')->willReturn('fileResource'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => 'fileResource', + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE], $requestParts) + ->willReturn($response); + + $this->uploadForNotLocalizableAsset('images/ziggy.png', 'ziggy')->shouldReturn(201); + } + + function it_uploads_an_asset_reference_file_from_a_file_resource( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath(Argument::any())->shouldNotBeCalled(); + + $fileResource = fopen('php://stdin', 'r'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => $fileResource, + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', 'en_US'], $requestParts) + ->willReturn($response); + + $this->uploadForLocalizableAsset($fileResource, 'ziggy', 'en_US')->shouldReturn(201); + } + + function it_throws_an_exception_if_the_upload_response_contains_errors( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath('images/ziggy.png')->willReturn('fileResource'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => 'fileResource', + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + + $responseContent = +<<getContents()->willReturn($responseContent); + + $resourceClient + ->createMultipartResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, ['ziggy', 'en_US'], $requestParts) + ->willReturn($response); + + $this->shouldThrow(new UploadAssetReferenceFileErrorException('Some variation files were not generated properly.', [ + [ + 'message' => 'Impossible to "resize" the image "/tmp/pim/file_storage/4/2/5/1/ziggy-en_US-ecommerce.png" with a width bigger than the original.', + 'scope' => 'ecommerce', + 'locale' => 'en_US' + ], + [ + 'message' => 'Impossible to "resize" the image "/tmp/pim/file_storage/4/2/5/1/ziggy-en_US-mobile.png" with a height bigger than the original.', + 'scope' => 'mobile', + 'locale' => 'en_US' + ] + ])) + ->during('uploadForLocalizableAsset', ['images/ziggy.png', 'ziggy', 'en_US']); + } + + function it_downloads_a_localizable_asset_reference_file($resourceClient, ResponseInterface $response, StreamInterface $streamBody) + { + $resourceClient + ->getStreamedResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, ['ziggy', 'en_US']) + ->willReturn($response); + + $this->downloadFromLocalizableAsset('ziggy', 'en_US')->shouldReturn($response); + } + + function it_downloads_a_not_localizable_asset_reference_file($resourceClient, ResponseInterface $response, StreamInterface $streamBody) + { + $resourceClient + ->getStreamedResource(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, ['ziggy', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE]) + ->willReturn($response); + + $this->downloadFromNotLocalizableAsset('ziggy')->shouldReturn($response); + } +} diff --git a/spec/Api/AssetTagApiSpec.php b/spec/Api/AssetTagApiSpec.php new file mode 100644 index 00000000..10147c96 --- /dev/null +++ b/spec/Api/AssetTagApiSpec.php @@ -0,0 +1,111 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + public function it_is_initializable() + { + $this->shouldHaveType('Akeneo\Pim\ApiClient\Api\AssetTagApi'); + } + + public function it_gets_an_asset_tag($resourceClient) + { + $assetTag = ['code' => 'logo']; + + $resourceClient->getResource(AssetTagApi::ASSET_TAG_URI, ['logo'])->willReturn($assetTag); + + $this->get('logo')->shouldReturn($assetTag); + } + + public function it_upserts_an_asset_tag($resourceClient) + { + $resourceClient + ->upsertResource(AssetTagApi::ASSET_TAG_URI, ['logo'], []) + ->willReturn(201); + + $this->upsert('logo')->shouldReturn(201); + } + + function it_returns_a_list_of_asset_tags_with_default_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetTagApi::ASSET_TAGS_URI, [], 10, false, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage()->shouldReturn($page); + } + + function it_returns_a_list_of_asset_tags_with_limit_and_count( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetTagApi::ASSET_TAGS_URI, [], 10, true, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true)->shouldReturn($page); + } + + function it_returns_a_cursor_on_the_list_of_asset_tags( + $resourceClient, + $pageFactory, + $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources( + AssetTagApi::ASSET_TAGS_URI, + [], + 10, + false, + [] + ) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $cursorFactory->createCursor(10, $page)->willReturn($cursor); + + $this->all(10, [])->shouldReturn($cursor); + } + + function it_returns_a_list_of_asset_tags_with_additional_query_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(AssetTagApi::ASSET_TAGS_URI, [], 10, true, ['foo' => 'bar']) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true, ['foo' => 'bar'])->shouldReturn($page); + } +} diff --git a/spec/Api/AssetVariationFileApiSpec.php b/spec/Api/AssetVariationFileApiSpec.php new file mode 100644 index 00000000..7e2b427b --- /dev/null +++ b/spec/Api/AssetVariationFileApiSpec.php @@ -0,0 +1,138 @@ +beConstructedWith($resourceClient, $fileSystem); + } + + function it_is_initializable() + { + $this->shouldHaveType(AssetVariationFileApi::class); + $this->shouldImplement(AssetVariationFileApiInterface::class); + } + + function it_gets_a_localizable_asset_variation_file($resourceClient) + { + $assetVariationFile = [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_ziggy_en_US_mobile.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => 'http://akeneo-ped-master.local/api/rest/v1/assets/ziggy/variation-files/mobile/en_US/download' + ] + ], + ]; + + $resourceClient + ->getResource(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, ['ziggy', 'mobile', 'en_US']) + ->willReturn($assetVariationFile); + + $this->getFromLocalizableAsset('ziggy', 'mobile', 'en_US')->shouldReturn($assetVariationFile); + } + + function it_gets_a_not_localizable_asset_variation_file($resourceClient) + { + $assetVariationFile = [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_ziggy_mobile.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => 'http://akeneo-ped-master.local/api/rest/v1/assets/ziggy/variation-files/mobile/no-locale/download' + ] + ], + ]; + + $resourceClient + ->getResource(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, ['ziggy', 'mobile', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE]) + ->willReturn($assetVariationFile); + + $this->getFromNotLocalizableAsset('ziggy', 'mobile')->shouldReturn($assetVariationFile); + } + + function it_uploads_a_localizable_asset_variation_file( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath('images/ziggy.png')->willReturn('fileResource'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => 'fileResource', + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, ['ziggy', 'mobile', 'en_US'], $requestParts) + ->willReturn($response); + + $this->uploadForLocalizableAsset('images/ziggy.png', 'ziggy', 'mobile', 'en_US')->shouldReturn(201); + } + + function it_uploads_a_not_localizable_asset_variation_file( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath('images/ziggy.png')->willReturn('fileResource'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => 'fileResource', + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, ['ziggy', 'mobile', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE], $requestParts) + ->willReturn($response); + + $this->uploadForNotLocalizableAsset('images/ziggy.png', 'ziggy', 'mobile')->shouldReturn(201); + } + + function it_uploads_an_asset_variation_file_from_a_file_resource( + $resourceClient, + $fileSystem, + ResponseInterface $response, + StreamInterface $responseBody + ) { + $fileSystem->getResourceFromPath(Argument::any())->shouldNotBeCalled(); + + $fileResource = fopen('php://stdin', 'r'); + + $requestParts = [[ + 'name' => 'file', + 'contents' => $fileResource, + ]]; + + $response->getStatusCode()->willReturn(201); + $response->getBody()->willReturn($responseBody); + $responseBody->getContents()->willReturn(''); + + $resourceClient + ->createMultipartResource(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, ['ziggy', 'mobile', 'en_US'], $requestParts) + ->willReturn($response); + + $this->uploadForLocalizableAsset($fileResource, 'ziggy', 'mobile', 'en_US')->shouldReturn(201); + } +} diff --git a/spec/Api/LocaleApiSpec.php b/spec/Api/LocaleApiSpec.php index 3e82385e..798749a8 100644 --- a/spec/Api/LocaleApiSpec.php +++ b/spec/Api/LocaleApiSpec.php @@ -6,8 +6,8 @@ use Akeneo\Pim\ApiClient\Api\LocaleApiInterface; use Akeneo\Pim\ApiClient\Api\Operation\ListableResourceInterface; use Akeneo\Pim\ApiClient\Client\ResourceClientInterface; -use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\PageFactoryInterface; +use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorFactoryInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; use PhpSpec\ObjectBehavior; diff --git a/spec/Api/MeasurementFamilyApiSpec.php b/spec/Api/MeasurementFamilyApiSpec.php index cf03d5ab..3cf27dd7 100644 --- a/spec/Api/MeasurementFamilyApiSpec.php +++ b/spec/Api/MeasurementFamilyApiSpec.php @@ -4,14 +4,7 @@ use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApi; use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApiInterface; -use Akeneo\Pim\ApiClient\Api\Operation\ListableResourceInterface; -use Akeneo\Pim\ApiClient\Api\Operation\UpsertableResourceListInterface; use Akeneo\Pim\ApiClient\Client\ResourceClientInterface; -use Akeneo\Pim\ApiClient\Pagination\PageFactoryInterface; -use Akeneo\Pim\ApiClient\Pagination\PageInterface; -use Akeneo\Pim\ApiClient\Pagination\ResourceCursorFactoryInterface; -use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; -use Akeneo\Pim\ApiClient\Stream\UpsertResourceListResponse; use PhpSpec\ObjectBehavior; class MeasurementFamilyApiSpec extends ObjectBehavior diff --git a/spec/Api/ProductApiSpec.php b/spec/Api/ProductApiSpec.php index 37491985..6a9855bc 100644 --- a/spec/Api/ProductApiSpec.php +++ b/spec/Api/ProductApiSpec.php @@ -12,8 +12,8 @@ use Akeneo\Pim\ApiClient\Api\ProductApiInterface; use Akeneo\Pim\ApiClient\Client\ResourceClientInterface; use Akeneo\Pim\ApiClient\Exception\InvalidArgumentException; -use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\PageFactoryInterface; +use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorFactoryInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; use Akeneo\Pim\ApiClient\Stream\UpsertResourceListResponse; diff --git a/spec/Api/ProductDraftApiSpec.php b/spec/Api/ProductDraftApiSpec.php new file mode 100644 index 00000000..bd41142e --- /dev/null +++ b/spec/Api/ProductDraftApiSpec.php @@ -0,0 +1,59 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_initializable() + { + $this->shouldHaveType(ProductDraftApi::class); + $this->shouldImplement(ProductDraftApiInterface::class); + $this->shouldImplement(GettableResourceInterface::class); + } + + function it_gets_a_product_draft($resourceClient) + { + $draft = [ + 'identifier' => 'foo', + 'family' => 'bar', + 'parent' => null, + 'groups' => [], + 'categories' => [], + 'enabled' => true, + 'values' => [], + 'created' => 'this is a date formatted to ISO-8601', + 'updated' => 'this is a date formatted to ISO-8601', + 'associations' => [], + 'metadata' => [ + 'workflow_status' => 'draft_in_progress', + ], + ]; + + $resourceClient->getResource(ProductDraftApi::PRODUCT_DRAFT_URI, ['foo'])->willReturn($draft); + + $this->get('foo')->shouldReturn($draft); + } + + function it_submits_a_product_draft_for_approval($resourceClient) + { + $resourceClient->createResource(ProductDraftApi::PRODUCT_PROPOSAL_URI, ['foo'])->willReturn(201); + + $this->submitForApproval('foo')->shouldReturn(201); + } +} diff --git a/spec/Api/ProductMediaFileApiSpec.php b/spec/Api/ProductMediaFileApiSpec.php index dd887d07..d5d7db9d 100644 --- a/spec/Api/ProductMediaFileApiSpec.php +++ b/spec/Api/ProductMediaFileApiSpec.php @@ -2,16 +2,16 @@ namespace spec\Akeneo\Pim\ApiClient\Api; +use Akeneo\Pim\ApiClient\Api\MediaFileApiInterface; use Akeneo\Pim\ApiClient\Api\Operation\DownloadableResourceInterface; use Akeneo\Pim\ApiClient\Api\Operation\GettableResourceInterface; use Akeneo\Pim\ApiClient\Api\Operation\ListableResourceInterface; use Akeneo\Pim\ApiClient\Api\ProductMediaFileApi; -use Akeneo\Pim\ApiClient\Api\MediaFileApiInterface; use Akeneo\Pim\ApiClient\Client\ResourceClientInterface; use Akeneo\Pim\ApiClient\Exception\RuntimeException; use Akeneo\Pim\ApiClient\FileSystem\FileSystemInterface; -use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\PageFactoryInterface; +use Akeneo\Pim\ApiClient\Pagination\PageInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorFactoryInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; use PhpSpec\ObjectBehavior; diff --git a/spec/Api/ProductModelDraftApiSpec.php b/spec/Api/ProductModelDraftApiSpec.php new file mode 100644 index 00000000..6ef8ab7a --- /dev/null +++ b/spec/Api/ProductModelDraftApiSpec.php @@ -0,0 +1,56 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_initializable() + { + $this->shouldHaveType(ProductModelDraftApi::class); + $this->shouldImplement(ProductModelDraftApiInterface::class); + $this->shouldImplement(GettableResourceInterface::class); + } + + function it_gets_a_product_model_draft($resourceClient) + { + $draft = [ + 'identifier' => 'a_product_model', + 'family_variant' => 'bar', + 'parent' => null, + 'values' => [], + 'created' => 'this is a date formatted to ISO-8601', + 'updated' => 'this is a date formatted to ISO-8601', + 'associations' => [], + 'metadata' => [ + 'workflow_status' => 'draft_in_progress', + ], + ]; + + $resourceClient->getResource(ProductModelDraftApi::PRODUCT_MODEL_DRAFT_URI, ['a_product_model'])->willReturn($draft); + + $this->get('a_product_model')->shouldReturn($draft); + } + + function it_submits_a_product_model_draft_for_approval($resourceClient) + { + $resourceClient->createResource(ProductModelDraftApi::PRODUCT_MODEL_PROPOSAL_URI, ['a_product_model'])->willReturn(201); + + $this->submitForApproval('a_product_model')->shouldReturn(201); + } +} diff --git a/spec/Api/PublishedProductApiSpec.php b/spec/Api/PublishedProductApiSpec.php new file mode 100644 index 00000000..5e3ac249 --- /dev/null +++ b/spec/Api/PublishedProductApiSpec.php @@ -0,0 +1,118 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_initializable() + { + $this->shouldHaveType(PublishedProductApi::class); + $this->shouldImplement(PublishedProductApiInterface::class); + $this->shouldImplement(GettableResourceInterface::class); + $this->shouldImplement(ListableResourceInterface::class); + } + + function it_returns_a_published_product($resourceClient) + { + $publishedProductCode = 'foo'; + $publishedProduct = [ + 'identifier' => 'foo', + 'family' => 'tshirts', + 'enabled' => true, + 'categories' => [ + 'bar', + ], + ]; + + $resourceClient + ->getResource(PublishedProductApi::PUBLISHED_PRODUCT_URI, [$publishedProductCode]) + ->willReturn($publishedProduct); + + $this->get($publishedProductCode)->shouldReturn($publishedProduct); + } + + function it_returns_a_list_of_published_products_with_default_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(PublishedProductApi::PUBLISHED_PRODUCTS_URI, [], 10, false, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage()->shouldReturn($page); + } + + function it_returns_a_list_of_published_products_with_limit_and_count( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(PublishedProductApi::PUBLISHED_PRODUCTS_URI, [], 10, true, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true)->shouldReturn($page); + } + + function it_returns_a_cursor_on_the_list_of_published_products( + $resourceClient, + $pageFactory, + $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources( + PublishedProductApi::PUBLISHED_PRODUCTS_URI, + [], + 10, + false, + ['pagination_type' => 'search_after'] + ) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $cursorFactory->createCursor(10, $page)->willReturn($cursor); + + $this->all(10, [])->shouldReturn($cursor); + } + + function it_returns_a_list_of_published_products_with_additional_query_parameters( + $resourceClient, + $pageFactory, + PageInterface $page + ) { + $resourceClient + ->getResources(PublishedProductApi::PUBLISHED_PRODUCTS_URI, [], 10, true, ['foo' => 'bar']) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + + $this->listPerPage(10, true, ['foo' => 'bar'])->shouldReturn($page); + } +} diff --git a/spec/Api/ReferenceEntityApiSpec.php b/spec/Api/ReferenceEntityApiSpec.php new file mode 100644 index 00000000..41251643 --- /dev/null +++ b/spec/Api/ReferenceEntityApiSpec.php @@ -0,0 +1,84 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_a_reference_entity_api() + { + $this->shouldImplement(ReferenceEntityApiInterface::class); + } + + function it_returns_a_reference_entity(ResourceClientInterface $resourceClient) + { + $referenceEntity = [ + '_links' => [ + 'image_download' => [ + 'href' => 'https://localhost/api/rest/v1/reference-entities-media-files/img.png' + ] + ], + 'code' => 'designer', + 'labels' => [ + 'en_US' => 'Designer' + ], + 'image' => 'img.png' + ]; + + $resourceClient + ->getResource(ReferenceEntityApi::REFERENCE_ENTITY_URI, ['designer']) + ->willReturn($referenceEntity); + + $this->get('designer')->shouldReturn($referenceEntity); + } + + function it_returns_a_cursor_to_list_all_the_reference_entities( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources(ReferenceEntityApi::REFERENCE_ENTITIES_URI, [], null, false, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + $cursorFactory->createCursor(null, $page)->willReturn($cursor); + + $this->all()->shouldReturn($cursor); + } + + function it_upserts_a_reference_entity(ResourceClientInterface $resourceClient) + { + $referenceEntityData = [ + 'code' => 'designer', + 'labels' => [ + 'en_US' => 'Designer' + ] + ]; + $resourceClient + ->upsertResource(ReferenceEntityApi::REFERENCE_ENTITY_URI, ['designer'], $referenceEntityData) + ->willReturn(204); + + $this->upsert('designer', $referenceEntityData)->shouldReturn(204); + } +} diff --git a/spec/Api/ReferenceEntityAttributeApiSpec.php b/spec/Api/ReferenceEntityAttributeApiSpec.php new file mode 100644 index 00000000..d4773201 --- /dev/null +++ b/spec/Api/ReferenceEntityAttributeApiSpec.php @@ -0,0 +1,126 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_a_reference_entity_attribute_api() + { + $this->shouldImplement(ReferenceEntityAttributeApiInterface::class); + } + + function it_returns_a_reference_entity_attribute(ResourceClientInterface $resourceClient) + { + $attribute = [ + 'code' => 'description', + 'labels' => [ + 'en_US' => 'Description', + 'fr_FR' => 'Description', + ], + 'type' => 'text', + 'localizable' => true, + 'scopable' => false, + 'is_required_for_completeness' => true, + 'max_characters' => null, + 'is_textarea' => true, + 'is_rich_text_editor' => true, + 'validation_rule' => null, + 'validation_regexp' => null, + ]; + + $resourceClient + ->getResource(ReferenceEntityAttributeApi::REFERENCE_ENTITY_ATTRIBUTE_URI, ['designer', 'description']) + ->willReturn($attribute); + + $this->get('designer', 'description')->shouldReturn($attribute); + } + + function it_returns_a_cursor_to_list_all_the_attributes_of_a_reference_entity(ResourceClientInterface $resourceClient) + { + $attributes = [ + [ + 'code' => 'label', + 'labels' => [ + 'en_US' => 'Label', + ], + 'type' => 'text', + 'localizable' => true, + 'scopable' => false, + 'is_required_for_completeness' => false, + 'max_characters' => null, + 'is_textarea' => false, + 'is_rich_text_editor' => false, + 'validation_rule' => 'none', + 'validation_regexp' => null, + '_links' => [ + 'self' => [ + 'href' => 'http://localhost/api/rest/v1/reference-entities/designer/attributes/label', + ], + ], + ], + [ + 'code' => 'birthdate', + 'labels' => [ + 'en_US' => 'Birthdate', + ], + 'type' => 'text', + 'localizable' => false, + 'scopable' => false, + 'is_required_for_completeness' => false, + 'max_characters' => null, + 'is_textarea' => false, + 'is_rich_text_editor' => false, + 'validation_rule' => 'none', + 'validation_regexp' => null, + '_links' => [ + 'self' => [ + 'href' => 'http://localhost/api/rest/v1/reference-entities/designer/attributes/birthdate', + ], + ], + ], + ]; + + $resourceClient + ->getResource(ReferenceEntityAttributeApi::REFERENCE_ENTITY_ATTRIBUTES_URI, ['designer']) + ->willReturn($attributes); + + $this->all('designer', [])->shouldReturn($attributes); + } + + function it_upserts_a_reference_entity_attribute(ResourceClientInterface $resourceClient) + { + $attributeData = [ + 'code' => 'description', + 'labels' => [ + 'en_US' => 'Description', + 'fr_FR' => 'Description', + ], + 'type' => 'text', + 'localizable' => true, + 'scopable' => false + ]; + + $resourceClient + ->upsertResource(ReferenceEntityAttributeApi::REFERENCE_ENTITY_ATTRIBUTE_URI, ['designer', 'description'], $attributeData) + ->willReturn(204); + + $this->upsert('designer', 'description', $attributeData)->shouldReturn(204); + } +} diff --git a/spec/Api/ReferenceEntityAttributeOptionApiSpec.php b/spec/Api/ReferenceEntityAttributeOptionApiSpec.php new file mode 100644 index 00000000..14ca5610 --- /dev/null +++ b/spec/Api/ReferenceEntityAttributeOptionApiSpec.php @@ -0,0 +1,91 @@ +beConstructedWith($resourceClient); + } + + function it_is_a_reference_entity_attribute_api() + { + $this->shouldImplement(ReferenceEntityAttributeOptionApiInterface::class); + } + + function it_returns_a_reference_entity_attribute_option(ResourceClientInterface $resourceClient) + { + $option = [ + 'code' => 'red', + 'labels' => [ + 'en_US' => 'Red', + 'fr_FR' => 'Rouge', + ], + ]; + + $resourceClient + ->getResource( + ReferenceEntityAttributeOptionApi::REFERENCE_ENTITY_ATTRIBUTE_OPTION_URI, + ['designer', 'favorite_color', 'red'] + ) + ->willReturn($option); + + $this->get('designer', 'favorite_color', 'red')->shouldReturn($option); + } + + function it_returns_the_list_of_attribute_options_of_a_reference_entity_attribute(ResourceClientInterface $resourceClient) + { + $options = [ + [ + 'code' => 'red', + 'labels' => [ + 'en_US' => 'Red', + 'fr_FR' => 'Rouge', + ], + ], + [ + 'code' => 'blue', + 'labels' => [ + 'en_US' => 'Blue', + 'fr_FR' => 'Bleu', + ], + ] + ]; + + $resourceClient + ->getResource( + ReferenceEntityAttributeOptionApi::REFERENCE_ENTITY_ATTRIBUTE_OPTIONS_URI, + ['designer', 'favorite_color'] + ) + ->willReturn($options); + + $this->all('designer', 'favorite_color')->shouldReturn($options); + } + + function it_upserts_a_reference_entity_attribute_option(ResourceClientInterface $resourceClient) + { + $option = [ + 'code' => 'red', + 'labels' => [ + 'en_US' => 'Red', + 'fr_FR' => 'Rouge', + ], + ]; + + $resourceClient + ->upsertResource( + ReferenceEntityAttributeOptionApi::REFERENCE_ENTITY_ATTRIBUTE_OPTION_URI, + ['designer', 'favorite_color', 'red'], + $option + ) + ->willReturn(204); + + $this->upsert('designer', 'favorite_color', 'red', $option)->shouldReturn(204); + } +} diff --git a/spec/Api/ReferenceEntityMediaFileApiSpec.php b/spec/Api/ReferenceEntityMediaFileApiSpec.php new file mode 100644 index 00000000..e0abd26f --- /dev/null +++ b/spec/Api/ReferenceEntityMediaFileApiSpec.php @@ -0,0 +1,63 @@ +beConstructedWith($resourceClient, $fileSystem); + } + + function it_is_a_reference_entity_media_file_api() + { + $this->shouldImplement(ReferenceEntityMediaFileApiInterface::class); + } + + function it_downloads_a_reference_entity_media_file(ResourceClientInterface $resourceClient, ResponseInterface $response) + { + $resourceClient + ->getStreamedResource(ReferenceEntityMediaFileApi::MEDIA_FILE_DOWNLOAD_URI, ['images/starck.jpg']) + ->willReturn($response); + + $this->download('images/starck.jpg')->shouldReturn($response); + } + + function it_creates_a_reference_entity_media_file( + ResourceClientInterface $resourceClient, + FileSystemInterface $fileSystem, + ResponseInterface $response + ) { + $fileResource = fopen('php://memory', 'r'); + $fileSystem->getResourceFromPath(Argument::any())->shouldNotBeCalled(); + + $requestParts = [ + [ + 'name' => 'file', + 'contents' => $fileResource, + ] + ]; + + $response->getHeader('Reference-entities-media-file-code')->shouldBeCalled()->willReturn( + [ + '0/f/b/f/0fbffddc95c3d610b39e3f3797b14c6c33e98a4f_starck.jpg' + ] + ); + + $resourceClient + ->createMultipartResource(ReferenceEntityMediaFileApi::MEDIA_FILE_CREATE_URI, [], $requestParts) + ->shouldBeCalled() + ->willReturn($response); + + $this->create($fileResource) + ->shouldReturn('0/f/b/f/0fbffddc95c3d610b39e3f3797b14c6c33e98a4f_starck.jpg'); + } +} diff --git a/spec/Api/ReferenceEntityRecordApiSpec.php b/spec/Api/ReferenceEntityRecordApiSpec.php new file mode 100644 index 00000000..808c4cc9 --- /dev/null +++ b/spec/Api/ReferenceEntityRecordApiSpec.php @@ -0,0 +1,137 @@ +beConstructedWith($resourceClient, $pageFactory, $cursorFactory); + } + + function it_is_a_reference_entity_record_api() + { + $this->shouldImplement(ReferenceEntityRecordApiInterface::class); + } + + function it_returns_a_reference_entity_record(ResourceClientInterface $resourceClient) + { + $record = [ + 'code' => 'starck', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'Philippe Starck' + ], + ] + ] + ]; + + $resourceClient + ->getResource(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORD_URI, ['designer', 'starck']) + ->willReturn($record); + + $this->get('designer', 'starck')->shouldReturn($record); + } + + function it_returns_a_cursor_to_list_all_the_records_of_reference_entity( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory, + PageInterface $page, + ResourceCursorInterface $cursor + ) { + $resourceClient + ->getResources(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORDS_URI, ['designer'], null, false, []) + ->willReturn([]); + + $pageFactory->createPage([])->willReturn($page); + $cursorFactory->createCursor(null, $page)->willReturn($cursor); + + $this->all('designer', [])->shouldReturn($cursor); + } + + function it_upserts_a_reference_entity_record(ResourceClientInterface $resourceClient) + { + $recordData = [ + 'code' => 'starck', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'Philippe Starck' + ], + ] + ] + ]; + $resourceClient + ->upsertResource(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORD_URI, ['designer', 'starck'], $recordData) + ->willReturn(204); + + $this->upsert('designer', 'starck', $recordData)->shouldReturn(204); + } + + function it_upserts_a_list_of_reference_entity_records(ResourceClientInterface $resourceClient) + { + $records = [ + [ + 'code' => 'starck', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'Philippe Starck' + ], + ] + ] + ], + [ + 'code' => 'dyson', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'James Dyson' + ], + ] + ] + ] + ]; + + $responses = [ + [ + 'code' => 'starck', + 'status_code' =>204 + ], + [ + 'code' => 'dyson', + 'status_code' =>201 + ], + ]; + + $resourceClient + ->upsertJsonResourceList(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORDS_URI, ['designer'], $records) + ->willReturn($responses); + + $this->upsertList('designer', $records)->shouldReturn($responses); + } +} diff --git a/spec/Client/AuthenticatedHttpClientSpec.php b/spec/Client/AuthenticatedHttpClientSpec.php index 35ab751d..8ae0b81f 100644 --- a/spec/Client/AuthenticatedHttpClientSpec.php +++ b/spec/Client/AuthenticatedHttpClientSpec.php @@ -3,10 +3,10 @@ namespace spec\Akeneo\Pim\ApiClient\Client; use Akeneo\Pim\ApiClient\Api\AuthenticationApiInterface; -use Akeneo\Pim\ApiClient\Exception\UnauthorizedHttpException; use Akeneo\Pim\ApiClient\Client\AuthenticatedHttpClient; use Akeneo\Pim\ApiClient\Client\HttpClient; use Akeneo\Pim\ApiClient\Client\HttpClientInterface; +use Akeneo\Pim\ApiClient\Exception\UnauthorizedHttpException; use Akeneo\Pim\ApiClient\Security\Authentication; use PhpSpec\ObjectBehavior; use Psr\Http\Message\ResponseInterface; diff --git a/spec/Client/HttpClientSpec.php b/spec/Client/HttpClientSpec.php index c139eaa9..5141e4cc 100644 --- a/spec/Client/HttpClientSpec.php +++ b/spec/Client/HttpClientSpec.php @@ -3,8 +3,8 @@ namespace spec\Akeneo\Pim\ApiClient\Client; use Akeneo\Pim\ApiClient\Client\HttpClient; -use Akeneo\Pim\ApiClient\Exception\HttpException; use Akeneo\Pim\ApiClient\Client\HttpClientInterface; +use Akeneo\Pim\ApiClient\Exception\HttpException; use PhpSpec\ObjectBehavior; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; diff --git a/spec/Client/HttpExceptionHandlerSpec.php b/spec/Client/HttpExceptionHandlerSpec.php index e70db4b2..f26ec7d0 100644 --- a/spec/Client/HttpExceptionHandlerSpec.php +++ b/spec/Client/HttpExceptionHandlerSpec.php @@ -2,6 +2,7 @@ namespace spec\Akeneo\Pim\ApiClient\Client; +use Akeneo\Pim\ApiClient\Client\HttpExceptionHandler; use Akeneo\Pim\ApiClient\Exception\BadRequestHttpException; use Akeneo\Pim\ApiClient\Exception\ClientErrorHttpException; use Akeneo\Pim\ApiClient\Exception\ForbiddenHttpException; @@ -13,7 +14,6 @@ use Akeneo\Pim\ApiClient\Exception\TooManyRequestsHttpException; use Akeneo\Pim\ApiClient\Exception\UnauthorizedHttpException; use Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException; -use Akeneo\Pim\ApiClient\Client\HttpExceptionHandler; use Akeneo\Pim\ApiClient\Exception\UnsupportedMediaTypeHttpException; use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; diff --git a/spec/Client/ResourceClientSpec.php b/spec/Client/ResourceClientSpec.php index 8f683e82..84064540 100644 --- a/spec/Client/ResourceClientSpec.php +++ b/spec/Client/ResourceClientSpec.php @@ -2,14 +2,14 @@ namespace spec\Akeneo\Pim\ApiClient\Client; +use Akeneo\Pim\ApiClient\Client\HttpClient; use Akeneo\Pim\ApiClient\Client\HttpClientInterface; use Akeneo\Pim\ApiClient\Client\ResourceClient; use Akeneo\Pim\ApiClient\Client\ResourceClientInterface; use Akeneo\Pim\ApiClient\Exception\InvalidArgumentException; -use Akeneo\Pim\ApiClient\Client\HttpClient; use Akeneo\Pim\ApiClient\MultipartStream\MultipartStreamBuilder; -use Akeneo\Pim\ApiClient\Stream\MultipartStreamBuilderFactory; use Akeneo\Pim\ApiClient\Routing\UriGeneratorInterface; +use Akeneo\Pim\ApiClient\Stream\MultipartStreamBuilderFactory; use Akeneo\Pim\ApiClient\Stream\UpsertResourceListResponse; use Akeneo\Pim\ApiClient\Stream\UpsertResourceListResponseFactory; use PhpSpec\ObjectBehavior; diff --git a/spec/Exception/HttpExceptionSpec.php b/spec/Exception/HttpExceptionSpec.php index 2da09893..a44ca5bc 100644 --- a/spec/Exception/HttpExceptionSpec.php +++ b/spec/Exception/HttpExceptionSpec.php @@ -7,7 +7,6 @@ use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; class HttpExceptionSpec extends ObjectBehavior { diff --git a/src/AkeneoPimClient.php b/src/AkeneoPimClient.php index bda181ce..8bdb5a9a 100644 --- a/src/AkeneoPimClient.php +++ b/src/AkeneoPimClient.php @@ -2,6 +2,16 @@ namespace Akeneo\Pim\ApiClient; +use Akeneo\Pim\ApiClient\Api\AssetApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetCategoryApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetApiInterface as AssetManagerApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetFamilyApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetReferenceFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetTagApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetVariationFileApiInterface; use Akeneo\Pim\ApiClient\Api\AssociationTypeApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeGroupApiInterface; @@ -16,7 +26,15 @@ use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApiInterface; use Akeneo\Pim\ApiClient\Api\MediaFileApiInterface; use Akeneo\Pim\ApiClient\Api\ProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductDraftApiInterface; use Akeneo\Pim\ApiClient\Api\ProductModelApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductModelDraftApiInterface; +use Akeneo\Pim\ApiClient\Api\PublishedProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityRecordApiInterface; use Akeneo\Pim\ApiClient\Security\Authentication; /** @@ -76,6 +94,60 @@ class AkeneoPimClient implements AkeneoPimClientInterface /** @var MeasurementFamilyApiInterface */ private $measurementFamilyApi; + /** @var PublishedProductApiInterface */ + private $publishedProductApi; + + /** @var ProductModelDraftApiInterface */ + private $productModelDraftApi; + + /** @var ProductDraftApiInterface */ + private $productDraftApi; + + /** @var AssetApiInterface */ + private $assetApi; + + /** @var AssetCategoryApiInterface */ + private $assetCategoryApi; + + /** @var AssetTagApiInterface */ + private $assetTagApi; + + /** @var AssetReferenceFileApiInterface */ + private $assetReferenceFileApi; + + /** @var AssetVariationFileApiInterface */ + private $assetVariationFileApi; + + /** @var ReferenceEntityRecordApiInterface */ + private $referenceEntityRecordApi; + + /** @var ReferenceEntityMediaFileApiInterface */ + private $referenceEntityMediaFileApi; + + /** @var ReferenceEntityAttributeApiInterface */ + private $referenceEntityAttributeApi; + + /** @var ReferenceEntityAttributeOptionApiInterface */ + private $referenceEntityAttributeOptionApi; + + /** @var ReferenceEntityApiInterface */ + private $referenceEntityApi; + + /** @var AssetManagerApiInterface */ + private $assetManagerApi; + + /** @var AssetFamilyApiInterface */ + private $assetFamilyApi; + + /** @var AssetAttributeApiInterface */ + private $assetAttributeApi; + + /** @var AssetAttributeOptionApiInterface */ + private $assetAttributeOptionApi; + + /** @var AssetMediaFileApiInterface */ + private $assetMediaFileApi; + public function __construct( Authentication $authentication, ProductApiInterface $productApi, @@ -92,7 +164,25 @@ public function __construct( MeasurementFamilyApiInterface $measurementFamilyApi, AssociationTypeApiInterface $associationTypeApi, FamilyVariantApiInterface $familyVariantApi, - ProductModelApiInterface $productModelApi + ProductModelApiInterface $productModelApi, + ProductModelDraftApiInterface $productModelDraftApi, + PublishedProductApiInterface $publishedProductApi, + ProductDraftApiInterface $productDraftApi, + AssetApiInterface $assetApi, + AssetCategoryApiInterface $assetCategoryApi, + AssetTagApiInterface $assetTagApi, + AssetReferenceFileApiInterface $assetReferenceFileApi, + AssetVariationFileApiInterface $assetVariationFileApi, + ReferenceEntityRecordApiInterface $referenceEntityRecordApi, + ReferenceEntityMediaFileApiInterface $referenceEntityMediaFileApi, + ReferenceEntityAttributeApiInterface $referenceEntityAttributeApi, + ReferenceEntityAttributeOptionApiInterface $referenceEntityAttributeOptionApi, + ReferenceEntityApiInterface $referenceEntityApi, + AssetManagerApiInterface $assetManagerApi, + AssetFamilyApiInterface $assetFamilyApi, + AssetAttributeApiInterface $assetAttributeApi, + AssetAttributeOptionApiInterface $assetAttributeOptionApi, + AssetMediaFileApiInterface $assetMediaFileApi ) { $this->authentication = $authentication; $this->productApi = $productApi; @@ -110,6 +200,24 @@ public function __construct( $this->associationTypeApi = $associationTypeApi; $this->familyVariantApi = $familyVariantApi; $this->productModelApi = $productModelApi; + $this->publishedProductApi = $publishedProductApi; + $this->productDraftApi = $productDraftApi; + $this->productModelDraftApi = $productModelDraftApi; + $this->assetApi = $assetApi; + $this->assetCategoryApi = $assetCategoryApi; + $this->assetTagApi = $assetTagApi; + $this->assetReferenceFileApi = $assetReferenceFileApi; + $this->assetVariationFileApi = $assetVariationFileApi; + $this->referenceEntityRecordApi = $referenceEntityRecordApi; + $this->referenceEntityMediaFileApi = $referenceEntityMediaFileApi; + $this->referenceEntityAttributeApi = $referenceEntityAttributeApi; + $this->referenceEntityAttributeOptionApi = $referenceEntityAttributeOptionApi; + $this->referenceEntityApi = $referenceEntityApi; + $this->assetManagerApi = $assetManagerApi; + $this->assetFamilyApi = $assetFamilyApi; + $this->assetAttributeApi = $assetAttributeApi; + $this->assetAttributeOptionApi = $assetAttributeOptionApi; + $this->assetMediaFileApi = $assetMediaFileApi; } /** @@ -247,4 +355,148 @@ public function getProductModelApi(): ProductModelApiInterface { return $this->productModelApi; } + + /** + * {@inheritdoc} + */ + public function getPublishedProductApi(): PublishedProductApiInterface + { + return $this->publishedProductApi; + } + + /** + * {@inheritdoc} + */ + public function getProductModelDraftApi(): ProductModelDraftApiInterface + { + return $this->productModelDraftApi; + } + + /** + * {@inheritdoc} + */ + public function getProductDraftApi(): ProductDraftApiInterface + { + return $this->productDraftApi; + } + + /** + * @return AssetApiInterface + */ + public function getAssetApi(): AssetApiInterface + { + return $this->assetApi; + } + + /** + * {@inheritdoc} + */ + public function getAssetCategoryApi(): AssetCategoryApiInterface + { + return $this->assetCategoryApi; + } + + /** + * {@inheritdoc} + */ + public function getAssetTagApi(): AssetTagApiInterface + { + return $this->assetTagApi; + } + + /** + * {@inheritdoc} + */ + public function getAssetReferenceFileApi(): AssetReferenceFileApiInterface + { + return $this->assetReferenceFileApi; + } + + /** + * {@inheritdoc} + */ + public function getAssetVariationFileApi(): AssetVariationFileApiInterface + { + return $this->assetVariationFileApi; + } + + /** + * {@inheritdoc} + */ + public function getReferenceEntityRecordApi(): ReferenceEntityRecordApiInterface + { + return $this->referenceEntityRecordApi; + } + + /** + * {@inheritdoc} + */ + public function getReferenceEntityMediaFileApi(): ReferenceEntityMediaFileApiInterface + { + return $this->referenceEntityMediaFileApi; + } + + /** + * {@inheritdoc} + */ + public function getReferenceEntityAttributeApi(): ReferenceEntityAttributeApiInterface + { + return $this->referenceEntityAttributeApi; + } + + /** + * {@inheritdoc} + */ + public function getReferenceEntityAttributeOptionApi(): ReferenceEntityAttributeOptionApiInterface + { + return $this->referenceEntityAttributeOptionApi; + } + + /** + * {@inheritdoc} + */ + public function getReferenceEntityApi(): ReferenceEntityApiInterface + { + return $this->referenceEntityApi; + } + + /** + * {@inheritDoc} + */ + public function getAssetManagerApi(): AssetManagerApiInterface + { + return $this->assetManagerApi; + } + + /** + * {@inheritDoc} + */ + public function getAssetFamilyApi(): AssetFamilyApiInterface + { + return $this->assetFamilyApi; + } + + /** + * {@inheritDoc} + */ + public function getAssetAttributeApi(): AssetAttributeApiInterface + { + return $this->assetAttributeApi; + } + + /** + * {@inheritDoc} + */ + public function getAssetAttributeOptionApi(): AssetAttributeOptionApiInterface + { + return $this->assetAttributeOptionApi; + } + + /** + * {@inheritDoc} + */ + public function getAssetMediaFileApi(): AssetMediaFileApiInterface + { + return $this->assetMediaFileApi; + } } diff --git a/src/AkeneoPimClientBuilder.php b/src/AkeneoPimClientBuilder.php index 7e74618b..0589b5fb 100644 --- a/src/AkeneoPimClientBuilder.php +++ b/src/AkeneoPimClientBuilder.php @@ -2,6 +2,16 @@ namespace Akeneo\Pim\ApiClient; +use Akeneo\Pim\ApiClient\Api\AssetApi; +use Akeneo\Pim\ApiClient\Api\AssetCategoryApi; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetApi as AssetManagerApi; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeApi; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeOptionApi; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetFamilyApi; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetMediaFileApi; +use Akeneo\Pim\ApiClient\Api\AssetReferenceFileApi; +use Akeneo\Pim\ApiClient\Api\AssetTagApi; +use Akeneo\Pim\ApiClient\Api\AssetVariationFileApi; use Akeneo\Pim\ApiClient\Api\AssociationTypeApi; use Akeneo\Pim\ApiClient\Api\AttributeApi; use Akeneo\Pim\ApiClient\Api\AttributeGroupApi; @@ -16,8 +26,16 @@ use Akeneo\Pim\ApiClient\Api\MeasureFamilyApi; use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApi; use Akeneo\Pim\ApiClient\Api\ProductApi; +use Akeneo\Pim\ApiClient\Api\ProductDraftApi; use Akeneo\Pim\ApiClient\Api\ProductMediaFileApi; use Akeneo\Pim\ApiClient\Api\ProductModelApi; +use Akeneo\Pim\ApiClient\Api\ProductModelDraftApi; +use Akeneo\Pim\ApiClient\Api\PublishedProductApi; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityApi; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeApi; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeOptionApi; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityMediaFileApi; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityRecordApi; use Akeneo\Pim\ApiClient\Cache\LRUCache; use Akeneo\Pim\ApiClient\Client\AuthenticatedHttpClient; use Akeneo\Pim\ApiClient\Client\CachedResourceClient; @@ -181,7 +199,7 @@ protected function buildAuthenticatedClient(Authentication $authentication): Ake { [$resourceClient, $pageFactory, $cursorFactory, $fileSystem] = $this->setUp($authentication); - $resourceClientWithCache = !$this->cacheEnabled ? $resourceClient : new CachedResourceClient($resourceClient, new Cache()); + $resourceClientWithCache = !$this->cacheEnabled ? $resourceClient : new CachedResourceClient($resourceClient, new LRUCache()); return new AkeneoPimClient( $authentication, @@ -199,7 +217,25 @@ protected function buildAuthenticatedClient(Authentication $authentication): Ake new MeasurementFamilyApi($resourceClientWithCache), new AssociationTypeApi($resourceClientWithCache, $pageFactory, $cursorFactory), new FamilyVariantApi($resourceClientWithCache, $pageFactory, $cursorFactory), - new ProductModelApi($resourceClient, $pageFactory, $cursorFactory) + new ProductModelApi($resourceClient, $pageFactory, $cursorFactory), + new ProductModelDraftApi($resourceClient, $pageFactory, $cursorFactory), + new PublishedProductApi($resourceClient, $pageFactory, $cursorFactory), + new ProductDraftApi($resourceClient, $pageFactory, $cursorFactory), + new AssetApi($resourceClient, $pageFactory, $cursorFactory), + new AssetCategoryApi($resourceClient, $pageFactory, $cursorFactory), + new AssetTagApi($resourceClient, $pageFactory, $cursorFactory), + new AssetReferenceFileApi($resourceClient, $fileSystem), + new AssetVariationFileApi($resourceClient, $fileSystem), + new ReferenceEntityRecordApi($resourceClient, $pageFactory, $cursorFactory), + new ReferenceEntityMediaFileApi($resourceClient, $fileSystem), + new ReferenceEntityAttributeApi($resourceClient), + new ReferenceEntityAttributeOptionApi($resourceClient), + new ReferenceEntityApi($resourceClient, $pageFactory, $cursorFactory), + new AssetManagerApi($resourceClient, $pageFactory, $cursorFactory), + new AssetFamilyApi($resourceClient, $pageFactory, $cursorFactory), + new AssetAttributeApi($resourceClient), + new AssetAttributeOptionApi($resourceClient), + new AssetMediaFileApi($resourceClient, $fileSystem) ); } diff --git a/src/AkeneoPimClientInterface.php b/src/AkeneoPimClientInterface.php index 94e20804..83e65b5a 100644 --- a/src/AkeneoPimClientInterface.php +++ b/src/AkeneoPimClientInterface.php @@ -2,6 +2,16 @@ namespace Akeneo\Pim\ApiClient; +use Akeneo\Pim\ApiClient\Api\AssetApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetCategoryApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetApiInterface as AssetManagerApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetFamilyApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetManager\AssetMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetReferenceFileApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetTagApiInterface; +use Akeneo\Pim\ApiClient\Api\AssetVariationFileApiInterface; use Akeneo\Pim\ApiClient\Api\AssociationTypeApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeApiInterface; use Akeneo\Pim\ApiClient\Api\AttributeGroupApiInterface; @@ -16,7 +26,15 @@ use Akeneo\Pim\ApiClient\Api\MeasurementFamilyApiInterface; use Akeneo\Pim\ApiClient\Api\MediaFileApiInterface; use Akeneo\Pim\ApiClient\Api\ProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductDraftApiInterface; use Akeneo\Pim\ApiClient\Api\ProductModelApiInterface; +use Akeneo\Pim\ApiClient\Api\ProductModelDraftApiInterface; +use Akeneo\Pim\ApiClient\Api\PublishedProductApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityAttributeOptionApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityMediaFileApiInterface; +use Akeneo\Pim\ApiClient\Api\ReferenceEntityRecordApiInterface; /** * Client to use the Akeneo PIM API. @@ -60,4 +78,40 @@ public function getAssociationTypeApi(): AssociationTypeApiInterface; public function getFamilyVariantApi(): FamilyVariantApiInterface; public function getProductModelApi(): ProductModelApiInterface; + + public function getPublishedProductApi(): PublishedProductApiInterface; + + public function getProductModelDraftApi(): ProductModelDraftApiInterface; + + public function getProductDraftApi(): ProductDraftApiInterface; + + public function getAssetApi(): AssetApiInterface; + + public function getAssetCategoryApi(): AssetCategoryApiInterface; + + public function getAssetTagApi(): AssetTagApiInterface; + + public function getAssetReferenceFileApi(): AssetReferenceFileApiInterface; + + public function getAssetVariationFileApi(): AssetVariationFileApiInterface; + + public function getReferenceEntityRecordApi(): ReferenceEntityRecordApiInterface; + + public function getReferenceEntityMediaFileApi(): ReferenceEntityMediaFileApiInterface; + + public function getReferenceEntityAttributeApi(): ReferenceEntityAttributeApiInterface; + + public function getReferenceEntityAttributeOptionApi(): ReferenceEntityAttributeOptionApiInterface; + + public function getReferenceEntityApi(): ReferenceEntityApiInterface; + + public function getAssetManagerApi(): AssetManagerApiInterface; + + public function getAssetFamilyApi(): AssetFamilyApiInterface; + + public function getAssetAttributeApi(): AssetAttributeApiInterface; + + public function getAssetAttributeOptionApi(): AssetAttributeOptionApiInterface; + + public function getAssetMediaFileApi(): AssetMediaFileApiInterface; } diff --git a/src/Api/AssetApi.php b/src/Api/AssetApi.php new file mode 100644 index 00000000..86a16603 --- /dev/null +++ b/src/Api/AssetApi.php @@ -0,0 +1,109 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class AssetApi implements AssetApiInterface +{ + const ASSETS_URI = '/api/rest/v1/assets'; + const ASSET_URI = '/api/rest/v1/assets/%s'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var PageFactoryInterface */ + private $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + private $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::ASSET_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function all(int $pageSize = 10, array $queryParameters = []): ResourceCursorInterface + { + $queryParameters['pagination_type'] = 'search_after'; + + $firstPage = $this->listPerPage($pageSize, false, $queryParameters); + + return $this->cursorFactory->createCursor($pageSize, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function listPerPage(int $limit = 10, bool $withCount = false, array $queryParameters = []): PageInterface + { + $data = $this->resourceClient->getResources( + static::ASSETS_URI, + [], + $limit, + $withCount, + $queryParameters + ); + return $this->pageFactory->createPage($data); + } + + /** + * {@inheritdoc} + */ + public function create(string $code, array $data = []): int + { + if (array_key_exists('code', $data)) { + throw new InvalidArgumentException('The parameter "code" should not be defined in the data parameter'); + } + + $data['code'] = $code; + + return $this->resourceClient->createResource(static::ASSETS_URI, [], $data); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $code, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_URI, [$code], $data); + } + + /** + * {@inheritdoc} + */ + public function upsertList($resources): \Traversable + { + return $this->resourceClient->upsertStreamResourceList(static::ASSETS_URI, [], $resources); + } +} diff --git a/src/Api/AssetApiInterface.php b/src/Api/AssetApiInterface.php new file mode 100644 index 00000000..aa1dc85c --- /dev/null +++ b/src/Api/AssetApiInterface.php @@ -0,0 +1,27 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface AssetApiInterface extends + GettableResourceInterface, + ListableResourceInterface, + CreatableResourceInterface, + UpsertableResourceInterface, + UpsertableResourceListInterface +{ +} diff --git a/src/Api/AssetCategoryApi.php b/src/Api/AssetCategoryApi.php new file mode 100644 index 00000000..2dd04c0c --- /dev/null +++ b/src/Api/AssetCategoryApi.php @@ -0,0 +1,108 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class AssetCategoryApi implements AssetCategoryApiInterface +{ + const ASSET_CATEGORIES_URI = '/api/rest/v1/asset-categories'; + const ASSET_CATEGORY_URI = '/api/rest/v1/asset-categories/%s'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var PageFactoryInterface */ + private $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + private $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::ASSET_CATEGORY_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function all(int $pageSize = 10, array $queryParameters = []): ResourceCursorInterface + { + $firstPage = $this->listPerPage($pageSize, false, $queryParameters); + + return $this->cursorFactory->createCursor($pageSize, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function listPerPage(int $limit = 10, bool $withCount = false, array $queryParameters = []): PageInterface + { + $data = $this->resourceClient->getResources( + static::ASSET_CATEGORIES_URI, + [], + $limit, + $withCount, + $queryParameters + ); + + return $this->pageFactory->createPage($data); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $code, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_CATEGORY_URI, [$code], $data); + } + + /** + * {@inheritdoc} + */ + public function upsertList($resources): \Traversable + { + return $this->resourceClient->upsertStreamResourceList(static::ASSET_CATEGORIES_URI, [], $resources); + } + + /** + * {@inheritdoc} + */ + public function create(string $code, array $data = []): int + { + if (array_key_exists('code', $data)) { + throw new InvalidArgumentException('The parameter "code" should not be defined in the data parameter'); + } + + $data['code'] = $code; + + return $this->resourceClient->createResource(static::ASSET_CATEGORIES_URI, [], $data); + } +} diff --git a/src/Api/AssetCategoryApiInterface.php b/src/Api/AssetCategoryApiInterface.php new file mode 100644 index 00000000..03ea3b0c --- /dev/null +++ b/src/Api/AssetCategoryApiInterface.php @@ -0,0 +1,27 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface AssetCategoryApiInterface extends + GettableResourceInterface, + ListableResourceInterface, + UpsertableResourceInterface, + UpsertableResourceListInterface, + CreatableResourceInterface +{ +} diff --git a/src/Api/AssetManager/AssetApi.php b/src/Api/AssetManager/AssetApi.php new file mode 100644 index 00000000..2435e304 --- /dev/null +++ b/src/Api/AssetManager/AssetApi.php @@ -0,0 +1,85 @@ +resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $assetFamilyCode, string $assetCode): array + { + return $this->resourceClient->getResource(static::ASSET_URI, [$assetFamilyCode, $assetCode]); + } + + /** + * {@inheritdoc} + */ + public function all(string $assetFamilyCode, array $queryParameters = []): ResourceCursorInterface + { + $data = $this->resourceClient->getResources( + static::ASSETS_URI, + [$assetFamilyCode], + null, + null, + $queryParameters + ); + + $firstPage = $this->pageFactory->createPage($data); + + return $this->cursorFactory->createCursor(null, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $assetFamilyCode, string $assetCode, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_URI, [$assetFamilyCode, $assetCode], $data); + } + + /** + * {@inheritdoc} + */ + public function upsertList(string $assetFamilyCode, array $assets): array + { + return $this->resourceClient->upsertJsonResourceList(static::ASSETS_URI, [$assetFamilyCode], $assets); + } + + /** + * {@inheritdoc} + */ + public function delete(string $assetFamilyCode, string $assetCode): int + { + return $this->resourceClient->deleteResource(static::ASSET_URI, [$assetFamilyCode, $assetCode]); + } +} diff --git a/src/Api/AssetManager/AssetApiInterface.php b/src/Api/AssetManager/AssetApiInterface.php new file mode 100644 index 00000000..07fe5b36 --- /dev/null +++ b/src/Api/AssetManager/AssetApiInterface.php @@ -0,0 +1,70 @@ +resourceClient = $resourceClient; + } + + /** + * {@inheritdoc} + */ + public function get(string $assetFamilyCode, string $attributeCode): array + { + return $this->resourceClient->getResource(static::ASSET_ATTRIBUTE_URI, [$assetFamilyCode, $attributeCode]); + } + + /** + * {@inheritdoc} + */ + public function all(string $assetFamilyCode, array $queryParameters = []): array + { + return $this->resourceClient->getResource(static::ASSET_ATTRIBUTES_URI, [$assetFamilyCode]); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $assetFamilyCode, string $attributeCode, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_ATTRIBUTE_URI, [$assetFamilyCode, $attributeCode], $data); + } +} diff --git a/src/Api/AssetManager/AssetAttributeApiInterface.php b/src/Api/AssetManager/AssetAttributeApiInterface.php new file mode 100644 index 00000000..a1ccbf76 --- /dev/null +++ b/src/Api/AssetManager/AssetAttributeApiInterface.php @@ -0,0 +1,34 @@ +resourceClient = $resourceClient; + } + + /** + * {@inheritdoc} + */ + public function get(string $assetFamilyCode, string $attributeCode, string $attributeOptionCode): array + { + return $this->resourceClient->getResource( + static::ASSET_ATTRIBUTE_OPTION_URI, + [$assetFamilyCode, $attributeCode, $attributeOptionCode] + ); + } + + /** + * {@inheritdoc} + */ + public function all(string $assetFamilyCode, string $attributeCode): array + { + return $this->resourceClient->getResource( + static::ASSET_ATTRIBUTE_OPTIONS_URI, + [$assetFamilyCode, $attributeCode] + ); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $assetFamilyCode, string $attributeCode, string $attributeOptionCode, array $data = []): int + { + return $this->resourceClient->upsertResource( + static::ASSET_ATTRIBUTE_OPTION_URI, + [$assetFamilyCode, $attributeCode, $attributeOptionCode], + $data + ); + } +} diff --git a/src/Api/AssetManager/AssetAttributeOptionApiInterface.php b/src/Api/AssetManager/AssetAttributeOptionApiInterface.php new file mode 100644 index 00000000..510d0724 --- /dev/null +++ b/src/Api/AssetManager/AssetAttributeOptionApiInterface.php @@ -0,0 +1,34 @@ +resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::ASSET_FAMILY_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function all(array $queryParameters = []): ResourceCursorInterface + { + $data = $this->resourceClient->getResources( + static::ASSET_FAMILIES_URI, + [], + null, + false, + $queryParameters + ); + + $firstPage = $this->pageFactory->createPage($data); + + return $this->cursorFactory->createCursor(null, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $code, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_FAMILY_URI, [$code], $data); + } +} diff --git a/src/Api/AssetManager/AssetFamilyApiInterface.php b/src/Api/AssetManager/AssetFamilyApiInterface.php new file mode 100644 index 00000000..e892f909 --- /dev/null +++ b/src/Api/AssetManager/AssetFamilyApiInterface.php @@ -0,0 +1,35 @@ +resourceClient = $resourceClient; + $this->fileSystem = $fileSystem; + } + + /** + * {@inheritdoc} + */ + public function download($code): ResponseInterface + { + return $this->resourceClient->getStreamedResource(static::MEDIA_FILE_DOWNLOAD_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function create($mediaFile): string + { + if (is_string($mediaFile)) { + $mediaFile = $this->fileSystem->getResourceFromPath($mediaFile); + } + + $requestParts = [ + [ + 'name' => 'file', + 'contents' => $mediaFile, + ] + ]; + + $response = $this->resourceClient->createMultipartResource(static::MEDIA_FILE_CREATE_URI, [], $requestParts); + + return $this->extractCodeFromCreationResponse($response); + } + + /** + * Extracts the code of a media-file from a creation response. + * + * @param ResponseInterface $response + * + * @throws RuntimeException if unable to extract the code + * + * @return string + */ + private function extractCodeFromCreationResponse(ResponseInterface $response): string + { + $header = $response->getHeader('asset-media-file-code'); + + if (empty($header)) { + throw new RuntimeException('The response does not contain the code of the created media-file.'); + } + + return (string) $header[0]; + } +} diff --git a/src/Api/AssetManager/AssetMediaFileApiInterface.php b/src/Api/AssetManager/AssetMediaFileApiInterface.php new file mode 100644 index 00000000..b62b4998 --- /dev/null +++ b/src/Api/AssetManager/AssetMediaFileApiInterface.php @@ -0,0 +1,34 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class AssetReferenceFileApi implements AssetReferenceFileApiInterface +{ + const ASSET_REFERENCE_FILE_URI = '/api/rest/v1/assets/%s/reference-files/%s'; + const ASSET_REFERENCE_FILE_DOWNLOAD_URI = '/api/rest/v1/assets/%s/reference-files/%s/download'; + const NOT_LOCALIZABLE_ASSET_LOCALE_CODE = 'no-locale'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var FileSystemInterface */ + private $fileSystem; + + public function __construct(ResourceClientInterface $resourceClient, FileSystemInterface $fileSystem) + { + $this->resourceClient = $resourceClient; + $this->fileSystem = $fileSystem; + } + + /** + * {@inheritdoc} + */ + public function getFromLocalizableAsset(string $assetCode, string $localeCode): array + { + return $this->get($assetCode, $localeCode); + } + + /** + * {@inheritdoc} + */ + public function getFromNotLocalizableAsset(string $assetCode): array + { + return $this->get($assetCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE); + } + + /** + * {@inheritdoc} + */ + public function uploadForLocalizableAsset($referenceFile, string $assetCode, string $localeCode): int + { + return $this->upload($referenceFile, $assetCode, $localeCode); + } + + /** + * {@inheritdoc} + */ + public function uploadForNotLocalizableAsset($referenceFile, string $assetCode): int + { + return $this->upload($referenceFile, $assetCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE); + } + + /** + * {@inheritdoc} + */ + public function downloadFromLocalizableAsset(string $assetCode, string $localeCode): ResponseInterface + { + return $this->resourceClient + ->getStreamedResource(static::ASSET_REFERENCE_FILE_DOWNLOAD_URI, [$assetCode, $localeCode]); + } + + /** + * {@inheritdoc} + */ + public function downloadFromNotLocalizableAsset(string $assetCode): ResponseInterface + { + return $this->resourceClient + ->getStreamedResource(static::ASSET_REFERENCE_FILE_DOWNLOAD_URI, [$assetCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE]); + } + + /** + * @param string|resource $referenceFile + * @param string $assetCode + * @param string $localeCode + * + * @return int + */ + private function upload($referenceFile, string $assetCode, string $localeCode): int + { + if (is_string($referenceFile)) { + $referenceFile = $this->fileSystem->getResourceFromPath($referenceFile); + } + + $requestParts = [[ + 'name' => 'file', + 'contents' => $referenceFile, + ]]; + + $response = $this->resourceClient->createMultipartResource(static::ASSET_REFERENCE_FILE_URI, [$assetCode, $localeCode], $requestParts); + $this->handleUploadErrors($response); + + return $response->getStatusCode(); + } + + /** + * @throws UploadAssetReferenceFileErrorException if an upload returns any errors. + */ + private function handleUploadErrors(ResponseInterface $response): void + { + $decodedResponse = json_decode($response->getBody()->getContents(), true); + $errors = isset($decodedResponse['errors']) ? $decodedResponse['errors'] : null; + + if (is_array($errors) && !empty($errors)) { + $message = isset($decodedResponse['message']) ? $decodedResponse['message'] : 'Errors occurred during the upload.'; + + throw new UploadAssetReferenceFileErrorException($message, $errors); + } + } + + private function get(string $assetCode, string $localeCode): array + { + return $this->resourceClient->getResource(static::ASSET_REFERENCE_FILE_URI, [$assetCode, $localeCode]); + } +} diff --git a/src/Api/AssetReferenceFileApiInterface.php b/src/Api/AssetReferenceFileApiInterface.php new file mode 100644 index 00000000..3c3eda6b --- /dev/null +++ b/src/Api/AssetReferenceFileApiInterface.php @@ -0,0 +1,100 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface AssetReferenceFileApiInterface +{ + /** + * Available since Akeneo PIM 2.1. + * Gets an asset reference file by its asset code and local code for a localizable asset. + * + * @param string $assetCode code of the asset + * @param string $localeCode code of the locale + * + * @throws HttpException If the request failed + * + * @return array + */ + public function getFromLocalizableAsset(string $assetCode, string $localeCode): array; + + /** + * Available since Akeneo PIM 2.1. + * Gets an asset reference file by its asset code for a not localizable asset. + * + * @param string $assetCode code of the asset + * + * @throws HttpException If the request failed + * + * @return array + */ + public function getFromNotLocalizableAsset(string $assetCode): array; + + /** + * Available since Akeneo PIM 2.1. + * Uploads a new reference file for a given localizable asset and locale. + * It will also automatically generate all the variation files corresponding to this reference file. + * + * @param string|resource $referenceFile file path or resource of the reference file to upload + * @param string $assetCode code of the asset + * @param string $localeCode code of the locale + * + * @throws HttpException If the request failed + * @throws UploadAssetReferenceFileErrorException If the upload returned any errors + * + * @return int Status code 201 indicating that the asset reference file has been well uploaded + */ + public function uploadForLocalizableAsset($referenceFile, string $assetCode, string $localeCode): int; + + /** + * Available since Akeneo PIM 2.1. + * Uploads a new reference file for a given not localizable asset. + * It will also automatically generate all the variation files corresponding to this reference file. + * + * @param string|resource $referenceFile file path or resource of the reference file to upload + * @param string $assetCode code of the asset + * + * @throws HttpException If the request failed + * @throws UploadAssetReferenceFileErrorException If the upload returned any errors + * + * @return int Status code 201 indicating that the asset reference file has been well uploaded + */ + public function uploadForNotLocalizableAsset($referenceFile, string $assetCode): int; + + /** + * Available since Akeneo PIM 2.1. + * Download an asset reference file by its asset code and local code for a localizable asset. + * + * @param string $assetCode code of the asset + * @param string $localeCode code of the locale + * + * @throws HttpException If the request failed + * + * @return ResponseInterface + */ + public function downloadFromLocalizableAsset(string $assetCode, string $localeCode): ResponseInterface; + + /** + * Available since Akeneo PIM 2.1. + * Download an asset reference file by its asset code for a not localizable asset. + * + * @param string $assetCode code of the asset + * + * @throws HttpException If the request failed + * + * @return ResponseInterface + */ + public function downloadFromNotLocalizableAsset(string $assetCode): ResponseInterface; +} diff --git a/src/Api/AssetTagApi.php b/src/Api/AssetTagApi.php new file mode 100644 index 00000000..31bc78b9 --- /dev/null +++ b/src/Api/AssetTagApi.php @@ -0,0 +1,85 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class AssetTagApi implements AssetTagApiInterface +{ + const ASSET_TAGS_URI = '/api/rest/v1/asset-tags'; + const ASSET_TAG_URI = '/api/rest/v1/asset-tags/%s'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var PageFactoryInterface */ + private $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + private $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::ASSET_TAG_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $code, array $data = []): int + { + return $this->resourceClient->upsertResource(static::ASSET_TAG_URI, [$code], $data); + } + + /** + * {@inheritdoc} + */ + public function all(int $pageSize = 10, array $queryParameters = []): ResourceCursorInterface + { + $firstPage = $this->listPerPage($pageSize, false, $queryParameters); + + return $this->cursorFactory->createCursor($pageSize, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function listPerPage(int $limit = 10, bool $withCount = false, array $queryParameters = []): PageInterface + { + $data = $this->resourceClient->getResources( + static::ASSET_TAGS_URI, + [], + $limit, + $withCount, + $queryParameters + ); + + return $this->pageFactory->createPage($data); + } +} diff --git a/src/Api/AssetTagApiInterface.php b/src/Api/AssetTagApiInterface.php new file mode 100644 index 00000000..f4283f5f --- /dev/null +++ b/src/Api/AssetTagApiInterface.php @@ -0,0 +1,20 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface AssetTagApiInterface extends GettableResourceInterface, ListableResourceInterface, UpsertableResourceInterface +{ +} diff --git a/src/Api/AssetVariationFileApi.php b/src/Api/AssetVariationFileApi.php new file mode 100644 index 00000000..217ee420 --- /dev/null +++ b/src/Api/AssetVariationFileApi.php @@ -0,0 +1,122 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class AssetVariationFileApi implements AssetVariationFileApiInterface +{ + const ASSET_VARIATION_FILE_URI = '/api/rest/v1/assets/%s/variation-files/%s/%s'; + const ASSET_VARIATION_FILE_DOWNLOAD_URI = '/api/rest/v1/assets/%s/variation-files/%s/%s/download'; + const NOT_LOCALIZABLE_ASSET_LOCALE_CODE = 'no-locale'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var FileSystemInterface */ + private $fileSystem; + + public function __construct(ResourceClientInterface $resourceClient, FileSystemInterface $fileSystem) + { + $this->resourceClient = $resourceClient; + $this->fileSystem = $fileSystem; + } + + /** + * {@inheritdoc} + */ + public function getFromNotLocalizableAsset(string $assetCode, string $channelCode): array + { + return $this->get($assetCode, $channelCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE); + } + + /** + * {@inheritdoc} + */ + public function getFromLocalizableAsset(string $assetCode, string $channelCode, string $localeCode): array + { + return $this->get($assetCode, $channelCode, $localeCode); + } + + /** + * {@inheritdoc} + */ + public function uploadForNotLocalizableAsset($variationFile, string $assetCode, string $channelCode): int + { + return $this->upload($variationFile, $assetCode, $channelCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE); + } + + /** + * {@inheritdoc} + */ + public function uploadForLocalizableAsset($variationFile, string $assetCode, string $channelCode, string $localeCode): int + { + return $this->upload($variationFile, $assetCode, $channelCode, $localeCode); + } + + /** + * {@inheritdoc} + */ + public function downloadFromLocalizableAsset(string $assetCode, string $channelCode, string $localeCode): ResponseInterface + { + return $this->resourceClient->getStreamedResource( + static::ASSET_VARIATION_FILE_DOWNLOAD_URI, + [$assetCode, $channelCode, $localeCode] + ); + } + + /** + * {@inheritdoc} + */ + public function downloadFromNotLocalizableAsset(string $assetCode, string $channelCode): ResponseInterface + { + return $this->resourceClient->getStreamedResource( + static::ASSET_VARIATION_FILE_DOWNLOAD_URI, + [$assetCode, $channelCode, static::NOT_LOCALIZABLE_ASSET_LOCALE_CODE] + ); + } + + private function get(string $assetCode, string $channelCode, string $localeCode): array + { + return $this->resourceClient->getResource(static::ASSET_VARIATION_FILE_URI, [$assetCode, $channelCode, $localeCode]); + } + + /** + * @param string|resource $variationFile + * @param string $assetCode + * @param string $channelCode + * @param string $localeCode + * + * @return int + */ + private function upload($variationFile, string $assetCode, string $channelCode, string $localeCode): int + { + if (is_string($variationFile)) { + $variationFile = $this->fileSystem->getResourceFromPath($variationFile); + } + + $requestParts = [[ + 'name' => 'file', + 'contents' => $variationFile, + ]]; + + $response = $this->resourceClient->createMultipartResource( + static::ASSET_VARIATION_FILE_URI, + [$assetCode, $channelCode, $localeCode], + $requestParts + ); + + return $response->getStatusCode(); + } +} diff --git a/src/Api/AssetVariationFileApiInterface.php b/src/Api/AssetVariationFileApiInterface.php new file mode 100644 index 00000000..188e358f --- /dev/null +++ b/src/Api/AssetVariationFileApiInterface.php @@ -0,0 +1,88 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface AssetVariationFileApiInterface +{ + /** + * Available since Akeneo PIM 2.1. + * Gets an asset variation file by its asset code, channel code and local code for a localizable asset. + * + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * @param string $localeCode code of the locale + * + * @return array + */ + public function getFromLocalizableAsset(string $assetCode, string $channelCode, string $localeCode): array; + + /** + * Available since Akeneo PIM 2.1. + * Gets an asset variation file by its asset code and channel code for a not localizable asset. + * + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * + * @return array + */ + public function getFromNotLocalizableAsset(string $assetCode, string $channelCode): array; + + /** + * Available since Akeneo PIM 2.1. + * Uploads a new variation file for a given localizable asset, channel and locale. + * + * @param string|resource $variationFile file path or resource of the variation file to upload + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * @param string $localeCode code of the locale + * + * @return int Status code 201 indicating that the asset variation file has been well uploaded + */ + public function uploadForLocalizableAsset($variationFile, string $assetCode, string $channelCode, string $localeCode): int; + + /** + * Available since Akeneo PIM 2.1. + * Uploads a new variation file for a given not localizable asset and channel. + * + * @param string|resource $variationFile file path or resource of the variation file to upload + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * + * @return int Status code 201 indicating that the asset variation file has been well uploaded + */ + public function uploadForNotLocalizableAsset($variationFile, string $assetCode, string $channelCode): int; + + /** + * Available since Akeneo PIM 2.1. + * Downloads an asset variation file by its asset code, channel code and local code for a localizable asset. + * + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * @param string $localeCode code of the locale + * + * @return ResponseInterface + */ + public function downloadFromLocalizableAsset(string $assetCode, string $channelCode, string $localeCode): ResponseInterface; + + /** + * Available since Akeneo PIM 2.1. + * Downloads an asset variation file by its asset code and channel code for a not localizable asset. + * + * @param string $assetCode code of the asset + * @param string $channelCode code of the channel + * + * @return ResponseInterface + */ + public function downloadFromNotLocalizableAsset(string $assetCode, string $channelCode): ResponseInterface; +} diff --git a/src/Api/Operation/DownloadableResourceInterface.php b/src/Api/Operation/DownloadableResourceInterface.php index 916c62dd..dd5ee5a2 100644 --- a/src/Api/Operation/DownloadableResourceInterface.php +++ b/src/Api/Operation/DownloadableResourceInterface.php @@ -4,7 +4,6 @@ use Akeneo\Pim\ApiClient\Exception\HttpException; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; /** * API that can download a resource. diff --git a/src/Api/ProductDraftApi.php b/src/Api/ProductDraftApi.php new file mode 100644 index 00000000..7ef80932 --- /dev/null +++ b/src/Api/ProductDraftApi.php @@ -0,0 +1,57 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ProductDraftApi implements ProductDraftApiInterface +{ + const PRODUCT_DRAFT_URI = '/api/rest/v1/products/%s/draft'; + const PRODUCT_PROPOSAL_URI = '/api/rest/v1/products/%s/proposal'; + + /** @var ResourceClientInterface */ + protected $resourceClient; + + /** @var PageFactoryInterface */ + protected $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + protected $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::PRODUCT_DRAFT_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function submitForApproval($code) + { + return $this->resourceClient->createResource(static::PRODUCT_PROPOSAL_URI, [$code]); + } +} diff --git a/src/Api/ProductDraftApiInterface.php b/src/Api/ProductDraftApiInterface.php new file mode 100644 index 00000000..f82360f1 --- /dev/null +++ b/src/Api/ProductDraftApiInterface.php @@ -0,0 +1,26 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ProductDraftApiInterface extends GettableResourceInterface +{ + /** + * Submits a product draft for approval, by its code. + * + * @param string $code + * + * @return int + */ + public function submitForApproval($code); +} diff --git a/src/Api/ProductMediaFileApi.php b/src/Api/ProductMediaFileApi.php index 77bacb5c..4e27e7f9 100644 --- a/src/Api/ProductMediaFileApi.php +++ b/src/Api/ProductMediaFileApi.php @@ -10,7 +10,6 @@ use Akeneo\Pim\ApiClient\Pagination\ResourceCursorFactoryInterface; use Akeneo\Pim\ApiClient\Pagination\ResourceCursorInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; /** * API implementation to manage the media files for the products. diff --git a/src/Api/ProductModelDraftApi.php b/src/Api/ProductModelDraftApi.php new file mode 100644 index 00000000..a789074c --- /dev/null +++ b/src/Api/ProductModelDraftApi.php @@ -0,0 +1,57 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ProductModelDraftApi implements ProductModelDraftApiInterface +{ + const PRODUCT_MODEL_DRAFT_URI = '/api/rest/v1/product-models/%s/draft'; + const PRODUCT_MODEL_PROPOSAL_URI = '/api/rest/v1/product-models/%s/proposal'; + + /** @var ResourceClientInterface */ + protected $resourceClient; + + /** @var PageFactoryInterface */ + protected $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + protected $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::PRODUCT_MODEL_DRAFT_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function submitForApproval($code) + { + return $this->resourceClient->createResource(static::PRODUCT_MODEL_PROPOSAL_URI, [$code]); + } +} diff --git a/src/Api/ProductModelDraftApiInterface.php b/src/Api/ProductModelDraftApiInterface.php new file mode 100644 index 00000000..22013155 --- /dev/null +++ b/src/Api/ProductModelDraftApiInterface.php @@ -0,0 +1,26 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ProductModelDraftApiInterface extends GettableResourceInterface +{ + /** + * Submits a product model draft for approval, by its code. + * + * @param string $code + * + * @return int + */ + public function submitForApproval($code); +} diff --git a/src/Api/PublishedProductApi.php b/src/Api/PublishedProductApi.php new file mode 100644 index 00000000..9dfd0a82 --- /dev/null +++ b/src/Api/PublishedProductApi.php @@ -0,0 +1,79 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class PublishedProductApi implements PublishedProductApiInterface +{ + const PUBLISHED_PRODUCTS_URI = 'api/rest/v1/published-products'; + const PUBLISHED_PRODUCT_URI = 'api/rest/v1/published-products/%s'; + + /** @var ResourceClientInterface */ + protected $resourceClient; + + /** @var PageFactoryInterface */ + protected $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + protected $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $code): array + { + return $this->resourceClient->getResource(static::PUBLISHED_PRODUCT_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function listPerPage(int $limit = 10, bool $withCount = false, array $queryParameters = []): PageInterface + { + $data = $this->resourceClient->getResources( + static::PUBLISHED_PRODUCTS_URI, + [], + $limit, + $withCount, + $queryParameters + ); + + return $this->pageFactory->createPage($data); + } + + /** + * {@inheritdoc} + */ + public function all(int $pageSize = 10, array $queryParameters = []): ResourceCursorInterface + { + $queryParameters['pagination_type'] = 'search_after'; + + $firstPage = $this->listPerPage($pageSize, false, $queryParameters); + + return $this->cursorFactory->createCursor($pageSize, $firstPage); + } +} diff --git a/src/Api/PublishedProductApiInterface.php b/src/Api/PublishedProductApiInterface.php new file mode 100644 index 00000000..20ee9efa --- /dev/null +++ b/src/Api/PublishedProductApiInterface.php @@ -0,0 +1,21 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface PublishedProductApiInterface extends + ListableResourceInterface, + GettableResourceInterface +{ +} diff --git a/src/Api/ReferenceEntityApi.php b/src/Api/ReferenceEntityApi.php new file mode 100644 index 00000000..0351ba39 --- /dev/null +++ b/src/Api/ReferenceEntityApi.php @@ -0,0 +1,74 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ReferenceEntityApi implements ReferenceEntityApiInterface +{ + const REFERENCE_ENTITY_URI = 'api/rest/v1/reference-entities/%s'; + const REFERENCE_ENTITIES_URI= 'api/rest/v1/reference-entities'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var PageFactoryInterface */ + private $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + private $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $referenceEntityCode): array + { + return $this->resourceClient->getResource(static::REFERENCE_ENTITY_URI, [$referenceEntityCode]); + } + + /** + * {@inheritdoc} + */ + public function all(array $queryParameters = []): ResourceCursorInterface + { + $data = $this->resourceClient->getResources( + static::REFERENCE_ENTITIES_URI, + [], + null, + false, + $queryParameters + ); + + $firstPage = $this->pageFactory->createPage($data); + + return $this->cursorFactory->createCursor(null, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $referenceEntityCode, array $data = []): int + { + return $this->resourceClient->upsertResource(static::REFERENCE_ENTITY_URI, [$referenceEntityCode], $data); + } +} diff --git a/src/Api/ReferenceEntityApiInterface.php b/src/Api/ReferenceEntityApiInterface.php new file mode 100644 index 00000000..2e085ad2 --- /dev/null +++ b/src/Api/ReferenceEntityApiInterface.php @@ -0,0 +1,53 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ReferenceEntityApiInterface +{ + /** + * Gets a single reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function get(string $referenceEntityCode): array; + + /** + * Gets a cursor to iterate over the list of reference entities + * + * @param array $queryParameters Additional query parameters to pass in the request + * + * @throws HttpException If the request failed. + * + * @return ResourceCursorInterface + */ + public function all(array $queryParameters = []): ResourceCursorInterface; + + /** + * Creates a reference entity if it does not exist yet, otherwise updates partially the reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * @param array $data Data of the reference entity to create or update + * + * @throws HttpException If the request failed. + * + * @return int Status code 201 indicating that the reference entity has been well created. + * Status code 204 indicating that the reference entity has been well updated. + */ + public function upsert(string $referenceEntityCode, array $data = []): int; +} diff --git a/src/Api/ReferenceEntityAttributeApi.php b/src/Api/ReferenceEntityAttributeApi.php new file mode 100644 index 00000000..bc96dfa1 --- /dev/null +++ b/src/Api/ReferenceEntityAttributeApi.php @@ -0,0 +1,50 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ReferenceEntityAttributeApi implements ReferenceEntityAttributeApiInterface +{ + const REFERENCE_ENTITY_ATTRIBUTE_URI = 'api/rest/v1/reference-entities/%s/attributes/%s'; + const REFERENCE_ENTITY_ATTRIBUTES_URI = 'api/rest/v1/reference-entities/%s/attributes'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + public function __construct(ResourceClientInterface $resourceClient) + { + $this->resourceClient = $resourceClient; + } + + /** + * {@inheritdoc} + */ + public function get(string $referenceEntityCode, string $attributeCode): array + { + return $this->resourceClient->getResource(static::REFERENCE_ENTITY_ATTRIBUTE_URI, [$referenceEntityCode, $attributeCode]); + } + + /** + * {@inheritdoc} + */ + public function all(string $referenceEntityCode, array $queryParameters = []): array + { + return $this->resourceClient->getResource(static::REFERENCE_ENTITY_ATTRIBUTES_URI, [$referenceEntityCode]); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $referenceEntityCode, string $attributeCode, array $data = []): int + { + return $this->resourceClient->upsertResource(static::REFERENCE_ENTITY_ATTRIBUTE_URI, [$referenceEntityCode, $attributeCode], $data); + } +} diff --git a/src/Api/ReferenceEntityAttributeApiInterface.php b/src/Api/ReferenceEntityAttributeApiInterface.php new file mode 100644 index 00000000..04b7f117 --- /dev/null +++ b/src/Api/ReferenceEntityAttributeApiInterface.php @@ -0,0 +1,53 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ReferenceEntityAttributeApiInterface +{ + /** + * Gets a single reference entity attribute. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $attributeCode Code of the attribute + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function get(string $referenceEntityCode, string $attributeCode): array; + + /** + * Gets the list of the attributes of a given reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * @param array $queryParameters Additional query parameters to pass in the request + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function all(string $referenceEntityCode, array $queryParameters = []): array; + + /** + * Creates a reference entity attribute if it does not exist yet, otherwise updates partially the attribute. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $attributeCode Code of the attribute + * @param array $data Data of the attribute to create or update + * + * @throws HttpException If the request failed. + * + * @return int Status code 201 indicating that the reference entity attribute has been well created. + * Status code 204 indicating that the reference entity attribute has been well updated. + */ + public function upsert(string $referenceEntityCode, string $attributeCode, array $data = []): int; +} diff --git a/src/Api/ReferenceEntityAttributeOptionApi.php b/src/Api/ReferenceEntityAttributeOptionApi.php new file mode 100644 index 00000000..915fcf81 --- /dev/null +++ b/src/Api/ReferenceEntityAttributeOptionApi.php @@ -0,0 +1,60 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ReferenceEntityAttributeOptionApi implements ReferenceEntityAttributeOptionApiInterface +{ + const REFERENCE_ENTITY_ATTRIBUTE_OPTION_URI = 'api/rest/v1/reference-entities/%s/attributes/%s/options/%s'; + const REFERENCE_ENTITY_ATTRIBUTE_OPTIONS_URI = 'api/rest/v1/reference-entities/%s/attributes/%s/options'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + public function __construct(ResourceClientInterface $resourceClient) + { + $this->resourceClient = $resourceClient; + } + + /** + * {@inheritdoc} + */ + public function get(string $referenceEntityCode, string $attributeCode, string $attributeOptionCode): array + { + return $this->resourceClient->getResource( + static::REFERENCE_ENTITY_ATTRIBUTE_OPTION_URI, + [$referenceEntityCode, $attributeCode, $attributeOptionCode] + ); + } + + /** + * {@inheritdoc} + */ + public function all(string $referenceEntityCode, string $attributeCode): array + { + return $this->resourceClient->getResource( + static::REFERENCE_ENTITY_ATTRIBUTE_OPTIONS_URI, + [$referenceEntityCode, $attributeCode] + ); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $referenceEntityCode, string $attributeCode, string $attributeOptionCode, array $data = []): int + { + return $this->resourceClient->upsertResource( + static::REFERENCE_ENTITY_ATTRIBUTE_OPTION_URI, + [$referenceEntityCode, $attributeCode, $attributeOptionCode], + $data + ); + } +} diff --git a/src/Api/ReferenceEntityAttributeOptionApiInterface.php b/src/Api/ReferenceEntityAttributeOptionApiInterface.php new file mode 100644 index 00000000..1b1c5f80 --- /dev/null +++ b/src/Api/ReferenceEntityAttributeOptionApiInterface.php @@ -0,0 +1,55 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ReferenceEntityAttributeOptionApiInterface +{ + /** + * Get an attribute option for a given attribute of a given reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $attributeCode Code of the attribute + * @param string $attributeOptionCode Code of the attribute option + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function get(string $referenceEntityCode, string $attributeCode, string $attributeOptionCode): array; + + /** + * Get the list of attribute options of a given attribute for a given reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $attributeCode Code of the attribute + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function all(string $referenceEntityCode, string $attributeCode): array; + + /** + * Creates a reference entity attribute option if it does not exist yet, otherwise updates partially the attribute option. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $attributeCode Code of the attribute + * @param string $attributeOptionCode Code of the attribute option + * @param array $data Data of the attribute option to create or update + * + * @throws HttpException If the request failed. + * + * @return int Status code 201 indicating that the reference entity attribute option has been well created. + * Status code 204 indicating that the reference entity attribute option has been well updated. + */ + public function upsert(string $referenceEntityCode, string $attributeCode, string $attributeOptionCode, array $data = []): int; +} diff --git a/src/Api/ReferenceEntityMediaFileApi.php b/src/Api/ReferenceEntityMediaFileApi.php new file mode 100644 index 00000000..2c99a2da --- /dev/null +++ b/src/Api/ReferenceEntityMediaFileApi.php @@ -0,0 +1,82 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ReferenceEntityMediaFileApi implements ReferenceEntityMediaFileApiInterface +{ + const MEDIA_FILE_DOWNLOAD_URI = 'api/rest/v1/reference-entities-media-files/%s'; + const MEDIA_FILE_CREATE_URI = 'api/rest/v1/reference-entities-media-files'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var FileSystemInterface */ + private $fileSystem; + + public function __construct(ResourceClientInterface $resourceClient, FileSystemInterface $fileSystem) + { + $this->resourceClient = $resourceClient; + $this->fileSystem = $fileSystem; + } + + /** + * {@inheritdoc} + */ + public function download($code): ResponseInterface + { + return $this->resourceClient->getStreamedResource(static::MEDIA_FILE_DOWNLOAD_URI, [$code]); + } + + /** + * {@inheritdoc} + */ + public function create($mediaFile): string + { + if (is_string($mediaFile)) { + $mediaFile = $this->fileSystem->getResourceFromPath($mediaFile); + } + + $requestParts = [ + [ + 'name' => 'file', + 'contents' => $mediaFile, + ] + ]; + + $response = $this->resourceClient->createMultipartResource(static::MEDIA_FILE_CREATE_URI, [], $requestParts); + + return $this->extractCodeFromCreationResponse($response); + } + + /** + * Extracts the code of a media-file from a creation response. + * + * @param ResponseInterface $response + * + * @throws RuntimeException if unable to extract the code + * + * @return string + */ + private function extractCodeFromCreationResponse(ResponseInterface $response): string + { + $header = $response->getHeader('Reference-entities-media-file-code'); + + if (empty($header)) { + throw new RuntimeException('The response does not contain the code of the created media-file.'); + } + + return (string)$header[0]; + } +} diff --git a/src/Api/ReferenceEntityMediaFileApiInterface.php b/src/Api/ReferenceEntityMediaFileApiInterface.php new file mode 100644 index 00000000..a7d4b40c --- /dev/null +++ b/src/Api/ReferenceEntityMediaFileApiInterface.php @@ -0,0 +1,40 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ReferenceEntityMediaFileApiInterface +{ + /** + * Downloads a reference entity media file by its code + * + * @param string $code Code of the media file + * + * @throws HttpException If the request failed. + * + * @return ResponseInterface + */ + public function download($code): ResponseInterface; + + /** + * Creates a new reference entity media file. + * + * @param string|resource $mediaFile File path or resource of the media file + * + * @throws HttpException If the request failed. + * @throws RuntimeException If the file could not be opened. + * + * @return string returns the code of the created media file + */ + public function create($mediaFile): string; +} diff --git a/src/Api/ReferenceEntityRecordApi.php b/src/Api/ReferenceEntityRecordApi.php new file mode 100644 index 00000000..dce21e24 --- /dev/null +++ b/src/Api/ReferenceEntityRecordApi.php @@ -0,0 +1,82 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class ReferenceEntityRecordApi implements ReferenceEntityRecordApiInterface +{ + const REFERENCE_ENTITY_RECORD_URI = 'api/rest/v1/reference-entities/%s/records/%s'; + const REFERENCE_ENTITY_RECORDS_URI = 'api/rest/v1/reference-entities/%s/records'; + + /** @var ResourceClientInterface */ + private $resourceClient; + + /** @var PageFactoryInterface */ + private $pageFactory; + + /** @var ResourceCursorFactoryInterface */ + private $cursorFactory; + + public function __construct( + ResourceClientInterface $resourceClient, + PageFactoryInterface $pageFactory, + ResourceCursorFactoryInterface $cursorFactory + ) { + $this->resourceClient = $resourceClient; + $this->pageFactory = $pageFactory; + $this->cursorFactory = $cursorFactory; + } + + /** + * {@inheritdoc} + */ + public function get(string $referenceEntityCode, string $recordCode): array + { + return $this->resourceClient->getResource(static::REFERENCE_ENTITY_RECORD_URI, [$referenceEntityCode, $recordCode]); + } + + /** + * {@inheritdoc} + */ + public function all(string $referenceEntityCode, array $queryParameters = []): ResourceCursorInterface + { + $data = $this->resourceClient->getResources( + static::REFERENCE_ENTITY_RECORDS_URI, + [$referenceEntityCode], + null, + false, + $queryParameters + ); + + $firstPage = $this->pageFactory->createPage($data); + + return $this->cursorFactory->createCursor(null, $firstPage); + } + + /** + * {@inheritdoc} + */ + public function upsert(string $referenceEntityCode, string $recordCode, array $data = []): int + { + return $this->resourceClient->upsertResource(static::REFERENCE_ENTITY_RECORD_URI, [$referenceEntityCode, $recordCode], $data); + } + + /** + * {@inheritdoc} + */ + public function upsertList(string $referenceEntityCode, array $records): array + { + return $this->resourceClient->upsertJsonResourceList(static::REFERENCE_ENTITY_RECORDS_URI, [$referenceEntityCode], $records); + } +} diff --git a/src/Api/ReferenceEntityRecordApiInterface.php b/src/Api/ReferenceEntityRecordApiInterface.php new file mode 100644 index 00000000..f516050f --- /dev/null +++ b/src/Api/ReferenceEntityRecordApiInterface.php @@ -0,0 +1,68 @@ + + * @copyright 2018 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +interface ReferenceEntityRecordApiInterface +{ + /** + * Gets a single reference entity record. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $recordCode Code of the record + * + * @throws HttpException If the request failed. + * + * @return array + */ + public function get(string $referenceEntityCode, string $recordCode): array; + + /** + * Gets a cursor to iterate over the list of records of a given reference entity. + * + * @param string $referenceEntityCode Code of the reference entity + * @param array $queryParameters Additional query parameters to pass in the request + * + * @throws HttpException If the request failed. + * + * @return ResourceCursorInterface + */ + public function all(string $referenceEntityCode, array $queryParameters = []): ResourceCursorInterface; + + /** + * Creates a reference entity record if it does not exist yet, otherwise updates partially the record. + * + * @param string $referenceEntityCode Code of the reference entity + * @param string $recordCode Code of the record + * @param array $data Data of the record to create or update + * + * @throws HttpException If the request failed. + * + * @return int Status code 201 indicating that the reference entity record has been well created. + * Status code 204 indicating that the reference entity record has been well updated. + */ + public function upsert(string $referenceEntityCode, string $recordCode, array $data = []): int; + + /** + * Updates or creates several reference entity records. + * + * @param string $referenceEntityCode Code of the reference entity + * @param array $records Array containing the records to create or update + * + * @throws HttpException + * + * @return array returns the list of the responses of each created or updated record. + */ + public function upsertList(string $referenceEntityCode, array $records): array; +} diff --git a/src/Exception/UploadAssetReferenceFileErrorException.php b/src/Exception/UploadAssetReferenceFileErrorException.php new file mode 100644 index 00000000..15147241 --- /dev/null +++ b/src/Exception/UploadAssetReferenceFileErrorException.php @@ -0,0 +1,35 @@ + + * @copyright 2017 Akeneo SAS (http://www.akeneo.com) + * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) + */ +class UploadAssetReferenceFileErrorException extends RuntimeException +{ + /** @var array */ + private $errors; + + /** + * @param string $message + * @param array $errors + */ + public function __construct($message, array $errors) + { + parent::__construct($message); + + $this->errors = $errors; + } + + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/tests/Api/Asset/GetAssetIntegration.php b/tests/Api/Asset/GetAssetIntegration.php new file mode 100644 index 00000000..330319fa --- /dev/null +++ b/tests/Api/Asset/GetAssetIntegration.php @@ -0,0 +1,70 @@ +server->setResponseOfPath( + '/'. sprintf(AssetApi::ASSET_URI, 'packshot', 'battleship'), + new ResponseStack( + new Response($this->getAsset(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetManagerApi(); + $asset = $api->get('packshot', 'battleship'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($asset, json_decode($this->getAsset(), true)); + } + + public function test_get_unknown_asset() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetApi::ASSET_URI, 'packshot', 'peace-sheep'), + new ResponseStack( + new Response('{"code": 404, "message":"Asset \"peace-sheep\" does not exist."}', [], 404) + ) + ); + + $this->expectException(\Akeneo\Pim\ApiClient\Exception\NotFoundHttpException::class); + $this->expectExceptionMessage('Asset "peace-sheep" does not exist.'); + + $api = $this->createClient()->getAssetManagerApi(); + $api->get('packshot', 'peace-sheep'); + } + + private function getAsset(): string + { + return <<server->setResponseOfPath( + '/'. sprintf(AssetApi::ASSETS_URI, 'packshot'), + new ResponseStack( + new Response($this->getFirstPage(), [], 200), + new Response($this->getSecondPage(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetManagerApi(); + $assetCursor = $api->all('packshot'); + $assets = iterator_to_array($assetCursor); + + Assert::assertCount(3, $assets); + } + + private function getFirstPage(): string + { + $baseUri = $this->server->getServerRoot(); + + return <<server->getServerRoot(); + + return <<server->setResponseOfPath( + '/'. sprintf(AssetApi::ASSET_URI, 'packshot', 'sku_54628_telescope'), + new ResponseStack( + new Response('', [], 204) + ) + ); + + $asset = [ + "code" => "sku_54628_telescope", + "values" => [ + "media_preview" => [ + [ + "locale" => null, + "channel" => null, + "data" => "sku_54628_picture1.jpg" + ] + ], + "photographer" => [ + [ + "locale" => null, + "channel" => null, + "data" => "ben_levy" + ] + ] + ] + ]; + + $api = $this->createClient()->getAssetManagerApi(); + $response = $api->upsert('packshot', 'sku_54628_telescope', $asset); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($asset)); + Assert::assertSame(204, $response); + } +} diff --git a/tests/Api/Asset/UpsertListAssetIntegration.php b/tests/Api/Asset/UpsertListAssetIntegration.php new file mode 100644 index 00000000..6f0363d1 --- /dev/null +++ b/tests/Api/Asset/UpsertListAssetIntegration.php @@ -0,0 +1,96 @@ +server->setResponseOfPath( + '/'. sprintf(AssetApi::ASSETS_URI, 'packshot'), + new ResponseStack( + new Response($responseBody, [], 200) + ) + ); + + $assets = [ + [ + "code" => "sku_54628_telescope", + "values" => [ + "media_preview" => [ + [ + "locale" => null, + "channel" => null, + "data" => "sku_54628_picture1.jpg" + ] + ], + "photographer" => [ + [ + "locale" => null, + "channel" => null, + "data" => "ben_levy" + ] + ] + ] + ], + [ + "code" => "sku_45689_dobson", + "values" => [ + "media_preview" => [ + [ + "locale" => null, + "channel" => null, + "data" => "sku_45689_dobson_pic1.jpg" + ] + ], + "photographer" => [ + [ + "locale" => null, + "channel" => null, + "data" => "ben_levy" + ] + ] + ] + ] + ]; + + $expectedResponses = [ + [ + 'code' => 'sku_54628_telescope', + 'status_code' => 204 + ], + [ + 'code' => 'sku_45689_dobson', + 'status_code' => 201 + ], + ]; + + $api = $this->createClient()->getAssetManagerApi(); + $responses = $api->upsertList('packshot', $assets); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($assets)); + Assert::assertSame($expectedResponses, $responses); + } +} diff --git a/tests/Api/AssetAttribute/GetAssetFamilyAttributeIntegration.php b/tests/Api/AssetAttribute/GetAssetFamilyAttributeIntegration.php new file mode 100644 index 00000000..73fe0d6f --- /dev/null +++ b/tests/Api/AssetAttribute/GetAssetFamilyAttributeIntegration.php @@ -0,0 +1,68 @@ +server->setResponseOfPath( + '/'. sprintf(AssetAttributeApi::ASSET_ATTRIBUTE_URI, 'packshot', 'media_preview'), + new ResponseStack( + new Response($this->getPackshotPreviewAttribute(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetAttributeApi(); + $familyAttribute = $api->get('packshot', 'media_preview'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($familyAttribute, json_decode($this->getPackshotPreviewAttribute(), true)); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + * @expectedExceptionMessage Resource `foo` does not exist. + */ + public function test_get_unknown_asset_family_attribute() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetAttributeApi::ASSET_ATTRIBUTE_URI, 'packshot', 'foo'), + new ResponseStack( + new Response('{"code": 404, "message":"Resource `foo` does not exist."}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetAttributeApi(); + $api->get('packshot', 'foo'); + } + + private function getPackshotPreviewAttribute(): string + { + return <<server->setResponseOfPath( + '/' . sprintf(AssetAttributeApi::ASSET_ATTRIBUTES_URI, 'packshot'), + new ResponseStack( + new Response($this->getAssetFamilyAttributes(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetAttributeApi(); + $assetFamilyAttributes = $api->all('packshot'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($assetFamilyAttributes, json_decode($this->getAssetFamilyAttributes(), true)); + } + + private function getAssetFamilyAttributes(): string + { + return <<server->setResponseOfPath( + '/'. sprintf(AssetAttributeApi::ASSET_ATTRIBUTE_URI, 'packshot', 'media_preview'), + new ResponseStack( + new Response('', [], 204) + ) + ); + + $assetFamilyAttribute = [ + 'code' => 'media_preview', + 'labels' => [ + 'en_US' => 'Media Preview' + ], + 'type' => 'media_link', + "value_per_locale" => false, + "value_per_channel" => false, + "is_required_for_completeness" => false, + "prefix" => "dam.com/my_assets/", + "suffix" => null, + "media_type" => "image" + ]; + + $api = $this->createClient()->getAssetAttributeApi(); + $response = $api->upsert('packshot', 'media_preview', $assetFamilyAttribute); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($assetFamilyAttribute)); + Assert::assertSame(204, $response); + } +} diff --git a/tests/Api/AssetAttributeOption/GetAssetFamilyAttributeOptionIntegration.php b/tests/Api/AssetAttributeOption/GetAssetFamilyAttributeOptionIntegration.php new file mode 100644 index 00000000..4f48436c --- /dev/null +++ b/tests/Api/AssetAttributeOption/GetAssetFamilyAttributeOptionIntegration.php @@ -0,0 +1,61 @@ +server->setResponseOfPath( + '/'. sprintf(AssetAttributeOptionApi::ASSET_ATTRIBUTE_OPTION_URI, 'packshot', 'wearing_model_size', 'small'), + new ResponseStack( + new Response($this->getPackshotAttributeOption(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetAttributeOptionApi(); + $familyAttributeOption = $api->get('packshot', 'wearing_model_size', 'small'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($familyAttributeOption, json_decode($this->getPackshotAttributeOption(), true)); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + * @expectedExceptionMessage Resource `XLS` does not exist. + */ + public function test_get_unknown_asset_family_attribute_option() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetAttributeOptionApi::ASSET_ATTRIBUTE_OPTION_URI, 'packshot', 'wearing_model_size', 'XLS'), + new ResponseStack( + new Response('{"code": 404, "message":"Resource `XLS` does not exist."}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetAttributeOptionApi(); + $api->get('packshot', 'wearing_model_size', 'XLS'); + } + + private function getPackshotAttributeOption(): string + { + return <<server->setResponseOfPath( + '/' . sprintf( + AssetAttributeOptionApi::ASSET_ATTRIBUTE_OPTIONS_URI, + 'packshot', + 'wearing_model_size' + ), + new ResponseStack( + new Response($this->getAssetFamilyAttributeOptions(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetAttributeOptionApi(); + $assetFamilyAttributeOptions = $api->all('packshot', 'wearing_model_size'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($assetFamilyAttributeOptions, json_decode($this->getAssetFamilyAttributeOptions(), true)); + } + + private function getAssetFamilyAttributeOptions(): string + { + return <<server->setResponseOfPath( + '/'. sprintf( + AssetAttributeOptionApi::ASSET_ATTRIBUTE_OPTION_URI, + 'packshot', + 'wearing_model_size', + 'size_27' + ), + new ResponseStack( + new Response('', [], 204) + ) + ); + + $assetFamilyAttributeOption = [ + "code" => "size_27", + "labels" => [ + "en_US" => "Size 27", + "fr_FR" => "Taille 36" + ] + ]; + + $api = $this->createClient()->getAssetAttributeOptionApi(); + $response = $api->upsert('packshot', 'wearing_model_size', 'size_27', $assetFamilyAttributeOption); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($assetFamilyAttributeOption)); + Assert::assertSame(204, $response); + } +} diff --git a/tests/Api/AssetFamily/GetAssetFamilyIntegration.php b/tests/Api/AssetFamily/GetAssetFamilyIntegration.php new file mode 100644 index 00000000..1c89589c --- /dev/null +++ b/tests/Api/AssetFamily/GetAssetFamilyIntegration.php @@ -0,0 +1,60 @@ +server->setResponseOfPath( + '/'. sprintf(AssetFamilyApi::ASSET_FAMILY_URI, 'packshot'), + new ResponseStack( + new Response($this->getPackshot(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetFamilyApi(); + $product = $api->get('packshot'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($product, json_decode($this->getPackshot(), true)); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + * @expectedExceptionMessage Asset family "foo" does not exist. + */ + public function test_get_unknown_asset_family() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetFamilyApi::ASSET_FAMILY_URI, 'foo'), + new ResponseStack( + new Response('{"code": 404, "message":"Asset family \"foo\" does not exist."}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetFamilyApi(); + $api->get('foo'); + } + + private function getPackshot(): string + { + return <<server->setResponseOfPath( + '/' . sprintf(AssetFamilyApi::ASSET_FAMILIES_URI), + new ResponseStack( + new Response($this->getFirstPage(), [], 200), + new Response($this->getSecondPage(), [], 200) + ) + ); + + $api = $this->createClient()->getAssetFamilyApi(); + $assetFamilyCursor = $api->all(); + $assetFamilies = iterator_to_array($assetFamilyCursor); + + Assert::assertCount(3, $assetFamilies); + } + + private function getFirstPage(): string + { + $baseUri = $this->server->getServerRoot(); + + return <<server->getServerRoot(); + + return <<server->setResponseOfPath( + '/'. sprintf(AssetFamilyApi::ASSET_FAMILY_URI, 'packshot'), + new ResponseStack( + new Response('', [], 201) + ) + ); + + $assetFamily = [ + 'code' => 'packshot', + 'labels' => [ + 'en_US' => 'Packshots' + ] + ]; + + $api = $this->createClient()->getAssetFamilyApi(); + $response = $api->upsert('packshot', $assetFamily); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($assetFamily)); + Assert::assertSame(201, $response); + } +} diff --git a/tests/Api/AssetMediaFile/CreateAssetMediaFileIntegration.php b/tests/Api/AssetMediaFile/CreateAssetMediaFileIntegration.php new file mode 100644 index 00000000..57c18c57 --- /dev/null +++ b/tests/Api/AssetMediaFile/CreateAssetMediaFileIntegration.php @@ -0,0 +1,54 @@ +server->setResponseOfPath( + '/' . AssetMediaFileApi::MEDIA_FILE_CREATE_URI, + new ResponseStack( + new Response('', ['Asset-media-file-code' => 'my-asset-media-code'], 201) + ) + ); + $mediaFile = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + $response = $this->createClient()->getAssetMediaFileApi()->create($mediaFile); + + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], + 'unicorn.png' + ); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], + 'image/png' + ); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], + 11255 + ); + Assert::assertSame('my-asset-media-code', $response); + } + + public function test_get_asset_media_file_code_regardless_of_the_header_case() + { + $this->server->setResponseOfPath( + '/' . AssetMediaFileApi::MEDIA_FILE_CREATE_URI, + new ResponseStack( + new Response('', ['Asset-Media-File-Code' => 'my-asset-media-code'], 201) + ) + ); + $mediaFile = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + $response = $this->createClient()->getAssetMediaFileApi()->create($mediaFile); + + Assert::assertSame('my-asset-media-code', $response); + } +} diff --git a/tests/Api/AssetReferenceFile/DownloadAssetReferenceFileIntegration.php b/tests/Api/AssetReferenceFile/DownloadAssetReferenceFileIntegration.php new file mode 100644 index 00000000..be36d505 --- /dev/null +++ b/tests/Api/AssetReferenceFile/DownloadAssetReferenceFileIntegration.php @@ -0,0 +1,87 @@ +server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, 'ziggy', 'en_US'), + new ResponseStack( + new Response(file_get_contents($expectedFilePath), [], 201) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $downloadResponse = $api->downloadFromLocalizableAsset('ziggy', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + + Assert::assertInstanceOf(ResponseInterface::class, $downloadResponse); + Assert::assertSame(file_get_contents($expectedFilePath), $downloadResponse->getBody()->getContents()); + } + + public function test_download_a_not_localizable_asset_reference_file() + { + $expectedFilePath = realpath(__DIR__ . '/../../fixtures/ziggy-certification.jpg'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, 'ziggy_certif', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response(file_get_contents($expectedFilePath), [], 201) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $downloadResponse = $api->downloadFromNotLocalizableAsset('ziggy_certif'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + + $this->assertInstanceOf(ResponseInterface::class, $downloadResponse); + Assert::assertSame(file_get_contents($expectedFilePath), $downloadResponse->getBody()->getContents()); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_download_from_localizable_asset_not_found() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, 'ziggy', 'en_US'), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $api->downloadFromLocalizableAsset('ziggy', 'en_US'); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_download_from_not_localizable_asset_not_found() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_DOWNLOAD_URI, 'ziggy_certif', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + + $api->downloadFromNotLocalizableAsset('ziggy_certif'); + } +} diff --git a/tests/Api/AssetReferenceFile/UploadAssetReferenceFileIntegration.php b/tests/Api/AssetReferenceFile/UploadAssetReferenceFileIntegration.php new file mode 100644 index 00000000..dfc67808 --- /dev/null +++ b/tests/Api/AssetReferenceFile/UploadAssetReferenceFileIntegration.php @@ -0,0 +1,176 @@ +server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, 'ziggy', 'en_US'), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $responseCode = $api->uploadForLocalizableAsset($filePath, 'ziggy', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy.png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 25685); + + Assert::assertSame(201, $responseCode); + + $assetReferenceFile = $api->getFromLocalizableAsset('ziggy', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadLocalizableInformations(), $assetReferenceFile); + } + + public function test_upload_a_not_localizable_asset_reference_file() + { + $filePath = realpath(__DIR__ . '/../../fixtures/ziggy-certification.jpg'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, 'ziggy-certification', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadNotLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $responseCode = $api->uploadForNotLocalizableAsset($filePath, 'ziggy-certification'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy-certification.jpg'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/jpeg'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 10513); + + Assert::assertSame(201, $responseCode); + + $assetReferenceFile = $api->getFromNotLocalizableAsset('ziggy-certification'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadNotLocalizableInformations(), $assetReferenceFile); + } + + public function test_upload_from_resource_file() + { + $filePath = __DIR__ . '/../../fixtures/ziggy.png'; + $file = fopen($filePath, 'rb'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, 'ziggy', 'en_US'), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + $responseCode = $api->uploadForLocalizableAsset($file, 'ziggy', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy.png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 25685); + + Assert::assertSame(201, $responseCode); + + $assetReferenceFile = $api->getFromLocalizableAsset('ziggy', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadLocalizableInformations(), $assetReferenceFile); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_upload_for_an_unknown_asset() + { + $filePath = realpath(__DIR__ . '/../../fixtures/ziggy.png'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, 'unknown_asset', 'en_US'), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + + $api->uploadForLocalizableAsset($filePath, 'unknown_asset', 'en_US'); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\UploadAssetReferenceFileErrorException + */ + public function test_upload_a_file_that_cannot_be_transformed_for_the_variations() + { + $filePath = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetReferenceFileApi::ASSET_REFERENCE_FILE_URI, 'unicorn', AssetReferenceFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response(json_encode($this->generateMessageForUploadAssetReferenceFileErrorException())) + ) + ); + + $api = $this->createClient()->getAssetReferenceFileApi(); + + $api->uploadForNotLocalizableAsset($filePath, 'unicorn'); + } + + protected function fakeUploadLocalizableInformations(){ + return [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_ziggy.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => '/api/rest/v1/assets/ziggy/reference-files/en_US/download' + ] + ], + ]; + } + + protected function fakeUploadNotLocalizableInformations(){ + return [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_ziggy_certification.jpg', + 'locale' => null, + '_link' => [ + 'download' => [ + 'href' => '/api/rest/v1/assets/ziggy_certif/reference-files/no-locale/download' + ] + ], + ]; + } + + protected function generateMessageForUploadAssetReferenceFileErrorException(){ + return [ + 'message' => 'Some variation files were not generated properly.', + 'errors' => [ + [ + 'message' => 'Impossible to "resize" the image "/tmp/pim/file_storage/4/2/5/1/unicorn-en_US-ecommerce.png" with a width bigger than the original.', + 'scope' => 'ecommerce', + 'locale' => null + ] + ] + ]; + } +} diff --git a/tests/Api/AssetVariationFile/DownloadAssetVariationFileApiIntegration.php b/tests/Api/AssetVariationFile/DownloadAssetVariationFileApiIntegration.php new file mode 100644 index 00000000..af67e48a --- /dev/null +++ b/tests/Api/AssetVariationFile/DownloadAssetVariationFileApiIntegration.php @@ -0,0 +1,88 @@ +server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_DOWNLOAD_URI, 'ziggy', 'ecommerce', 'en_US'), + new ResponseStack( + new Response(file_get_contents($expectedFilePath), [], 201) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $downloadResponse = $api->downloadFromLocalizableAsset('ziggy', 'ecommerce', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + + Assert::assertInstanceOf(ResponseInterface::class, $downloadResponse); + Assert::assertSame(file_get_contents($expectedFilePath), $downloadResponse->getBody()->getContents()); + } + + + public function test_download_a_not_localizable_asset_variation_file() + { + $expectedFilePath = realpath(__DIR__ . '/../../fixtures/ziggy-certification.jpg'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_DOWNLOAD_URI, 'ziggy_certif', 'ecommerce', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response(file_get_contents($expectedFilePath), [], 201) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $downloadResponse = $api->downloadFromNotLocalizableAsset('ziggy_certif', 'ecommerce'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + + Assert::assertInstanceOf(ResponseInterface::class, $downloadResponse); + Assert::assertSame(file_get_contents($expectedFilePath), $downloadResponse->getBody()->getContents()); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_download_from_localizable_asset_not_found() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_DOWNLOAD_URI, 'ziggy', 'mobile', 'en_US'), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $api->downloadFromLocalizableAsset('ziggy', 'mobile', 'en_US'); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_download_from_not_localizable_asset_not_found() + { + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_DOWNLOAD_URI, 'ziggy_certif', 'mobile', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $api->downloadFromNotLocalizableAsset('ziggy_certif', 'mobile'); + } +} diff --git a/tests/Api/AssetVariationFile/UploadAssetVariationFileApiIntegration.php b/tests/Api/AssetVariationFile/UploadAssetVariationFileApiIntegration.php new file mode 100644 index 00000000..cb0d9220 --- /dev/null +++ b/tests/Api/AssetVariationFile/UploadAssetVariationFileApiIntegration.php @@ -0,0 +1,164 @@ +server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, 'ziggy', 'ecommerce', 'en_US'), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $responseCode = $api->uploadForLocalizableAsset($filePath, 'ziggy', 'ecommerce', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy.png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 25685); + + Assert::assertSame(201, $responseCode); + + $assetVariationFile = $api->getFromLocalizableAsset('ziggy', 'ecommerce', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadLocalizableInformations(), $assetVariationFile); + } + + + public function test_upload_a_not_localizable_asset_variation_file() + { + $filePath = realpath(__DIR__ . '/../../fixtures/ziggy-certification.jpg'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, 'ziggy_certif', 'ecommerce', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadNotLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $responseCode = $api->uploadForNotLocalizableAsset($filePath, 'ziggy_certif', 'ecommerce'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy-certification.jpg'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/jpeg'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 10513); + + Assert::assertSame(201, $responseCode); + + $assetVariationFile = $api->getFromNotLocalizableAsset('ziggy_certif', 'ecommerce'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadNotLocalizableInformations(), $assetVariationFile); + } + + public function test_upload_from_resource_file() + { + $filePath = __DIR__ . '/../../fixtures/ziggy.png'; + $file = fopen($filePath, 'rb'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, 'ziggy', 'ecommerce', 'en_US'), + new ResponseStack( + new Response(file_get_contents($filePath), [], 201), + new Response(json_encode($this->fakeUploadLocalizableInformations()), [], 201) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + $responseCode = $api->uploadForLocalizableAsset($file, 'ziggy', 'ecommerce', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'POST'); + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], 'ziggy.png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], 'image/png'); + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], 25685); + + Assert::assertSame(201, $responseCode); + + $assetReferenceFile = $api->getFromLocalizableAsset('ziggy', 'ecommerce', 'en_US'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($this->fakeUploadLocalizableInformations(), $assetReferenceFile); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + */ + public function test_upload_for_an_unknown_asset() + { + $filePath = realpath(__DIR__ . '/../../fixtures/ziggy.png'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, 'unknown_asset', 'ecommerce', 'en_US'), + new ResponseStack( + new Response('{"code": 404, "message":"Not found"}', [], 404) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + + $api->uploadForLocalizableAsset($filePath, 'unknown_asset', 'ecommerce', 'en_US'); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException + */ + public function test_upload_for_an_asset_that_should_be_localizable() + { + $filePath = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + + $this->server->setResponseOfPath( + '/'. sprintf(AssetVariationFileApi::ASSET_VARIATION_FILE_URI, 'unicorn', 'ecommerce', AssetVariationFileApi::NOT_LOCALIZABLE_ASSET_LOCALE_CODE), + new ResponseStack( + new Response('{"code": 422, "message":"Unprocessable Entity"}', [], 422) + ) + ); + + $api = $this->createClient()->getAssetVariationFileApi(); + + $api->uploadForNotLocalizableAsset($filePath, 'unicorn', 'ecommerce'); + } + + protected function fakeUploadLocalizableInformations(){ + return [ + 'code' => '5/c/8/3/5c835e7785cb174d8e7e39d7ee63be559f233be0_ziggy_ecommerce.jpg', + 'locale' => 'en_US', + '_link' => [ + 'download' => [ + 'href' => '/api/rest/v1/assets/ziggy/variation-files/ecommerce/en_US/download' + ] + ] + ]; + } + + protected function fakeUploadNotLocalizableInformations(){ + return [ + 'code' => '2/9/b/f/29bfa18ced500c5fca2072dab978737576ca47ca_ziggy_certification_ecommerce.jpg', + 'locale' => null, + '_link' => [ + 'download' => [ + 'href' => '/api/rest/v1/assets/ziggy_certif/variation-files/ecommerce/no-locale/download' + ] + ] + ]; + } +} diff --git a/tests/Api/DownloadProductMediaFileTest.php b/tests/Api/DownloadProductMediaFileTest.php index 9e57fe50..d3ad3e74 100644 --- a/tests/Api/DownloadProductMediaFileTest.php +++ b/tests/Api/DownloadProductMediaFileTest.php @@ -8,7 +8,6 @@ use donatj\MockWebServer\ResponseStack; use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; class DownloadProductMediaFileTest extends ApiTestCase { diff --git a/tests/Api/ReferenceEntity/GetReferenceEntityIntegration.php b/tests/Api/ReferenceEntity/GetReferenceEntityIntegration.php new file mode 100644 index 00000000..66591000 --- /dev/null +++ b/tests/Api/ReferenceEntity/GetReferenceEntityIntegration.php @@ -0,0 +1,65 @@ +server->setResponseOfPath( + '/'. sprintf(ReferenceEntityApi::REFERENCE_ENTITY_URI, 'brand'), + new ResponseStack( + new Response($this->getBrand(), [], 200) + ) + ); + + $api = $this->createClient()->getReferenceEntityApi(); + $product = $api->get('brand'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($product, json_decode($this->getBrand(), true)); + } + + public function test_get_unknown_reference_entity() + { + $this->server->setResponseOfPath( + '/'. sprintf(ReferenceEntityApi::REFERENCE_ENTITY_URI, 'foo'), + new ResponseStack( + new Response('{"code": 404, "message":"Reference entity \"foo\" does not exist."}', [], 404) + ) + ); + + $this->expectException(\Akeneo\Pim\ApiClient\Exception\NotFoundHttpException::class); + $this->expectExceptionMessage('Reference entity "foo" does not exist.'); + + $api = $this->createClient()->getReferenceEntityApi(); + $api->get('foo'); + } + + private function getBrand(): string + { + return <<server->setResponseOfPath( + '/'. sprintf(ReferenceEntityApi::REFERENCE_ENTITIES_URI), + new ResponseStack( + new Response($this->getFirstPage(), [], 200), + new Response($this->getSecondPage(), [], 200) + ) + ); + + $api = $this->createClient()->getReferenceEntityApi(); + $referenceEntityCursor = $api->all(); + $referenceEntities = iterator_to_array($referenceEntityCursor); + + Assert::assertCount(3, $referenceEntities); + } + + private function getFirstPage(): string + { + $baseUri = $this->server->getServerRoot(); + + return <<server->getServerRoot(); + + return <<server->setResponseOfPath( + '/'. sprintf(ReferenceEntityApi::REFERENCE_ENTITY_URI, 'brand'), + new ResponseStack( + new Response('', [], 204) + ) + ); + + $referenceEntity = [ + 'code' => 'brand', + 'labels' => [ + 'en_US' => 'Brand' + ] + ]; + + $api = $this->createClient()->getReferenceEntityApi(); + $response = $api->upsert('brand', $referenceEntity); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($referenceEntity)); + Assert::assertSame(204, $response); + } +} diff --git a/tests/Api/ReferenceEntityMediaFile/CreateReferenceEntityMediaFileIntegration.php b/tests/Api/ReferenceEntityMediaFile/CreateReferenceEntityMediaFileIntegration.php new file mode 100644 index 00000000..93f931c4 --- /dev/null +++ b/tests/Api/ReferenceEntityMediaFile/CreateReferenceEntityMediaFileIntegration.php @@ -0,0 +1,60 @@ +server->setResponseOfPath( + '/' . ReferenceEntityMediaFileApi::MEDIA_FILE_CREATE_URI, + new ResponseStack( + new Response('', ['Reference-entities-media-file-code' => 'my-media-code'], 201) + ) + ); + $mediaFile = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + $response = $this->createClient()->getReferenceEntityMediaFileApi()->create($mediaFile); + + Assert::assertNotEmpty($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['name'], + 'unicorn.png' + ); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['type'], + 'image/png' + ); + Assert::assertSame( + $this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_FILES]['file']['size'], + 11255 + ); + Assert::assertSame('my-media-code', $response); + } + + public function test_get_media_file_code_regardless_of_the_header_case() + { + $this->server->setResponseOfPath( + '/' . ReferenceEntityMediaFileApi::MEDIA_FILE_CREATE_URI, + new ResponseStack( + new Response('', ['Reference-Entities-Media-File-Code' => 'my-media-code'], 201) + ) + ); + $mediaFile = realpath(__DIR__ . '/../../fixtures/unicorn.png'); + $response = $this->createClient()->getReferenceEntityMediaFileApi()->create($mediaFile); + + Assert::assertSame('my-media-code', $response); + } +} diff --git a/tests/Api/ReferenceEntityRecord/GetReferenceEntityRecordIntegration.php b/tests/Api/ReferenceEntityRecord/GetReferenceEntityRecordIntegration.php new file mode 100644 index 00000000..aff2e944 --- /dev/null +++ b/tests/Api/ReferenceEntityRecord/GetReferenceEntityRecordIntegration.php @@ -0,0 +1,66 @@ +server->setResponseOfPath( + '/'. sprintf(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORD_URI, 'designer', 'starck'), + new ResponseStack( + new Response($this->getStarckRecord(), [], 200) + ) + ); + + $api = $this->createClient()->getReferenceEntityRecordApi(); + $product = $api->get('designer', 'starck'); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_METHOD], 'GET'); + Assert::assertEquals($product, json_decode($this->getStarckRecord(), true)); + } + + /** + * @expectedException \Akeneo\Pim\ApiClient\Exception\NotFoundHttpException + * @expectedExceptionMessage Record "foo" does not exist for the reference entity "designer". + */ + public function test_get_unknow_product() + { + $this->server->setResponseOfPath( + '/'. sprintf(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORD_URI, 'designer', 'foo'), + new ResponseStack( + new Response('{"code": 404, "message":"Record \"foo\" does not exist for the reference entity \"designer\"."}', [], 404) + ) + ); + + $api = $this->createClient()->getReferenceEntityRecordApi(); + $api->get('designer', 'foo'); + } + + private function getStarckRecord(): string + { + return <<server->setResponseOfPath( + '/'. sprintf(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORDS_URI, 'designer'), + new ResponseStack( + new Response($this->getFirstPage(), [], 200), + new Response($this->getSecondPage(), [], 200) + ) + ); + + $api = $this->createClient()->getReferenceEntityRecordApi(); + $recordCursor = $api->all('designer'); + $records = iterator_to_array($recordCursor); + + Assert::assertCount(3, $records); + } + + private function getFirstPage(): string + { + $baseUri = $this->server->getServerRoot(); + + return <<server->getServerRoot(); + + return <<server->setResponseOfPath( + '/'. sprintf(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORDS_URI, 'designer'), + new ResponseStack( + new Response($responseBody, [], 200) + ) + ); + + $records = [ + [ + 'code' => 'starck', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'Philippe Starck' + ], + ] + ] + ], + [ + 'code' => 'dyson', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'James Dyson' + ], + ] + ] + ] + ]; + + $expectedResponses = [ + [ + 'code' => 'starck', + 'status_code' =>204 + ], + [ + 'code' => 'dyson', + 'status_code' =>201 + ], + ]; + + $api = $this->createClient()->getReferenceEntityRecordApi(); + $responses = $api->upsertList('designer', $records); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($records)); + Assert::assertSame($expectedResponses, $responses); + } +} diff --git a/tests/Api/ReferenceEntityRecord/UpsertReferenceEntityRecordIntegration.php b/tests/Api/ReferenceEntityRecord/UpsertReferenceEntityRecordIntegration.php new file mode 100644 index 00000000..d6277c06 --- /dev/null +++ b/tests/Api/ReferenceEntityRecord/UpsertReferenceEntityRecordIntegration.php @@ -0,0 +1,42 @@ +server->setResponseOfPath( + '/'. sprintf(ReferenceEntityRecordApi::REFERENCE_ENTITY_RECORD_URI, 'designer', 'starck'), + new ResponseStack( + new Response('', [], 204) + ) + ); + + $recordData = [ + 'code' => 'starck', + 'values' => [ + 'label' => [ + [ + 'channel' => null, + 'locale' => 'en_US', + 'data' => 'Philippe Starck' + ], + ] + ] + ]; + + $api = $this->createClient()->getReferenceEntityRecordApi(); + $response = $api->upsert('designer', 'starck', $recordData); + + Assert::assertSame($this->server->getLastRequest()->jsonSerialize()[RequestInfo::JSON_KEY_INPUT], json_encode($recordData)); + Assert::assertSame(204, $response); + } +} diff --git a/tests/Api/UpsertListProductTest.php b/tests/Api/UpsertListProductTest.php index c17d45d9..2cf6903e 100644 --- a/tests/Api/UpsertListProductTest.php +++ b/tests/Api/UpsertListProductTest.php @@ -7,7 +7,6 @@ use donatj\MockWebServer\Response; use donatj\MockWebServer\ResponseStack; use Http\Discovery\Psr17FactoryDiscovery; -use Http\Discovery\StreamFactoryDiscovery; use PHPUnit\Framework\Assert; class UpsertListProductTest extends ApiTestCase diff --git a/tests/fixtures/akeneo-logo.pdf b/tests/fixtures/akeneo-logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f09613c83e58b40f9440db2c301a0d89881fb292 GIT binary patch literal 21595 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 L+;sR4ft~zcAR?{v literal 0 HcmV?d00001 diff --git a/tests/fixtures/ziggy.png b/tests/fixtures/ziggy.png new file mode 100644 index 0000000000000000000000000000000000000000..8aa27348bd060a679682cf82bed420ade52d2aec GIT binary patch literal 25685 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 M07*qoM6N<$g02T2%K!iX literal 0 HcmV?d00001