diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
new file mode 100644
index 0000000..7e0cc7c
--- /dev/null
+++ b/.github/workflows/integration.yml
@@ -0,0 +1,51 @@
+name: Integration tests
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read # to clone the repos and get release assets (shivammathur/setup-php)
+
+jobs:
+ integration:
+ permissions:
+ contents: read # to clone the repos and get release assets (shivammathur/setup-php)
+ name: Integration tests
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: [ '8.1', '8.2' ]
+ fail-fast: false
+ steps:
+ - name: Checkout module code
+ uses: actions/checkout@v3
+ with:
+ path: ps_apiresources
+ - uses: actions/checkout@v3
+ name: Checkout PrestaShop repository
+ with:
+ fetch-depth: 0
+ repository: PrestaShop/PrestaShop
+ path: prestashop
+ - name: Build Docker
+ run: |
+ cd prestashop
+ USER_ID=$(id -u) GROUP_ID=$(id -g) PS_INSTALL_AUTO=0 docker-compose build --no-cache && docker-compose up -d --force-recreate
+ if [ $? -ne 0 ]; then
+ echo "docker install failed"
+ exit
+ fi
+ bash -l -c 'while [[ "$(curl -L -s -o /dev/null -w %{http_code} 'http://localhost:8001/install-dev/')" != "200" ]]; do echo "waiting for shop install"; sleep 5; done'
+ USER_ID=$(id -u) GROUP_ID=$(id -g) docker exec prestashop_prestashop-git_1 php bin/console prestashop:module uninstall ps_apiresources
+ - name: Install Module
+ run: |
+ rm -rf prestashop/modules/ps_apiresources
+ mkdir -p prestashop/modules/ps_apiresources
+ cp -r ps_apiresources/* prestashop/modules/ps_apiresources
+ ls -l prestashop/modules/ps_apiresources
+ USER_ID=$(id -u) GROUP_ID=$(id -g) docker exec prestashop_prestashop-git_1 composer install --no-interaction --working-dir=/var/www/html/modules/ps_apiresources
+ USER_ID=$(id -u) GROUP_ID=$(id -g) docker exec prestashop_prestashop-git_1 php bin/console prestashop:module install ps_apiresources
+ USER_ID=$(id -u) GROUP_ID=$(id -g) docker exec prestashop_prestashop-git_1 composer create-test-db
+ - name: Run integration tests
+ run : |
+ USER_ID=$(id -u) GROUP_ID=$(id -g) docker exec prestashop_prestashop-git_1 vendor/bin/phpunit -c modules/ps_apiresources/tests/Integration/phpunit.xml
diff --git a/composer.json b/composer.json
index d738a81..c2511d0 100644
--- a/composer.json
+++ b/composer.json
@@ -21,6 +21,11 @@
},
"classmap": ["ps_apiresources.php"]
},
+ "autoload-dev": {
+ "psr-4": {
+ "PsApiResourcesTest\\": "tests/"
+ }
+ },
"config": {
"preferred-install": "dist",
"classmap-authoritative": true,
diff --git a/src/ApiPlatform/Resources/ApiAccess.php b/src/ApiPlatform/Resources/ApiAccess.php
index 88575ad..5ee3a4f 100644
--- a/src/ApiPlatform/Resources/ApiAccess.php
+++ b/src/ApiPlatform/Resources/ApiAccess.php
@@ -28,11 +28,18 @@
namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
-use ApiPlatform\Core\Annotation\ApiProperty;
+use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Put;
+use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Command\AddApiAccessCommand;
+use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Command\DeleteApiAccessCommand;
+use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Command\EditApiAccessCommand;
use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Exception\ApiAccessNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Query\GetApiAccessForEditing;
+use PrestaShopBundle\ApiPlatform\Processor\CommandProcessor;
use PrestaShopBundle\ApiPlatform\Provider\QueryProvider;
#[ApiResource(
@@ -60,25 +67,84 @@
],
],
],
- exceptionToStatus: [ApiAccessNotFoundException::class => 404],
provider: QueryProvider::class,
extraProperties: [
'query' => GetApiAccessForEditing::class,
+ 'CQRSQuery' => GetApiAccessForEditing::class,
'scopes' => ['api_access_read'],
]
),
+ new Delete(
+ uriTemplate: '/api-access/{apiAccessId}',
+ requirements: ['apiAccessId' => '\d+'],
+ openapiContext: [
+ 'summary' => 'Delete API Access details',
+ 'description' => 'Delete API Access public details only, sensitive information like secrets is not returned',
+ 'parameters' => [
+ [
+ 'name' => 'apiAccessId',
+ 'in' => 'path',
+ 'required' => true,
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ 'description' => 'Id of the API Access you are deleting',
+ ],
+ [
+ 'name' => 'Authorization',
+ 'in' => 'scopes',
+ 'description' => 'api_access_write',
+ ],
+ ],
+ ],
+ output: false,
+ provider: QueryProvider::class,
+ extraProperties: [
+ 'query' => DeleteApiAccessCommand::class,
+ 'CQRSQuery' => DeleteApiAccessCommand::class,
+ 'scopes' => ['api_access_write'],
+ ]
+ ),
+ new Post(
+ uriTemplate: '/api-access',
+ processor: CommandProcessor::class,
+ extraProperties: [
+ 'command' => AddApiAccessCommand::class,
+ 'CQRSCommand' => AddApiAccessCommand::class,
+ 'scopes' => ['api_access_write'],
+ ]
+ ),
+ new Put(
+ uriTemplate: '/api-access/{apiAccessId}',
+ read: false,
+ processor: CommandProcessor::class,
+ extraProperties: [
+ 'command' => EditApiAccessCommand::class,
+ 'query' => GetApiAccessForEditing::class,
+ 'CQRSCommand' => EditApiAccessCommand::class,
+ 'CQRSQuery' => GetApiAccessForEditing::class,
+ 'scopes' => ['api_access_write'],
+ ]
+ ),
],
+ exceptionToStatus: [ApiAccessNotFoundException::class => 404],
)]
class ApiAccess
{
#[ApiProperty(identifier: true)]
public int $apiAccessId;
- public string $clientName;
+ public string $secret;
- public string $clientId;
+ public string $apiClientId;
+
+ public string $clientName;
public string $description;
public bool $enabled;
+
+ public int $lifetime;
+
+ public array $scopes;
}
diff --git a/src/ApiPlatform/Resources/CartRule.php b/src/ApiPlatform/Resources/CartRule.php
new file mode 100644
index 0000000..ab54b38
--- /dev/null
+++ b/src/ApiPlatform/Resources/CartRule.php
@@ -0,0 +1,76 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Put;
+use PrestaShop\PrestaShop\Core\Domain\CartRule\Command\EditCartRuleCommand;
+use PrestaShopBundle\ApiPlatform\Processor\CommandProcessor;
+
+#[ApiResource(
+ operations: [
+ new Put(
+ uriTemplate: '/cart-rule',
+ processor: CommandProcessor::class,
+ extraProperties: ['CQRSCommand' => EditCartRuleCommand::class]
+ ),
+ ],
+)]
+class CartRule
+{
+ public int $cartRuleId;
+
+ public string $description;
+
+ public string $code;
+
+ public array $minimumAmount;
+
+ public bool $minimumAmountShippingIncluded;
+
+ public int $customerId;
+
+ public array $localizedNames;
+
+ public bool $highlightInCart;
+
+ public bool $allowPartialUse;
+
+ public int $priority;
+
+ public bool $active;
+
+ public array $validityDateRange;
+
+ public int $totalQuantity;
+
+ public int $quantityPerUser;
+
+ public array $cartRuleAction;
+}
diff --git a/src/ApiPlatform/Resources/CustomerGroup.php b/src/ApiPlatform/Resources/CustomerGroup.php
new file mode 100644
index 0000000..2bc33c1
--- /dev/null
+++ b/src/ApiPlatform/Resources/CustomerGroup.php
@@ -0,0 +1,119 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\Command\AddCustomerGroupCommand;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\Command\DeleteCustomerGroupCommand;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\Command\EditCustomerGroupCommand;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\Exception\GroupNotFoundException;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\Query\GetCustomerGroupForEditing;
+use PrestaShop\PrestaShop\Core\Domain\Customer\Group\QueryResult\EditableCustomerGroup;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate;
+
+#[ApiResource(
+ operations: [
+ new CQRSGet(
+ uriTemplate: '/customers/group/{customerGroupId}',
+ CQRSQuery: GetCustomerGroupForEditing::class,
+ scopes: [
+ 'customer_group_read',
+ ],
+ // QueryResult format doesn't match with ApiResource, so we can specify a mapping so that it is normalized with extra fields adapted for the ApiResource DTO
+ CQRSQueryMapping: [
+ // EditableCustomerGroup::$id is normalized as [customerGroupId]
+ '[id]' => '[customerGroupId]',
+ // EditableCustomerGroup::$reduction is normalized as [reductionPercent]
+ '[reduction]' => '[reductionPercent]',
+ ],
+ ),
+ new CQRSCreate(
+ uriTemplate: '/customers/group',
+ CQRSCommand: AddCustomerGroupCommand::class,
+ CQRSQuery: GetCustomerGroupForEditing::class,
+ scopes: [
+ 'customer_group_write',
+ ],
+ // Here, we use query mapping to adapt normalized query result for the ApiPlatform DTO
+ CQRSQueryMapping: [
+ '[id]' => '[customerGroupId]',
+ '[reduction]' => '[reductionPercent]',
+ ],
+ // Here, we use command mapping to adapt the normalized command result for the CQRS query
+ CQRSCommandMapping: [
+ '[groupId]' => '[customerGroupId]',
+ ],
+ ),
+ new CQRSUpdate(
+ uriTemplate: '/customers/group/{customerGroupId}',
+ CQRSCommand: EditCustomerGroupCommand::class,
+ CQRSQuery: GetCustomerGroupForEditing::class,
+ scopes: [
+ 'customer_group_write',
+ ],
+ // Here we use the ApiResource DTO mapping to transform the normalized query result
+ ApiResourceMapping: [
+ '[id]' => '[customerGroupId]',
+ '[reduction]' => '[reductionPercent]',
+ ],
+ ),
+ new CQRSDelete(
+ uriTemplate: '/customers/group/{customerGroupId}',
+ CQRSQuery: DeleteCustomerGroupCommand::class,
+ scopes: [
+ 'customer_group_write',
+ ],
+ // Here, we use query mapping to adapt URI parameters to the expected constructor parameter name
+ CQRSQueryMapping: [
+ '[customerGroupId]' => '[groupId]',
+ ],
+ ),
+ ],
+ exceptionToStatus: [GroupNotFoundException::class => 404],
+)]
+class CustomerGroup
+{
+ #[ApiProperty(identifier: true)]
+ public int $customerGroupId;
+
+ public array $localizedNames;
+
+ public float $reductionPercent;
+
+ public bool $displayPriceTaxExcluded;
+
+ public bool $showPrice;
+
+ public array $shopIds;
+}
diff --git a/src/ApiPlatform/Resources/FoundProduct.php b/src/ApiPlatform/Resources/FoundProduct.php
new file mode 100644
index 0000000..2118975
--- /dev/null
+++ b/src/ApiPlatform/Resources/FoundProduct.php
@@ -0,0 +1,108 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GetCollection;
+use PrestaShop\PrestaShop\Core\Domain\Product\Query\SearchProducts;
+use PrestaShopBundle\ApiPlatform\Provider\QueryProvider;
+
+#[ApiResource(
+ operations: [
+ new GetCollection(
+ uriTemplate: '/products/search/{phrase}/{resultsLimit}/{isoCode}',
+ openapiContext: [
+ 'parameters' => [
+ [
+ 'name' => 'phrase',
+ 'in' => 'path',
+ 'required' => true,
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ ],
+ [
+ 'name' => 'resultsLimit',
+ 'in' => 'path',
+ 'required' => true,
+ 'schema' => [
+ 'type' => 'int',
+ ],
+ ],
+ [
+ 'name' => 'isoCode',
+ 'in' => 'path',
+ 'required' => true,
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ ],
+ [
+ 'name' => 'orderId',
+ 'in' => 'query',
+ 'required' => false,
+ 'schema' => [
+ 'type' => 'int',
+ ],
+ ],
+ ],
+ ],
+ provider: QueryProvider::class,
+ extraProperties: [
+ 'CQRSQuery' => SearchProducts::class,
+ ]
+ ),
+ ],
+)]
+class FoundProduct
+{
+ #[ApiProperty(identifier: true)]
+ public int $productId;
+
+ public bool $availableOutOfStock;
+
+ public string $name;
+
+ public float $taxRate;
+
+ public string $formattedPrice;
+
+ public float $priceTaxIncl;
+
+ public float $priceTaxExcl;
+
+ public int $stock;
+
+ public string $location;
+
+ public array $combinations;
+
+ public array $customizationFields;
+}
diff --git a/src/ApiPlatform/Resources/Hook.php b/src/ApiPlatform/Resources/Hook.php
new file mode 100644
index 0000000..dd4817c
--- /dev/null
+++ b/src/ApiPlatform/Resources/Hook.php
@@ -0,0 +1,107 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\Put;
+use PrestaShop\PrestaShop\Core\Domain\Hook\Command\UpdateHookStatusCommand;
+use PrestaShop\PrestaShop\Core\Domain\Hook\Exception\HookNotFoundException;
+use PrestaShop\PrestaShop\Core\Domain\Hook\Query\GetHook;
+use PrestaShop\PrestaShop\Core\Domain\Hook\Query\GetHookStatus;
+use PrestaShopBundle\ApiPlatform\Processor\CommandProcessor;
+use PrestaShopBundle\ApiPlatform\Provider\QueryProvider;
+
+#[ApiResource(
+ operations: [
+ new Get(
+ uriTemplate: '/hook-status/{id}',
+ requirements: ['id' => '\d+'],
+ openapiContext: [
+ 'summary' => 'Get hook status A',
+ 'description' => 'Get hook status B',
+ 'parameters' => [
+ [
+ 'name' => 'id',
+ 'in' => 'path',
+ 'required' => true,
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ 'description' => 'Id of the hook you are requesting the status from',
+ ],
+ [
+ 'name' => 'Authorization',
+ 'in' => 'scopes',
+ 'description' => 'hook_read
hook_write ',
+ ],
+ ],
+ ],
+ exceptionToStatus: [HookNotFoundException::class => 404],
+ provider: QueryProvider::class,
+ extraProperties: [
+ 'CQRSQuery' => GetHookStatus::class,
+ 'scopes' => ['hook_read'],
+ ]
+ ),
+ new Put(
+ uriTemplate: '/hook-status',
+ processor: CommandProcessor::class,
+ extraProperties: [
+ 'CQRSCommand' => UpdateHookStatusCommand::class,
+ 'scopes' => ['hook_write'],
+ ]
+ ),
+ new Get(
+ uriTemplate: '/hooks/{id}',
+ requirements: ['id' => '\d+'],
+ exceptionToStatus: [HookNotFoundException::class => 404],
+ provider: QueryProvider::class,
+ extraProperties: [
+ 'CQRSQuery' => GetHook::class,
+ 'scopes' => ['hook_read'],
+ ]
+ ),
+ ],
+)]
+class Hook
+{
+ #[ApiProperty(identifier: true)]
+ public int $id;
+
+ public bool $active;
+
+ public string $name;
+
+ public string $title;
+
+ public string $description;
+}
diff --git a/src/ApiPlatform/Resources/Product.php b/src/ApiPlatform/Resources/Product.php
new file mode 100644
index 0000000..743ea83
--- /dev/null
+++ b/src/ApiPlatform/Resources/Product.php
@@ -0,0 +1,115 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PrestaShop\Module\APIResources\ApiPlatform\Resources;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use PrestaShop\PrestaShop\Core\Domain\Product\Command\AddProductCommand;
+use PrestaShop\PrestaShop\Core\Domain\Product\Command\DeleteProductCommand;
+use PrestaShop\PrestaShop\Core\Domain\Product\Command\UpdateProductCommand;
+use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException;
+use PrestaShop\PrestaShop\Core\Domain\Product\Query\GetProductForEditing;
+use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopAssociationNotFound;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet;
+use PrestaShopBundle\ApiPlatform\Metadata\CQRSPartialUpdate;
+use Symfony\Component\HttpFoundation\Response;
+
+#[ApiResource(
+ operations: [
+ new CQRSGet(
+ uriTemplate: '/product/{productId}',
+ CQRSQuery: GetProductForEditing::class,
+ scopes: [
+ 'product_read',
+ ],
+ CQRSQueryMapping: Product::QUERY_MAPPING,
+ ),
+ new CQRSCreate(
+ uriTemplate: '/product',
+ CQRSCommand: AddProductCommand::class,
+ CQRSQuery: GetProductForEditing::class,
+ scopes: [
+ 'product_write',
+ ],
+ CQRSQueryMapping: Product::QUERY_MAPPING,
+ CQRSCommandMapping: [
+ '[type]' => '[productType]',
+ '[names]' => '[localizedNames]',
+ ],
+ ),
+ new CQRSPartialUpdate(
+ uriTemplate: '/product/{productId}',
+ CQRSCommand: UpdateProductCommand::class,
+ CQRSQuery: GetProductForEditing::class,
+ scopes: [
+ 'product_write',
+ ],
+ CQRSQueryMapping: Product::QUERY_MAPPING,
+ CQRSCommandMapping: Product::UPDATE_MAPPING,
+ ),
+ new CQRSDelete(
+ uriTemplate: '/product/{productId}',
+ CQRSQuery: DeleteProductCommand::class,
+ scopes: [
+ 'product_write',
+ ],
+ ),
+ ],
+ exceptionToStatus: [
+ ProductNotFoundException::class => Response::HTTP_NOT_FOUND,
+ ShopAssociationNotFound::class => Response::HTTP_NOT_FOUND,
+ ],
+)]
+class Product
+{
+ #[ApiProperty(identifier: true)]
+ public int $productId;
+
+ public string $type;
+
+ public bool $active;
+
+ public array $names;
+
+ public array $descriptions;
+
+ public const QUERY_MAPPING = [
+ '[langId]' => '[displayLanguageId]',
+ '[basicInformation][localizedNames]' => '[names]',
+ '[basicInformation][localizedDescriptions]' => '[descriptions]',
+ ];
+
+ public const UPDATE_MAPPING = [
+ '[type]' => '[productType]',
+ '[names]' => '[localizedNames]',
+ '[descriptions]' => '[localizedDescriptions]',
+ ];
+}
diff --git a/tests/Integration/ApiPlatform/ApiTestCase.php b/tests/Integration/ApiPlatform/ApiTestCase.php
new file mode 100644
index 0000000..b57343b
--- /dev/null
+++ b/tests/Integration/ApiPlatform/ApiTestCase.php
@@ -0,0 +1,193 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase as SymfonyApiTestCase;
+use ApiPlatform\Symfony\Bundle\Test\Client;
+use PrestaShop\PrestaShop\Core\Domain\ApiAccess\Command\AddApiAccessCommand;
+use PrestaShop\PrestaShop\Core\Domain\Configuration\ShopConfigurationInterface;
+use PrestaShop\PrestaShop\Core\Domain\Language\Command\AddLanguageCommand;
+use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
+use Tests\Resources\DatabaseDump;
+
+abstract class ApiTestCase extends SymfonyApiTestCase
+{
+ protected const CLIENT_ID = 'test_client_id';
+ protected const CLIENT_NAME = 'test_client_name';
+
+ protected static ?string $clientSecret = null;
+
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ DatabaseDump::restoreTables(['api_access']);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ DatabaseDump::restoreTables(['api_access']);
+ self::$clientSecret = null;
+ }
+
+ protected static function createClient(array $kernelOptions = [], array $defaultOptions = []): Client
+ {
+ if (!isset($defaultOptions['headers']['accept'])) {
+ $defaultOptions['headers']['accept'] = ['application/json'];
+ }
+
+ if (!isset($defaultOptions['headers']['content-type'])) {
+ $defaultOptions['headers']['content-type'] = ['application/json'];
+ }
+
+ return parent::createClient($kernelOptions, $defaultOptions);
+ }
+
+ protected function getBearerToken(array $scopes = []): string
+ {
+ if (null === self::$clientSecret) {
+ self::createApiAccess($scopes);
+ }
+ $client = static::createClient();
+ $parameters = ['parameters' => [
+ 'client_id' => static::CLIENT_ID,
+ 'client_secret' => static::$clientSecret,
+ 'grant_type' => 'client_credentials',
+ 'scope' => $scopes,
+ ]];
+ $options = [
+ 'extra' => $parameters,
+ 'headers' => [
+ 'content-type' => 'application/x-www-form-urlencoded',
+ ],
+ ];
+ $response = $client->request('POST', '/api/oauth2/token', $options);
+
+ return json_decode($response->getContent())->access_token;
+ }
+
+ protected static function createApiAccess(array $scopes = [], int $lifetime = 10000): void
+ {
+ $client = static::createClient();
+ $command = new AddApiAccessCommand(
+ static::CLIENT_NAME,
+ static::CLIENT_ID,
+ true,
+ '',
+ $lifetime,
+ $scopes
+ );
+
+ $container = $client->getContainer();
+ $commandBus = $container->get('prestashop.core.command_bus');
+ $createdApiAccess = $commandBus->handle($command);
+
+ self::$clientSecret = $createdApiAccess->getSecret();
+ }
+
+ protected static function addLanguageByLocale(string $locale): int
+ {
+ $client = static::createClient();
+ $isoCode = substr($locale, 0, strpos($locale, '-'));
+
+ // Copy resource assets into tmp folder to mimic an upload file path
+ $flagImage = __DIR__ . '/../../Resources/assets/lang/' . $isoCode . '.jpg';
+ if (!file_exists($flagImage)) {
+ $flagImage = __DIR__ . '/../../Resources/assets/lang/en.jpg';
+ }
+
+ $tmpFlagImage = sys_get_temp_dir() . '/' . $isoCode . '.jpg';
+ $tmpNoPictureImage = sys_get_temp_dir() . '/' . $isoCode . '-no-picture.jpg';
+ copy($flagImage, $tmpFlagImage);
+ copy($flagImage, $tmpNoPictureImage);
+
+ $command = new AddLanguageCommand(
+ $locale,
+ $isoCode,
+ $locale,
+ 'd/m/Y',
+ 'd/m/Y H:i:s',
+ $tmpFlagImage,
+ $tmpNoPictureImage,
+ false,
+ true,
+ [1]
+ );
+
+ $container = $client->getContainer();
+ $commandBus = $container->get('prestashop.core.command_bus');
+
+ return $commandBus->handle($command)->getValue();
+ }
+
+ protected static function addShopGroup(string $groupName, string $color = null): int
+ {
+ $shopGroup = new \ShopGroup();
+ $shopGroup->name = $groupName;
+ $shopGroup->active = true;
+
+ if ($color !== null) {
+ $shopGroup->color = $color;
+ }
+
+ if (!$shopGroup->add()) {
+ throw new \RuntimeException('Could not create shop group');
+ }
+
+ return (int) $shopGroup->id;
+ }
+
+ protected static function addShop(string $shopName, int $shopGroupId, string $color = null): int
+ {
+ $shop = new \Shop();
+ $shop->active = true;
+ $shop->id_shop_group = $shopGroupId;
+ // 2 : ID Category for "Home" in database
+ $shop->id_category = 2;
+ $shop->theme_name = _THEME_NAME_;
+ $shop->name = $shopName;
+ if ($color !== null) {
+ $shop->color = $color;
+ }
+
+ if (!$shop->add()) {
+ throw new \RuntimeException('Could not create shop');
+ }
+ $shop->setTheme();
+ \Shop::resetContext();
+ \Shop::resetStaticCache();
+
+ return (int) $shop->id;
+ }
+
+ protected static function updateConfiguration(string $configurationKey, $value, ShopConstraint $shopConstraint = null): void
+ {
+ self::getContainer()->get(ShopConfigurationInterface::class)->set($configurationKey, $value, $shopConstraint ?: ShopConstraint::allShops());
+ }
+}
diff --git a/tests/Integration/ApiPlatform/CustomerGroupApiTest.php b/tests/Integration/ApiPlatform/CustomerGroupApiTest.php
new file mode 100644
index 0000000..0be11f7
--- /dev/null
+++ b/tests/Integration/ApiPlatform/CustomerGroupApiTest.php
@@ -0,0 +1,239 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use Group;
+use Tests\Resources\DatabaseDump;
+
+class CustomerGroupApiTest extends ApiTestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ DatabaseDump::restoreTables(['group', 'group_lang', 'group_reduction', 'group_shop', 'category_group']);
+ self::createApiAccess(['customer_group_write', 'customer_group_read']);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ DatabaseDump::restoreTables(['group', 'group_lang', 'group_reduction', 'group_shop', 'category_group']);
+ }
+
+ /**
+ * @dataProvider getProtectedEndpoints
+ *
+ * @param string $method
+ * @param string $uri
+ */
+ public function testProtectedEndpoints(string $method, string $uri): void
+ {
+ $client = static::createClient();
+ $response = $client->request($method, $uri);
+ self::assertResponseStatusCodeSame(401);
+
+ $content = $response->getContent(false);
+ $this->assertNotEmpty($content);
+ $decodedContent = json_decode($content, true);
+ $this->assertArrayHasKey('title', $decodedContent);
+ $this->assertArrayHasKey('detail', $decodedContent);
+ $this->assertStringContainsString('An error occurred', $decodedContent['title']);
+ $this->assertStringContainsString('Full authentication is required to access this resource.', $decodedContent['detail']);
+ }
+
+ public function getProtectedEndpoints(): iterable
+ {
+ yield 'get endpoint' => [
+ 'GET',
+ '/api/customers/group/1',
+ ];
+
+ yield 'create endpoint' => [
+ 'POST',
+ '/api/customers/group',
+ ];
+
+ yield 'update endpoint' => [
+ 'PUT',
+ '/api/customers/group/1',
+ ];
+ }
+
+ public function testAddCustomerGroup(): int
+ {
+ $numberOfGroups = count(\Group::getGroups(\Context::getContext()->language->id));
+
+ $bearerToken = $this->getBearerToken(['customer_group_write']);
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/customers/group', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => [
+ 'localizedNames' => [
+ 1 => 'test1',
+ ],
+ 'reductionPercent' => 10.3,
+ 'displayPriceTaxExcluded' => true,
+ 'showPrice' => true,
+ 'shopIds' => [1],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(201);
+ self::assertCount($numberOfGroups + 1, \Group::getGroups(\Context::getContext()->language->id));
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ $this->assertArrayHasKey('customerGroupId', $decodedResponse);
+ $customerGroupId = $decodedResponse['customerGroupId'];
+ $this->assertEquals(
+ [
+ 'customerGroupId' => $customerGroupId,
+ 'localizedNames' => [
+ 1 => 'test1',
+ ],
+ 'reductionPercent' => 10.3,
+ 'displayPriceTaxExcluded' => true,
+ 'showPrice' => true,
+ 'shopIds' => [1],
+ ],
+ $decodedResponse
+ );
+
+ return $customerGroupId;
+ }
+
+ /**
+ * @depends testAddCustomerGroup
+ *
+ * @param int $customerGroupId
+ *
+ * @return int
+ */
+ public function testUpdateCustomerGroup(int $customerGroupId): int
+ {
+ $numberOfGroups = count(\Group::getGroups(\Context::getContext()->language->id));
+
+ $bearerToken = $this->getBearerToken(['customer_group_write']);
+ $client = static::createClient();
+ // Update customer group with partial data
+ $response = $client->request('PUT', '/api/customers/group/' . $customerGroupId, [
+ 'auth_bearer' => $bearerToken,
+ 'json' => [
+ 'localizedNames' => [
+ 1 => 'new_test1',
+ ],
+ 'displayPriceTaxExcluded' => false,
+ 'shopIds' => [1],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+ // No new group
+ self::assertCount($numberOfGroups, \Group::getGroups(\Context::getContext()->language->id));
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ // Returned data has modified fields, the others haven't changed
+ $this->assertEquals(
+ [
+ 'customerGroupId' => $customerGroupId,
+ 'localizedNames' => [
+ 1 => 'new_test1',
+ ],
+ 'reductionPercent' => 10.3,
+ 'displayPriceTaxExcluded' => false,
+ 'showPrice' => true,
+ 'shopIds' => [1],
+ ],
+ $decodedResponse
+ );
+
+ return $customerGroupId;
+ }
+
+ /**
+ * @depends testUpdateCustomerGroup
+ *
+ * @param int $customerGroupId
+ *
+ * @return int
+ */
+ public function testGetCustomerGroup(int $customerGroupId): int
+ {
+ $bearerToken = $this->getBearerToken(['customer_group_read']);
+ $client = static::createClient();
+ $response = $client->request('GET', '/api/customers/group/' . $customerGroupId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(200);
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ // Returned data has modified fields, the others haven't changed
+ $this->assertEquals(
+ [
+ 'customerGroupId' => $customerGroupId,
+ 'localizedNames' => [
+ 1 => 'new_test1',
+ ],
+ 'reductionPercent' => 10.3,
+ 'displayPriceTaxExcluded' => false,
+ 'showPrice' => true,
+ 'shopIds' => [1],
+ ],
+ $decodedResponse
+ );
+
+ return $customerGroupId;
+ }
+
+ /**
+ * @depends testGetCustomerGroup
+ *
+ * @param int $customerGroupId
+ *
+ * @return void
+ */
+ public function testDeleteCustomerGroup(int $customerGroupId): void
+ {
+ $bearerToken = $this->getBearerToken(['customer_group_read', 'customer_group_write']);
+ $client = static::createClient();
+ // Update customer group with partial data
+ $response = $client->request('DELETE', '/api/customers/group/' . $customerGroupId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(204);
+ $this->assertEmpty($response->getContent());
+
+ $client = static::createClient();
+ $client->request('GET', '/api/customers/group/' . $customerGroupId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(404);
+ }
+}
diff --git a/tests/Integration/ApiPlatform/GetHookStatusTest.php b/tests/Integration/ApiPlatform/GetHookStatusTest.php
new file mode 100644
index 0000000..3f53338
--- /dev/null
+++ b/tests/Integration/ApiPlatform/GetHookStatusTest.php
@@ -0,0 +1,124 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use Tests\Resources\DatabaseDump;
+
+class GetHookStatusTest extends ApiTestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ DatabaseDump::restoreTables(['hook']);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ DatabaseDump::restoreTables(['hook']);
+ }
+
+ public function testGetHookStatus(): void
+ {
+ $inactiveHook = new \Hook();
+ $inactiveHook->name = 'inactiveHook';
+ $inactiveHook->active = false;
+ $inactiveHook->add();
+
+ $activeHook = new \Hook();
+ $activeHook->name = 'activeHook';
+ $activeHook->active = true;
+ $activeHook->add();
+
+ $bearerToken = $this->getBearerToken([
+ 'hook_read',
+ 'hook_write',
+ ]);
+ $response = static::createClient()->request('GET', '/api/hook-status/' . (int) $inactiveHook->id, ['auth_bearer' => $bearerToken]);
+ self::assertEquals(json_decode($response->getContent())->active, $inactiveHook->active);
+ self::assertResponseStatusCodeSame(200);
+
+ $response = static::createClient()->request('GET', '/api/hook-status/' . (int) $activeHook->id, ['auth_bearer' => $bearerToken]);
+ self::assertEquals(json_decode($response->getContent())->active, $activeHook->active);
+ self::assertResponseStatusCodeSame(200);
+
+ static::createClient()->request('GET', '/api/hook-status/' . 9999, ['auth_bearer' => $bearerToken]);
+ self::assertResponseStatusCodeSame(404);
+
+ static::createClient()->request('GET', '/api/hook-status/' . $activeHook->id);
+ self::assertResponseStatusCodeSame(401);
+
+ $inactiveHook->delete();
+ $activeHook->delete();
+ }
+
+ public function testDisableHook(): void
+ {
+ $hook = new \Hook();
+ $hook->name = 'disableHook';
+ $hook->active = true;
+ $hook->add();
+
+ $bearerToken = $this->getBearerToken([
+ 'hook_read',
+ 'hook_write',
+ ]);
+ static::createClient()->request('PUT', '/api/hook-status', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => ['id' => (int) $hook->id, 'active' => false],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+
+ $response = static::createClient()->request('GET', '/api/hook-status/' . (int) $hook->id, ['auth_bearer' => $bearerToken]);
+ self::assertEquals(json_decode($response->getContent())->active, false);
+ self::assertResponseStatusCodeSame(200);
+ }
+
+ public function testEnableHook(): void
+ {
+ $hook = new \Hook();
+ $hook->name = 'enableHook';
+ $hook->active = false;
+ $hook->add();
+
+ $bearerToken = $this->getBearerToken([
+ 'hook_read',
+ 'hook_write',
+ ]);
+ static::createClient()->request('PUT', '/api/hook-status', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => ['id' => (int) $hook->id, 'active' => true],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+
+ $response = static::createClient()->request('GET', '/api/hook-status/' . (int) $hook->id, ['auth_bearer' => $bearerToken]);
+ self::assertEquals(json_decode($response->getContent())->active, true);
+ self::assertResponseStatusCodeSame(200);
+ }
+}
diff --git a/tests/Integration/ApiPlatform/GetHookTest.php b/tests/Integration/ApiPlatform/GetHookTest.php
new file mode 100644
index 0000000..550302b
--- /dev/null
+++ b/tests/Integration/ApiPlatform/GetHookTest.php
@@ -0,0 +1,72 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use Tests\Resources\DatabaseDump;
+
+class GetHookTest extends ApiTestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ DatabaseDump::restoreTables(['hook']);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ DatabaseDump::restoreTables(['hook']);
+ }
+
+ public function testGetHook(): void
+ {
+ $hook = new \Hook();
+ $hook->name = 'testHook';
+ $hook->active = true;
+ $hook->add();
+
+ $bearerToken = $this->getBearerToken([
+ 'hook_read',
+ 'hook_write',
+ ]);
+
+ $response = static::createClient()->request('GET', '/api/hooks/' . (int) $hook->id, ['auth_bearer' => $bearerToken]);
+ self::assertEquals(json_decode($response->getContent())->active, $hook->active);
+ self::assertResponseStatusCodeSame(200);
+
+ static::createClient()->request('GET', '/api/hooks/' . 9999, ['auth_bearer' => $bearerToken]);
+ self::assertResponseStatusCodeSame(404);
+
+ static::createClient()->request('GET', '/api/hooks/' . $hook->id);
+ self::assertResponseStatusCodeSame(401);
+
+ $hook->delete();
+ }
+}
diff --git a/tests/Integration/ApiPlatform/ProductEndpointTest.php b/tests/Integration/ApiPlatform/ProductEndpointTest.php
new file mode 100644
index 0000000..6b6d5e5
--- /dev/null
+++ b/tests/Integration/ApiPlatform/ProductEndpointTest.php
@@ -0,0 +1,310 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType;
+use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
+use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\ProductGridDefinitionFactory;
+use PrestaShop\PrestaShop\Core\Grid\Query\ProductQueryBuilder;
+use PrestaShop\PrestaShop\Core\Search\Filters\ProductFilters;
+use Tests\Resources\Resetter\LanguageResetter;
+use Tests\Resources\Resetter\ProductResetter;
+use Tests\Resources\ResourceResetter;
+
+class ProductEndpointTest extends ApiTestCase
+{
+ protected const EN_LANG_ID = 1;
+ protected static int $frenchLangId;
+
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ (new ResourceResetter())->backupTestModules();
+ ProductResetter::resetProducts();
+ LanguageResetter::resetLanguages();
+ self::$frenchLangId = self::addLanguageByLocale('fr-FR');
+ self::createApiAccess(['product_write', 'product_read']);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ ProductResetter::resetProducts();
+ LanguageResetter::resetLanguages();
+ // Reset modules folder that are removed with the FR language
+ (new ResourceResetter())->resetTestModules();
+ }
+
+ /**
+ * @dataProvider getProtectedEndpoints
+ *
+ * @param string $method
+ * @param string $uri
+ */
+ public function testProtectedEndpoints(string $method, string $uri, string $contentType = 'application/json'): void
+ {
+ $options['headers']['content-type'] = $contentType;
+ // Check that endpoints are not accessible without a proper Bearer token
+ $client = static::createClient([], $options);
+ $response = $client->request($method, $uri);
+ self::assertResponseStatusCodeSame(401);
+
+ $content = $response->getContent(false);
+ $this->assertNotEmpty($content);
+ $decodedContent = json_decode($content, true);
+ $this->assertArrayHasKey('title', $decodedContent);
+ $this->assertArrayHasKey('detail', $decodedContent);
+ $this->assertStringContainsString('An error occurred', $decodedContent['title']);
+ $this->assertStringContainsString('Full authentication is required to access this resource.', $decodedContent['detail']);
+ }
+
+ public function getProtectedEndpoints(): iterable
+ {
+ yield 'get endpoint' => [
+ 'GET',
+ '/api/product/1',
+ ];
+
+ yield 'create endpoint' => [
+ 'POST',
+ '/api/product',
+ ];
+
+ yield 'update endpoint' => [
+ 'PATCH',
+ '/api/product/1',
+ 'application/merge-patch+json',
+ ];
+ }
+
+ public function testAddProduct(): int
+ {
+ $productsNumber = $this->getProductsNumber();
+ $bearerToken = $this->getBearerToken(['product_write']);
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/product', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nom produit',
+ ],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(201);
+ $newProductsNumber = $this->getProductsNumber();
+ self::assertEquals($productsNumber + 1, $newProductsNumber);
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ $this->assertArrayHasKey('productId', $decodedResponse);
+ $productId = $decodedResponse['productId'];
+ $this->assertEquals(
+ [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'productId' => $productId,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nom produit',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => '',
+ self::$frenchLangId => '',
+ ],
+ 'active' => false,
+ ],
+ $decodedResponse
+ );
+
+ return $productId;
+ }
+
+ /**
+ * @depends testAddProduct
+ *
+ * @param int $productId
+ *
+ * @return int
+ */
+ public function testPartialUpdateProduct(int $productId): int
+ {
+ $productsNumber = $this->getProductsNumber();
+ $bearerToken = $this->getBearerToken(['product_write']);
+ $client = static::createClient();
+
+ // Update product with partial data, even multilang fields can be updated language by language
+ $response = $client->request('PATCH', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ 'headers' => [
+ 'content-type' => 'application/merge-patch+json',
+ ],
+ 'json' => [
+ 'names' => [
+ self::$frenchLangId => 'nouveau nom',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => 'new description',
+ ],
+ 'active' => true,
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+ // No new product
+ $this->assertEquals($productsNumber, $this->getProductsNumber());
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ // Returned data has modified fields, the others haven't changed
+ $this->assertEquals(
+ [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'productId' => $productId,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nouveau nom',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => 'new description',
+ self::$frenchLangId => '',
+ ],
+ 'active' => true,
+ ],
+ $decodedResponse
+ );
+
+ // Update product with partial data, only name default language the other names are not impacted
+ $response = $client->request('PATCH', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ 'headers' => [
+ 'content-type' => 'application/merge-patch+json',
+ ],
+ 'json' => [
+ 'names' => [
+ self::EN_LANG_ID => 'new product name',
+ ],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ // Returned data has modified fields, the others haven't changed
+ $this->assertEquals(
+ [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'productId' => $productId,
+ 'names' => [
+ self::EN_LANG_ID => 'new product name',
+ self::$frenchLangId => 'nouveau nom',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => 'new description',
+ self::$frenchLangId => '',
+ ],
+ 'active' => true,
+ ],
+ $decodedResponse
+ );
+
+ return $productId;
+ }
+
+ /**
+ * @depends testPartialUpdateProduct
+ *
+ * @param int $productId
+ */
+ public function testGetProduct(int $productId): int
+ {
+ $bearerToken = $this->getBearerToken(['product_read']);
+ $client = static::createClient();
+ $response = $client->request('GET', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(200);
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ // Returned data has modified fields, the others haven't changed
+ $this->assertEquals(
+ [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'productId' => $productId,
+ 'names' => [
+ self::EN_LANG_ID => 'new product name',
+ self::$frenchLangId => 'nouveau nom',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => 'new description',
+ self::$frenchLangId => '',
+ ],
+ 'active' => true,
+ ],
+ $decodedResponse
+ );
+
+ return $productId;
+ }
+
+ /**
+ * @depends testGetProduct
+ *
+ * @param int $productId
+ */
+ public function testDeleteProduct(int $productId): void
+ {
+ $productsNumber = $this->getProductsNumber();
+ $bearerToken = $this->getBearerToken(['product_read', 'product_write']);
+ $client = static::createClient();
+ // Update customer group with partial data
+ $response = $client->request('DELETE', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(204);
+ $this->assertEmpty($response->getContent());
+
+ // One less products
+ $this->assertEquals($productsNumber - 1, $this->getProductsNumber());
+
+ $client = static::createClient();
+ $client->request('GET', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ ]);
+ self::assertResponseStatusCodeSame(404);
+ }
+
+ protected function getProductsNumber(): int
+ {
+ /** @var ProductQueryBuilder $productQueryBuilder */
+ $productQueryBuilder = $this->getContainer()->get('prestashop.core.grid.query_builder.product');
+ $queryBuilder = $productQueryBuilder->getCountQueryBuilder(new ProductFilters(ShopConstraint::allShops(), ProductFilters::getDefaults(), ProductGridDefinitionFactory::GRID_ID));
+
+ return (int) $queryBuilder->executeQuery()->fetchOne();
+ }
+}
diff --git a/tests/Integration/ApiPlatform/ProductMultiShopEndpointTest.php b/tests/Integration/ApiPlatform/ProductMultiShopEndpointTest.php
new file mode 100644
index 0000000..41501b6
--- /dev/null
+++ b/tests/Integration/ApiPlatform/ProductMultiShopEndpointTest.php
@@ -0,0 +1,217 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+
+declare(strict_types=1);
+
+namespace PsApiResourcesTest\Integration\ApiPlatform;
+
+use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType;
+use PrestaShop\PrestaShop\Core\Multistore\MultistoreConfig;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Tests\Resources\Resetter\ConfigurationResetter;
+use Tests\Resources\Resetter\LanguageResetter;
+use Tests\Resources\Resetter\ProductResetter;
+use Tests\Resources\Resetter\ShopResetter;
+use Tests\Resources\ResourceResetter;
+
+class ProductMultiShopEndpointTest extends ApiTestCase
+{
+ protected const EN_LANG_ID = 1;
+ protected static int $frenchLangId;
+
+ protected const DEFAULT_SHOP_GROUP_ID = 1;
+ protected static int $secondShopGroupId;
+
+ protected const DEFAULT_SHOP_ID = 1;
+ protected static int $secondShopId;
+ protected static int $thirdShopId;
+ protected static int $fourthShopId;
+
+ protected static array $defaultProductData;
+
+ public static function setUpBeforeClass(): void
+ {
+ parent::setUpBeforeClass();
+ (new ResourceResetter())->backupTestModules();
+ ProductResetter::resetProducts();
+ LanguageResetter::resetLanguages();
+ ShopResetter::resetShops();
+ ConfigurationResetter::resetConfiguration();
+
+ self::$frenchLangId = self::addLanguageByLocale('fr-FR');
+
+ self::updateConfiguration(MultistoreConfig::FEATURE_STATUS, 1);
+ self::$secondShopGroupId = self::addShopGroup('Second group');
+ self::$secondShopId = self::addShop('Second shop', self::DEFAULT_SHOP_GROUP_ID);
+ self::$thirdShopId = self::addShop('Third shop', self::$secondShopGroupId);
+ self::$fourthShopId = self::addShop('Fourth shop', self::$secondShopGroupId);
+ self::createApiAccess(['product_write', 'product_read']);
+
+ self::$defaultProductData = [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nom produit',
+ ],
+ 'descriptions' => [
+ self::EN_LANG_ID => '',
+ self::$frenchLangId => '',
+ ],
+ 'active' => false,
+ ];
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ parent::tearDownAfterClass();
+ ProductResetter::resetProducts();
+ LanguageResetter::resetLanguages();
+ ShopResetter::resetShops();
+ ConfigurationResetter::resetConfiguration();
+ // Reset modules folder that are removed with the FR language
+ (new ResourceResetter())->resetTestModules();
+ }
+
+ public function testShopContextIsRequired(): void
+ {
+ $bearerToken = $this->getBearerToken(['product_write']);
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/product', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nom produit',
+ ],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
+ $content = $response->getContent(false);
+ $this->assertStringContainsString('Multi shop is enabled, you must specify a shop context', $content);
+ }
+
+ public function testCreateProductForFirstShop(): int
+ {
+ $bearerToken = $this->getBearerToken(['product_write']);
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/product', [
+ 'auth_bearer' => $bearerToken,
+ 'json' => [
+ 'type' => ProductType::TYPE_STANDARD,
+ 'names' => [
+ self::EN_LANG_ID => 'product name',
+ self::$frenchLangId => 'nom produit',
+ ],
+ ],
+ 'extra' => [
+ 'parameters' => [
+ 'shopId' => self::DEFAULT_SHOP_ID,
+ ],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(201);
+
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ $this->assertArrayHasKey('productId', $decodedResponse);
+ $productId = $decodedResponse['productId'];
+ $this->assertProductData($productId, self::$defaultProductData, $response);
+
+ return $productId;
+ }
+
+ /**
+ * @depends testCreateProductForFirstShop
+ *
+ * @param int $productId
+ *
+ * @return int
+ */
+ public function testGetProductForFirstShopIsSuccessful(int $productId): int
+ {
+ $bearerToken = $this->getBearerToken(['product_read']);
+ $client = static::createClient();
+ $response = $client->request('GET', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ 'extra' => [
+ 'parameters' => [
+ 'shopId' => self::DEFAULT_SHOP_ID,
+ ],
+ ],
+ ]);
+ self::assertResponseStatusCodeSame(200);
+ $this->assertProductData($productId, self::$defaultProductData, $response);
+
+ return $productId;
+ }
+
+ /**
+ * @depends testGetProductForFirstShopIsSuccessful
+ *
+ * @param int $productId
+ *
+ * @return int
+ */
+ public function testGetProductForSecondShopIsFailing(int $productId): int
+ {
+ $bearerToken = $this->getBearerToken(['product_read']);
+ $client = static::createClient();
+ $response = $client->request('GET', '/api/product/' . $productId, [
+ 'auth_bearer' => $bearerToken,
+ 'extra' => [
+ 'parameters' => [
+ 'shopId' => self::$secondShopId,
+ ],
+ ],
+ ]);
+
+ self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
+ $content = $response->getContent(false);
+ $this->assertStringContainsString(sprintf(
+ 'Could not find association between Product %d and Shop %d',
+ $productId,
+ self::$secondShopId
+ ), $content);
+
+ return $productId;
+ }
+
+ protected function assertProductData(int $productId, array $expectedData, ResponseInterface $response): void
+ {
+ // Merge expected data with default one, this way no need to always specify all the fields
+ $checkedData = array_merge(self::$defaultProductData, ['productId' => $productId], $expectedData);
+ $decodedResponse = json_decode($response->getContent(), true);
+ $this->assertNotFalse($decodedResponse);
+ $this->assertNotFalse($decodedResponse);
+ $this->assertArrayHasKey('productId', $decodedResponse);
+ $this->assertEquals(
+ $decodedResponse,
+ $checkedData
+ );
+ }
+}
diff --git a/tests/Integration/bootstrap.php b/tests/Integration/bootstrap.php
new file mode 100644
index 0000000..7045d3c
--- /dev/null
+++ b/tests/Integration/bootstrap.php
@@ -0,0 +1,42 @@
+
+ * @copyright Since 2007 PrestaShop SA and Contributors
+ * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ */
+define('_PS_IN_TEST_', true);
+define('_PS_API_FORCE_TLS_VERSION_', false);
+define('_PS_ROOT_DIR_', dirname(__DIR__, 4));
+define('_PS_MODULE_DIR_', _PS_ROOT_DIR_ . '/tests/Resources/modules/');
+require_once dirname(__DIR__, 4) . '/vendor/smarty/smarty/libs/functions.php';
+require_once dirname(__DIR__, 4) . '/admin-dev/bootstrap.php';
+
+/*
+ * Following code makes tests run under phpstorm
+ * Else we get error : Class 'PHPUnit_Util_Configuration' not found
+ * @see https://stackoverflow.com/questions/33299149/phpstorm-8-and-phpunit-problems-with-runinseparateprocess
+ */
+if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
+ define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__, 4) . '/vendor/autoload.php');
+}
+
+require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
diff --git a/tests/Integration/phpunit.xml b/tests/Integration/phpunit.xml
new file mode 100644
index 0000000..5611864
--- /dev/null
+++ b/tests/Integration/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ .
+
+
+
+
+
+ isolatedProcess
+
+
+
diff --git a/tests/Resources/assets/lang/en.jpg b/tests/Resources/assets/lang/en.jpg
new file mode 100644
index 0000000..b924ed7
Binary files /dev/null and b/tests/Resources/assets/lang/en.jpg differ
diff --git a/tests/Resources/assets/lang/fr.jpg b/tests/Resources/assets/lang/fr.jpg
new file mode 100644
index 0000000..0b80c64
Binary files /dev/null and b/tests/Resources/assets/lang/fr.jpg differ