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