diff --git a/config/services.xml b/config/services.xml index 7b4dfb89..42309ff6 100644 --- a/config/services.xml +++ b/config/services.xml @@ -61,6 +61,16 @@ + + + + + %webgriffe_sylius_akeneo.webhook.secret% + + + + + diff --git a/config/webhook_routing.yaml b/config/webhook_routing.yaml new file mode 100644 index 00000000..c7cb0785 --- /dev/null +++ b/config/webhook_routing.yaml @@ -0,0 +1,4 @@ +webgriffe_sylius_akeneo_webhook: + path: /akeneo/webhook + methods: [POST] + controller: webgriffe_sylius_akeneo.controller.webhook::postAction diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index b66a578b..40418260 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -67,6 +67,8 @@ GEM sass-embedded (1.58.3) google-protobuf (~> 3.21) rake (>= 10.0.0) + sass-embedded (1.58.3-x86_64-darwin) + google-protobuf (~> 3.21) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) unicode-display_width (2.4.2) @@ -74,6 +76,7 @@ GEM PLATFORMS x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -81,4 +84,4 @@ DEPENDENCIES just-the-docs BUNDLED WITH - 2.4.19 + 2.4.22 diff --git a/docs/architecture_and_customization.md b/docs/architecture_and_customization.md index 5766f3e4..899e7593 100644 --- a/docs/architecture_and_customization.md +++ b/docs/architecture_and_customization.md @@ -1,7 +1,7 @@ --- title: Architecture & customization layout: page -nav_order: 5 +nav_order: 6 --- # Architecture & customization @@ -9,12 +9,13 @@ nav_order: 5 > This plugin makes use of [Symfony Messenger](https://symfony.com/doc/current/messenger.html) component. It is highly > recommended to have a minimum knowledge of these component to understand how this integration works. -This plugin has basically two entry points: +This plugin has basically three entry points: * The UI admin import button, this will import only products * The Import CLI command, this will import both product, product associations and attribute options +* The Webhook controller, this will import product and product associations when created/updated on Akeneo -Both this entry points deals to identify entities to import from Akeneo. When they have collected them they dispatch +These entry points deals to identify entities to import from Akeneo. When they have collected them they dispatch an `Webgriffe\SyliusAkeneoPlugin\Message\ItemImport` message on the messenger default bus. By default, in the configuration this message is handled by the main bus, the same bus used as default by Sylius for catalog promotions. This means that, if you have configured the main bus to run synchronously the import will be diff --git a/docs/contributing.md b/docs/contributing.md index a3b15104..12d49e10 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,7 +1,7 @@ --- title: Contributing layout: page -nav_order: 6 +nav_order: 7 --- # Contributing @@ -175,6 +175,7 @@ WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_CLIENT_ID=SAMPLE WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_SECRET=SAMPLE WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_USERNAME=SAMPLE WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_PASSWORD=SAMPLE +WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_WEBHOOK_SECRET=WEBHOOK_SECRET ``` Now, if you want you can import products from Akeneo to Sylius by launching the command: diff --git a/docs/images/akeneo-event-subscrition.png b/docs/images/akeneo-event-subscrition.png new file mode 100644 index 00000000..e28ca5da Binary files /dev/null and b/docs/images/akeneo-event-subscrition.png differ diff --git a/docs/index.md b/docs/index.md index 822b81a9..9c4c5dbe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ new features that this plugin could add. So, let's start! 🚀 - [Installation](installation.html) - [Configuration](configuration.html) - [Usage](usage.html) +- [Webhook](webhook.html) - [Architecture & customization](architecture_and_customization.html) - [Contributing](contributing.html) - [Upgrade guide](upgrade.html) diff --git a/docs/requirements.md b/docs/requirements.md index 2d71c339..298e355d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -9,7 +9,6 @@ nav_order: 1 * PHP `^8.0` * Sylius `^1.12` * Symfony `^5.4` or `^6.0` -* Akeneo PIM CE or EE `>= 3.2`. - The requirement for the version `3.2` is because the provided implementation of the product importer relies on - the `family_variant` key in the - Akeneo [GET Product model](https://api.akeneo.com/api-reference.html#get_product_models__code_) API response. +* Akeneo PIM CE or EE `>= 5.0`. + The requirement for the version `5.0` is because the plugin now requires the Akeneo API events to work properly. + See https://api.akeneo.com/events-documentation/overview.html#welcome-to-the-events-api-basics-documentation diff --git a/docs/upgrade/index.md b/docs/upgrade/index.md index 18859e45..5c105218 100644 --- a/docs/upgrade/index.md +++ b/docs/upgrade/index.md @@ -1,7 +1,7 @@ --- title: Upgrade layout: page -nav_order: 7 +nav_order: 8 has_children: true --- diff --git a/docs/upgrade/upgrade-2.0.md b/docs/upgrade/upgrade-2.*.md similarity index 98% rename from docs/upgrade/upgrade-2.0.md rename to docs/upgrade/upgrade-2.*.md index a64371f9..da0d8fbe 100644 --- a/docs/upgrade/upgrade-2.0.md +++ b/docs/upgrade/upgrade-2.*.md @@ -1,10 +1,15 @@ --- -title: Upgrade to 2.0 +title: Upgrade to 2.* layout: page nav_order: 0 parent: Upgrade --- +# Upgrade from `v2.2.0` to `v2.3.0` + +The v2.3.0 version introduces the support for webhooks. To enable check the new documentation [here](../webhook.html). +It is highly recommended to remove the import command that runs every minute from your crontab and use the webhook instead. + # Upgrade from `v1.17.0` to `v2.0.0` In the 2.0 version, we have introduced the Symfony Messenger component and removed all deprecations. diff --git a/docs/usage.md b/docs/usage.md index 44b36449..90674785 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -471,6 +471,9 @@ This will: * Import, every minute, all products that have been modified since the last execution, along with their associations * Reconcile Akeneo deleted products every 6 hours +> *NB*: The line that imports products and product associations every minute should be added only if you do not use the +> webhook feature (see next chapter). Otherwise, the products will be imported twice. + Import and Reconcile commands uses a [lock mechanism](https://symfony.com/doc/current/console/lockable_trait.html) which prevents running them if another instance of the same command is already running. {% endraw %} diff --git a/docs/webhook.md b/docs/webhook.md new file mode 100644 index 00000000..0105570d --- /dev/null +++ b/docs/webhook.md @@ -0,0 +1,58 @@ +--- +title: Webhook +layout: page +nav_order: 5 +--- + +{% raw %} + +# Webhook + +This plugin provides a webhook that can be used to automatically import products from Akeneo PIM to Sylius when they are +created or updated. +To use the webhook you need to: + +1. Import the routes needed for the plugin by adding the following to your `config/routes.yaml` file: + ```yaml + webgriffe_sylius_akeneo_plugin_webhook: + resource: "@WebgriffeSyliusAkeneoPlugin/config/webhook_routing.yaml" + prefix: '' + ``` + The url of the webhook can be anything you want but it must be the same you will configure in Akeneo PIM. The + imported resource will use /akeneo/webhook, but if you prefer you can add any prefix you want or you can completely + rewrite the url: + ```yaml + webgriffe_sylius_akeneo_plugin_webhook: + path: /akeneo/complete/url/rewrite/webhook + methods: [POST] + controller: webgriffe_sylius_akeneo.controller.webhook::postAction + ``` +2. Configure the webhook in Akeneo PIM. Remember that events API are available from Akeneo 5. You can find the webhook + configuration in the Akeneo PIM's + menu: `Connect > Connection settings`. Select the current data destination connection (the one used from the plugin). + Now, select Event subscription from the left menu. + Check Event subscription activation and leave unchecked Use product UUID instead of product identifier? (this is not + currently supported). Now is time to insert the full URL previously configurated. + When you click the Save button, a new secret token will be generated. Copy it and paste it in the plugin's + configuration (see next step). + ![akeneo-event-subscrition.png](images%2Fakeneo-event-subscrition.png) +3. In the plugin configuration (probably in the file config/packages/webgriffe_sylius_akeneo_plugin.yaml) add the + following: + ```yaml + webhook: + secret: 'YOUR_TOKEN_VALUE' + ``` + Replace YOUR_TOKEN_VALUE with the secret token generated previously by Akeneo PIM. As always, we suggest to add this + token by using an env variable to keep it secret from the repository ( + see [Symfony best practices doc](https://symfony.com/doc/current/best_practices.html#configuration)). +4. If you want, you can now TEST the webhook with the dedicated button on Akeneo event subscription page. If any error + occurs, you can debug the webhook by adjusting the monolog.logger.webgriffe_sylius_akeneo_plugin monolog level to + debug, so that you will see if there is something that is currently not working. +5. Finally, it is highly suggested that you remove the Product and ProductAssociations importer from the crontab to + avoid products imported twice: + ```diff + - * * * * * /path/to/sylius/bin/console -e prod -q webgriffe:akeneo:import --since-file=/path/to/sylius/var/storage/akeneo-import-sincefile.txt --importer="Product" --importer="ProductAssociations" + ``` + +{% endraw %} + diff --git a/src/Controller/WebhookController.php b/src/Controller/WebhookController.php new file mode 100644 index 00000000..82cb21bc --- /dev/null +++ b/src/Controller/WebhookController.php @@ -0,0 +1,143 @@ +, + * created: string, + * updated: string, + * associations: array, + * quantified_associations: array, + * } + * @psalm-type AkeneoEventProductModel = array{ + * code: string, + * family: string, + * family_variant: string, + * parent: ?string, + * categories: string[], + * values: array, + * created: string, + * updated: string, + * associations: array, + * quantified_associations: array, + * } + * @psalm-type AkeneoEvent = array{ + * action: string, + * event_id: string, + * event_datetime: string, + * author: string, + * author_type: string, + * pim_source: string, + * data: array{ + * resource: AkeneoEventProduct|AkeneoEventProductModel + * }, + * } + * @psalm-type AkeneoEvents = array{ + * events: AkeneoEvent[], + * } + */ +final class WebhookController extends AbstractController +{ + public function __construct( + private LoggerInterface $logger, + private MessageBusInterface $messageBus, + private string $secret, + ) { + } + + /** + * As guideline see the documentation here: https://api.akeneo.com/getting-started/quick-start-my-first-webhook-5x/step-2.html + * + * @throws RuntimeException + * @throws \JsonException + */ + public function postAction(Request $request): Response + { + $timestamp = $request->headers->get('x-akeneo-request-timestamp'); + $signature = $request->headers->get('x-akeneo-request-signature'); + if (null === $timestamp || null === $signature) { + $this->logger->debug('The hash does not exists on the request! The request is not from Akeneo.'); + + return new Response('', Response::HTTP_UNAUTHORIZED); + } + + /** + * @psalm-suppress UnnecessaryVarAnnotation + * + * @var string|resource $body on Symfony 5 the annotation is resource|string + */ + $body = $request->getContent(); + $expectedSignature = hash_hmac('sha256', $timestamp . '.' . (string) $body, $this->secret); + if (false === hash_equals($signature, $expectedSignature)) { + $this->logger->debug('The hash does not match! The request is not from Akeneo or the secret is wrong.'); + + return new Response('', Response::HTTP_UNAUTHORIZED); + } + if (time() - (int) $timestamp > 300) { + $this->logger->debug('The request is too old (> 5min)'); + + throw new RuntimeException('Request is too old (> 5min)'); + } + + if ($body === '') { + $this->logger->debug('The request body is empty, probably this request is a test from Event Subscription page on Akeneo.'); + + return new Response(); + } + + /** + * @TODO Could this be improved by using serializer? Is it necessary or overwork? + * + * @var AkeneoEvents $akeneoEvents + */ + $akeneoEvents = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR); + + foreach ($akeneoEvents['events'] as $akeneoEvent) { + $this->logger->debug(sprintf('Received event %s with id "%s"', $akeneoEvent['action'], $akeneoEvent['event_id'])); + + $resource = $akeneoEvent['data']['resource']; + if (array_key_exists('identifier', $resource)) { + $productCode = $resource['identifier']; + $this->logger->debug(sprintf( + 'Dispatching product import message for %s', + $productCode, + )); + $this->messageBus->dispatch(new ItemImport( + ProductImporter::AKENEO_ENTITY, + $productCode, + )); + $this->logger->debug(sprintf( + 'Dispatching product associations import message for %s', + $productCode, + )); + $this->messageBus->dispatch(new ItemImport( + ProductAssociationsImporter::AKENEO_ENTITY, + $productCode, + )); + } + } + + return new Response(); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b7b138aa..4562a04a 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -33,6 +33,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() + ->arrayNode('webhook') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('secret')->isRequired()->cannotBeEmpty()->defaultNull()->end() + ->end() + ->end() + ->arrayNode('value_handlers') ->children() ->arrayNode('product') diff --git a/src/DependencyInjection/WebgriffeSyliusAkeneoExtension.php b/src/DependencyInjection/WebgriffeSyliusAkeneoExtension.php index ac904b71..59ecd88e 100644 --- a/src/DependencyInjection/WebgriffeSyliusAkeneoExtension.php +++ b/src/DependencyInjection/WebgriffeSyliusAkeneoExtension.php @@ -115,6 +115,7 @@ final class WebgriffeSyliusAkeneoExtension extends AbstractResourceExtension imp public function load(array $configs, ContainerBuilder $container): void { + /** @var array{resources: array|mixed, api_client: array, webhook: array{secret: ?string}, value_handlers: array} $config */ $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../../config')); @@ -122,6 +123,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerResources('webgriffe_sylius_akeneo', 'doctrine/orm', $config['resources'], $container); $this->registerApiClientParameters($config['api_client'], $container); + $this->registerWebhookParameters($config['webhook'], $container); $loader->load('services.xml'); @@ -252,4 +254,12 @@ private function registerTemporaryDirectoryParameter(ContainerBuilder $container } $container->setParameter($parameterKey, sys_get_temp_dir()); } + + /** + * @param array{secret: ?string} $webhook + */ + private function registerWebhookParameters(array $webhook, ContainerBuilder $container): void + { + $container->setParameter('webgriffe_sylius_akeneo.webhook.secret', $webhook['secret']); + } } diff --git a/src/ProductAssociations/Importer.php b/src/ProductAssociations/Importer.php index 36e802b4..fe236e3b 100644 --- a/src/ProductAssociations/Importer.php +++ b/src/ProductAssociations/Importer.php @@ -25,7 +25,7 @@ final class Importer implements ImporterInterface { - private const AKENEO_ENTITY = 'ProductAssociations'; + public const AKENEO_ENTITY = 'ProductAssociations'; /** * @param RepositoryInterface $productAssociationRepository diff --git a/src/TemporaryFilesManager.php b/src/TemporaryFilesManager.php index b35616ea..6726d3b4 100644 --- a/src/TemporaryFilesManager.php +++ b/src/TemporaryFilesManager.php @@ -27,6 +27,9 @@ public function generateTemporaryFilePath(string $fileIdentifier): string public function deleteAllTemporaryFiles(string $fileIdentifier): void { + if (!$this->filesystem->exists($this->temporaryDirectory)) { + return; + } $tempFiles = $this->finder->in($this->temporaryDirectory)->depth('== 0')->files()->name( '/^' . str_replace('*', '\*', $this->getFilePrefix($fileIdentifier)) . '[\w]+$/', ); diff --git a/tests/Application/.env b/tests/Application/.env index 52949634..e1a8ae49 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -38,4 +38,5 @@ WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_USERNAME= WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_PASSWORD= WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_CLIENT_ID= WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_SECRET= +WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_WEBHOOK_SECRET= ###< webgriffe/sylius-akeneo-plugin ### diff --git a/tests/Application/config/packages/webgriffe_sylius_akeneo_plugin.yaml b/tests/Application/config/packages/webgriffe_sylius_akeneo_plugin.yaml index 97489180..d0cd4fd5 100644 --- a/tests/Application/config/packages/webgriffe_sylius_akeneo_plugin.yaml +++ b/tests/Application/config/packages/webgriffe_sylius_akeneo_plugin.yaml @@ -8,7 +8,10 @@ webgriffe_sylius_akeneo: password: '%env(WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_PASSWORD)%' client_id: '%env(WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_CLIENT_ID)%' secret: '%env(WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_SECRET)%' - + + webhook: + secret: '%env(WEBGRIFFE_SYLIUS_AKENEO_PLUGIN_WEBHOOK_SECRET)%' + value_handlers: product: attribute: diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml index a78894cf..03fea837 100644 --- a/tests/Application/config/routes.yaml +++ b/tests/Application/config/routes.yaml @@ -1,3 +1,6 @@ webgriffe_sylius_akeneo_plugin_admin: resource: "@WebgriffeSyliusAkeneoPlugin/config/admin_routing.yaml" prefix: '/%sylius_admin.path_name%' + +webgriffe_sylius_akeneo_plugin_webhook: + resource: "@WebgriffeSyliusAkeneoPlugin/config/webhook_routing.yaml" diff --git a/tests/Application/config/routes/sylius_shop.yaml b/tests/Application/config/routes/sylius_shop.yaml index 92eeae0c..fae46cbf 100644 --- a/tests/Application/config/routes/sylius_shop.yaml +++ b/tests/Application/config/routes/sylius_shop.yaml @@ -11,4 +11,4 @@ sylius_shop_default_locale: path: / methods: [GET] defaults: - _controller: sylius.controller.shop.locale_switch:switchAction + _controller: sylius.controller.shop.locale_switch::switchAction diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Product/Box/_content.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Product/Box/_content.html.twig new file mode 100644 index 00000000..cde40e08 --- /dev/null +++ b/tests/Application/templates/bundles/SyliusShopBundle/Product/Box/_content.html.twig @@ -0,0 +1,33 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + +
+ +
+
+
+
{{ 'sylius.ui.view_more'|trans }}
+
+
+
+ {% include '@SyliusShop/Product/_mainImage.html.twig' with {'product': product} %} +
+
+ {{ product.name }} + + {% if not product.enabledVariants.empty() %} + {% set variant = product|sylius_resolve_variant %} + {% if variant.hasChannelPricingForChannel(sylius.channel) %} + {% set price = money.calculatePrice(variant) %} + {% set originalPrice = money.calculateOriginalPrice(variant) %} + {% set appliedPromotions = variant.getAppliedPromotionsForChannel(sylius.channel) %} + + {% include '@SyliusShop/Product/Show/_catalogPromotionLabels.html.twig' with {'appliedPromotions': appliedPromotions, 'withDescription': false} %} + + {% if variant|sylius_has_discount({'channel': sylius.channel}) %} +
{{ originalPrice }}
+ {% endif %} +
{{ price }}
+ {% endif %} + {% endif %} +
+
diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_priceWidget.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_priceWidget.html.twig new file mode 100644 index 00000000..468868ac --- /dev/null +++ b/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_priceWidget.html.twig @@ -0,0 +1,17 @@ +{% set variant = product|sylius_resolve_variant %} + +{% if variant is not null and variant.hasChannelPricingForChannel(sylius.channel) %} + {% set appliedPromotions = variant.getChannelPricingForChannel(sylius.channel).getAppliedPromotions() %} + {% include '@SyliusShop/Product/Show/_catalogPromotionLabels.html.twig' with {'appliedPromotions': appliedPromotions, 'withDescription': true} %} +{% endif %} + +
+
+ {% if not product.enabledVariants.empty() and variant.hasChannelPricingForChannel(sylius.channel) %} + {% include '@SyliusShop/Product/Show/_price.html.twig' %} + {% endif %} +
+
+ {{ product.code }} +
+
diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_variants.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_variants.html.twig new file mode 100644 index 00000000..4c13ee61 --- /dev/null +++ b/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_variants.html.twig @@ -0,0 +1,42 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + + + + + + + + + + + {% for key, variant in product.enabledVariants %} + {% set channelPricing = variant.getChannelPricingForChannel(sylius.channel) %} + + + {% if variant.hasChannelPricingForChannel(sylius.channel) %} + {% set appliedPromotions = channelPricing.appliedPromotions|map(promotion => ({'label': promotion.label, 'description': promotion.description})) %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
{{ 'sylius.ui.variant'|trans }}{{ 'sylius.ui.price'|trans }}
+ {{ variant.name|default(variant.descriptor) }} + {% if product.hasOptions() %} +
+ {% for optionValue in variant.optionValues %} +
+ {{ optionValue.value }} +
+ {% endfor %} +
+ {% endif %} +
+ {{ money.calculatePrice(variant) }} + + + {{ form_widget(form.cartItem.variant[key], {'label': false}) }} +
diff --git a/tests/Integration/Controller/WebhookControllerTest.php b/tests/Integration/Controller/WebhookControllerTest.php new file mode 100644 index 00000000..3ec56a5d --- /dev/null +++ b/tests/Integration/Controller/WebhookControllerTest.php @@ -0,0 +1,104 @@ +webhookController = self::getContainer()->get('webgriffe_sylius_akeneo.controller.webhook'); + $this->itemImportResultRepository = self::getContainer()->get('webgriffe_sylius_akeneo.repository.item_import_result'); + + $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + $fixtureLoader->load([], [], [], PurgeMode::createDeleteMode()); + + InMemoryProductApi::addResource(Product::create('PRODUCT')); + } + + /** @test */ + public function it_imports_created_products_on_akeneo(): void + { + $body = ['events' => [ + [ + 'action' => 'product.created', + 'event_id' => '1', + 'data' => [ + 'resource' => [ + 'identifier' => 'PRODUCT', + ], + ], + ], + ]]; + $request = new Request([], [], [], [], [], [], json_encode($body, \JSON_THROW_ON_ERROR)); + + $timestamp = (string) time(); + $signature = hash_hmac('sha256', $timestamp . '.' . json_encode($body, \JSON_THROW_ON_ERROR), ''); + + $request->headers->set('x-akeneo-request-timestamp', $timestamp); + $request->headers->set('x-akeneo-request-signature', $signature); + $this->webhookController->postAction($request); + + $itemImportResults = $this->itemImportResultRepository->findAll(); + self::assertCount(2, $itemImportResults); + self::assertEquals('Successfully imported item "Product" with identifier "PRODUCT" from Akeneo.', $itemImportResults[0]->getMessage()); + self::assertEquals('Successfully imported item "ProductAssociations" with identifier "PRODUCT" from Akeneo.', $itemImportResults[1]->getMessage()); + } + + /** @test */ + public function it_fails_if_secret_is_not_right(): void + { + $body = ['events' => [ + [ + 'action' => 'product.created', + 'event_id' => '1', + 'data' => [ + 'resource' => [ + 'identifier' => 'PRODUCT', + ], + ], + ], + ]]; + $request = new Request([], [], [], [], [], [], json_encode($body, \JSON_THROW_ON_ERROR)); + + $timestamp = (string) time(); + $signature = hash_hmac('sha256', $timestamp . '.' . json_encode($body, \JSON_THROW_ON_ERROR), 'PIPPO'); + + $request->headers->set('x-akeneo-request-timestamp', $timestamp); + $request->headers->set('x-akeneo-request-signature', $signature); + $this->webhookController->postAction($request); + + $itemImportResults = $this->itemImportResultRepository->findAll(); + self::assertCount(0, $itemImportResults); + } + + /** @test */ + public function it_accepts_test_webhook_from_akeneo(): void + { + $request = new Request([], [], [], [], [], [], null); + + $timestamp = (string) time(); + $signature = hash_hmac('sha256', $timestamp . '.', ''); + + $request->headers->set('x-akeneo-request-timestamp', $timestamp); + $request->headers->set('x-akeneo-request-signature', $signature); + $response = $this->webhookController->postAction($request); + + self::assertEquals(200, $response->getStatusCode()); + } +}