diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4069be..c70bba4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,10 +18,11 @@ jobs: - '6.3.*' - '6.4.*' - '7.0.*' + - '7.1.*' include: - php: '8.2' coverage: 'xdebug' - symfony-versions: '7.0.*' + symfony-versions: '7.1.*' name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} ${{ matrix.description }} diff --git a/.gitignore b/.gitignore index a9893ed..b153e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /node_modules/ /.idea/ /package-lock.json +/reports +/clover.xml diff --git a/README.md b/README.md index accfb7a..ce43f70 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Behat Messenger Context Bundle +[![Latest Stable Version](http://poser.pugx.org/macpaw/behat-messenger-context/v)](https://packagist.org/packages/macpaw/behat-messenger-context) +[![Total Downloads](http://poser.pugx.org/macpaw/behat-messenger-context/downloads)](https://packagist.org/packages/macpaw/behat-messenger-context) +[![Latest Unstable Version](http://poser.pugx.org/macpaw/behat-messenger-context/v/unstable)](https://packagist.org/packages/macpaw/behat-messenger-context) +[![License](http://poser.pugx.org/macpaw/behat-messenger-context/license)](https://packagist.org/packages/macpaw/behat-messenger-context) +[![PHP Version Require](http://poser.pugx.org/macpaw/behat-messenger-context/require/php)](https://packagist.org/packages/macpaw/behat-messenger-context) + +| Version | Build Status | Coverage | +|------------|------------------------------------------------------------|--------------------------------------------------------------------------| +| `master` | [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] | +| `develop` | [![CI][develop Build Status Image]][develop Build Status] | [![Coverage Status][develop Code Coverage Image]][develop Code Coverage] | -| Version | Build Status | Code Coverage | -|:---------:|:-------------:|:-----:| -| `master`| [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] | -| `develop`| [![CI][develop Build Status Image]][develop Build Status] | [![Coverage Status][develop Code Coverage Image]][develop Code Coverage] | This repository provides custom Behat step definitions for working with Symfony Messenger transports. It includes functionality for checking messages in transports, validating them against expected JSON structures, and working with variable fields. @@ -26,9 +32,16 @@ You can use regular expressions to validate messages that contain dynamic or var * Documentation for specific message: [Check Transport Message with Regexp](docs/MessengerContext/check_transport_message_regexp.md) * Documentation for all messages: [Check All Transport Messages with Regexp](docs/MessengerContext/check_all_transport_message_regexp.md) +### Check Every Messages with Mask Regular Expressions +You can use regular expression to validate all messages messages that contain dynamic or variable data. +* Documentation for all specific message: [Check Transport Messages with Regexp](docs/MessengerContext/check_transport_messages_regexp_mask.md) + ### Verify Message Count in a Transport Ensure that a specific number of messages exist in a given transport. +### Auto clean queue messages before scenario +Check details in [documentation](docs/MessengerContext/clear_transport_with_zentruck.md) + * Documentation: [Count Messages in Transport](docs/MessengerContext/count_message_transport.md) [master Build Status]: https://github.com/macpaw/behat-messenger-context/actions?query=workflow%3ACI+branch%3Amaster diff --git a/composer.json b/composer.json index 308265d..d61fa7b 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,9 @@ "slevomat/coding-standard": "^7.0", "squizlabs/php_codesniffer": "^3.6" }, + "suggest": { + "zenstruck/messenger-test": "To use Zentruck messages clearing" + }, "autoload": { "psr-4": { "BehatMessengerContext\\": "src" diff --git a/docs/MessengerContext/check_transport_message_regexp.md b/docs/MessengerContext/check_transport_message_regexp.md index bdb023b..97cef4b 100644 --- a/docs/MessengerContext/check_transport_message_regexp.md +++ b/docs/MessengerContext/check_transport_message_regexp.md @@ -2,6 +2,7 @@ # Behat Custom Step: Transport JSON Message Assertion with Regexp Fields This documentation outlines the purpose and usage of the custom Behat step definition for verifying if a Symfony Messenger transport contains a message with a specific JSON structure, using regular expressions to handle dynamic fields. + ## Purpose This function is designed to check if a given transport (such as an asynchronous queue) contains a message that matches a particular JSON structure, while allowing certain fields to be validated using regular expressions. It is particularly useful when testing messages with dynamic data, such as timestamps, unique identifiers, or payloads, where the exact value cannot be guaranteed. diff --git a/docs/MessengerContext/check_transport_messages_regexp_mask.md b/docs/MessengerContext/check_transport_messages_regexp_mask.md new file mode 100644 index 0000000..1cc06c0 --- /dev/null +++ b/docs/MessengerContext/check_transport_messages_regexp_mask.md @@ -0,0 +1,37 @@ + +# Behat Custom Step: Transport JSON Message Assertion with Regexp Fields + +This documentation outlines the purpose and usage of the custom Behat step definition for verifying if a Symfony Messenger transport contains a message with a specific JSON mask for every message, using regular expressions to handle dynamic fields. + +## Purpose + +This function is designed to check if a given transport (such as an asynchronous queue) contains a message that matches a message with a specific JSON mask for every message, while allowing certain fields to be validated using regular expressions. It is particularly useful when testing messages with dynamic data, such as timestamps, unique identifiers, or payloads, where the exact value cannot be guaranteed. +## Function Overview + +### Signature: +```php +/** + * @Then all transport :transportName messages have JSON by :fields with mask :mask: + */ +public function allTransportMessagesHaveJsonByFieldsWithMask( + string $transportName, + string $variableFields, + PyStringNode $expectedMessageList, +): void { +``` + +### Parameters: +- `transportName` (string): The name of the transport (e.g., 'webhook') where the message is expected to be found. +- `variableFields` (string): A comma-separated list of field names where values should be matched using regular expressions. +- `expectedMessage` (PyStringNode): The expected message content in JSON format, where fields marked with `~` in their values will be treated as regular expressions. + +```gherkin +And all transport "webhook" messages should contain message with JSON and variable fields "time, payload" by mask: + """ + { + "event": "customer_agreement_status_updated", + "time": "~^\\d{13}$", + "payload": "~^\\{.*\\}$" + } + """ +``` diff --git a/docs/MessengerContext/clear_transport.md b/docs/MessengerContext/clear_transport.md new file mode 100644 index 0000000..acdb105 --- /dev/null +++ b/docs/MessengerContext/clear_transport.md @@ -0,0 +1,58 @@ +# Clean with in-memory transport +Switch all your queue to in-memory transport + +example: + +```yaml + # config/packages/messenger.yaml + + # ... + + when@test: + framework: + messenger: + transports: + async: in-memory:// + some-another: in-memory:// + # ... +``` + +All this transport will be cleared automatically before start every scenario. + +# Clear queues messages before scenario with zentruck + +We also support auto clear message queue with [zenstruck/messenger-test](https://github.com/zenstruck/messenger-test) + +## Installation + +1. Install the library: + + ```bash + composer require --dev zenstruck/messenger-test + ``` +2. If not added automatically by Symfony Flex, add the bundle in `config/bundles.php`: + + ```php + Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle::class => ['test' => true], + ``` + +3. Update `config/packages/messenger.yaml` and override your transport(s) + in your `test` environment with `test://`: + + ```yaml + # config/packages/messenger.yaml + + # ... + + when@test: + framework: + messenger: + transports: + async: test:// + ``` + +More details you can see in [origin package repository](https://github.com/zenstruck/messenger-test) + +> **Note**:Zentruck will be used automatically after installation. + + diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a4dbfd3..25235dc 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -40,5 +40,5 @@ src/ - tests/ + tests/Unit diff --git a/phpstan.neon b/phpstan.neon index 22368e9..ade9387 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,5 +2,6 @@ parameters: excludes_analyse: paths: - src + - tests/Unit level: 5 treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4b498ea..7dbe7a5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,7 +11,7 @@ stopOnFailure="false"> - tests + tests/Unit diff --git a/src/Context/MessengerContext.php b/src/Context/MessengerContext.php index 15580a8..e2245e6 100644 --- a/src/Context/MessengerContext.php +++ b/src/Context/MessengerContext.php @@ -6,18 +6,54 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\PyStringNode; +use Behat\Hook\AfterFeature; +use Behat\Hook\BeforeFeature; +use Behat\Hook\BeforeScenario; use Exception; use SimilarArrays\SimilarArray; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Zenstruck\Messenger\Test\Bus\TestBus; +use Zenstruck\Messenger\Test\Transport\TestTransport; class MessengerContext extends SimilarArray implements Context { + private ContainerInterface $container; + private NormalizerInterface $normalizer; + protected static TransportRetriever $transportRetriever; + public function __construct( - private readonly ContainerInterface $container, - private readonly NormalizerInterface $normalizer + ContainerInterface $container, + NormalizerInterface $normalizer, + TransportRetriever $transportRetriever, ) { + $this->container = $container; + $this->normalizer = $normalizer; + static::$transportRetriever = $transportRetriever; + } + + #[BeforeFeature] + public static function startTrackMessages(): void + { + if (class_exists(TestTransport::class) && class_exists(TestBus::class)) { + TestTransport::resetAll(); + TestTransport::enableMessagesCollection(); + TestTransport::disableResetOnKernelShutdown(); + TestBus::enableMessagesCollection(); + } + } + + #[AfterFeature] + public static function stopTrackMessages(): void + { + self::clearMessenger(); + } + + #[BeforeScenario] + public function clearMessengerBeforeScenario(): void + { + self::clearMessenger(); } /** @@ -41,8 +77,8 @@ public function transportShouldContainMessageWithJson(string $transportName, PyS throw new Exception( sprintf( 'The transport doesn\'t contain message with such JSON, actual messages: %s', - $this->getPrettyJson($actualMessageList) - ) + $this->getPrettyJson($actualMessageList), + ), ); } @@ -52,7 +88,7 @@ public function transportShouldContainMessageWithJson(string $transportName, PyS public function transportShouldContainMessageWithJsonAndVariableFields( string $transportName, string $variableFields, - PyStringNode $expectedMessage + PyStringNode $expectedMessage, ): void { $variableFields = $variableFields ? array_map('trim', explode(',', $variableFields)) : []; $expectedMessage = $this->decodeExpectedJson($expectedMessage); @@ -71,8 +107,8 @@ public function transportShouldContainMessageWithJsonAndVariableFields( throw new Exception( sprintf( 'The transport doesn\'t contain message with such JSON, actual messages: %s', - $this->getPrettyJson($actualMessageList) - ) + $this->getPrettyJson($actualMessageList), + ), ); } @@ -93,8 +129,39 @@ public function allTransportMessagesShouldBeJson(string $transportName, PyString throw new Exception( sprintf( 'The expected transport messages doesn\'t match actual: %s', - $this->getPrettyJson($actualMessageList) - ) + $this->getPrettyJson($actualMessageList), + ), + ); + } + } + + /** + * // phpcs:disable + * @And all transport :transportName messages should contain message with JSON and variable fields :fields by mask :mask: + * // phpcs:enable + */ + public function allTransportMessagesHaveJsonByFieldsWithMask( + string $transportName, + string $variableFields, + PyStringNode $expectedMessageList, + ): void { + $expectedMessageList = $this->decodeExpectedJson($expectedMessageList); + $variableFields = explode(', ', $variableFields); + + $transport = $this->getMessengerTransportByName($transportName); + $actualMessageList = []; + $expectedMessages = []; + foreach ($transport->get() as $envelope) { + $actualMessageList[] = $this->convertToArray($envelope->getMessage()); + $expectedMessages[] = $expectedMessageList; + } + + if (!$this->isArraysSimilar($expectedMessages, $actualMessageList, $variableFields)) { + throw new Exception( + sprintf( + 'The expected transport messages doesn\'t match actual: %s', + $this->getPrettyJson($actualMessageList), + ), ); } } @@ -105,7 +172,7 @@ public function allTransportMessagesShouldBeJson(string $transportName, PyString public function allTransportMessagesShouldBeJsonWithVariableFields( string $transportName, string $variableFields, - PyStringNode $expectedMessageList + PyStringNode $expectedMessageList, ): void { $variableFields = $variableFields ? array_map('trim', explode(',', $variableFields)) : []; $expectedMessageList = $this->decodeExpectedJson($expectedMessageList); @@ -120,8 +187,8 @@ public function allTransportMessagesShouldBeJsonWithVariableFields( throw new Exception( sprintf( 'The expected transport messages doesn\'t match actual: %s', - $this->getPrettyJson($actualMessageList) - ) + $this->getPrettyJson($actualMessageList), + ), ); } } @@ -139,14 +206,15 @@ public function thereIsCountMessagesInTransport(int $expectedMessageCount, strin sprintf( 'In transport exist actual count: %s, but expected count: %s', $actualMessageCount, - $expectedMessageCount - ) + $expectedMessageCount, + ), ); } } /** * @param array $message + * * @return string|bool */ private function getPrettyJson(array $message) @@ -156,11 +224,12 @@ private function getPrettyJson(array $message) /** * @param mixed $object + * * @return array */ private function convertToArray($object): array { - return (array) $this->normalizer->normalize($object); + return (array)$this->normalizer->normalize($object); } /** @@ -172,7 +241,7 @@ private function decodeExpectedJson(PyStringNode $expectedJson): array trim($expectedJson->getRaw()), true, 512, - JSON_THROW_ON_ERROR + JSON_THROW_ON_ERROR, ); } @@ -192,7 +261,22 @@ private function getMessengerTransportByName(string $transportName): InMemoryTra } throw new Exception( - 'In memory transport ' . $fullName . ' not found' + 'In memory transport ' . $fullName . ' not found', ); } + + private static function clearMessenger(): void + { + if (class_exists(TestTransport::class)) { + TestTransport::resetAll(); + } else { + $transports = static::$transportRetriever->getAllTransports(); + + foreach ($transports as $transport) { + if ($transport instanceof InMemoryTransport) { + $transport->reset(); + } + } + } + } } diff --git a/src/Context/TransportRetriever.php b/src/Context/TransportRetriever.php new file mode 100644 index 0000000..12ee7c3 --- /dev/null +++ b/src/Context/TransportRetriever.php @@ -0,0 +1,33 @@ +receiverLocator = $receiverLocator; + } + + /** + * @return TransportInterface[] + */ + public function getAllTransports(): array + { + $transports = []; + + foreach ($this->receiverLocator->getProvidedServices() as $name) { + $transports[$name] = $this->receiverLocator->get($name); + } + + return $transports; + } +} diff --git a/src/Resources/config/messenger_context.xml b/src/Resources/config/messenger_context.xml index 4afa875..dabaa59 100644 --- a/src/Resources/config/messenger_context.xml +++ b/src/Resources/config/messenger_context.xml @@ -6,6 +6,10 @@ + + + + diff --git a/tests/Stub/Zentruck/TestBus.php b/tests/Stub/Zentruck/TestBus.php new file mode 100644 index 0000000..3e722b6 --- /dev/null +++ b/tests/Stub/Zentruck/TestBus.php @@ -0,0 +1,27 @@ +container = $this->createMock(Container::class); + $this->normalizer = $this->createMock(NormalizerInterface::class); + $this->transportRetriever = $this->createMock(TransportRetriever::class); + $this->inMemoryTransport = $this->createMock(InMemoryTransport::class); + + $this->messengerContext = new MessengerContext( + $this->container, + $this->normalizer, + $this->transportRetriever + ); + } + + public function testTransportShouldContainMessageWithJson(): void + { + $message = new \stdClass(); + $expectedMessage = ['key' => 'value']; + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([new \Symfony\Component\Messenger\Envelope($message)]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->normalizer + ->expects($this->once()) + ->method('normalize') + ->with($message) + ->willReturn($expectedMessage); + + $this->messengerContext->transportShouldContainMessageWithJson( + 'test', + new PyStringNode(['{ "key": "value" }'], 1) + ); + } + + public function testFailTransportShouldContainMessageWithJson(): void + { + $message = new \stdClass(); + $expectedMessage = ['key' => 'value']; + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([new \Symfony\Component\Messenger\Envelope($message)]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->normalizer + ->expects($this->once()) + ->method('normalize') + ->with($message) + ->willReturn($expectedMessage); + + $this->expectException(Exception::class); + $this->messengerContext->transportShouldContainMessageWithJson( + 'test', + new PyStringNode(['{ "key1": "value" }'], 1) + ); + } + + public function testTransportShouldContainMessageWithJsonThrowsException(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("The transport doesn't contain message with such JSON"); + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->messengerContext->transportShouldContainMessageWithJson( + 'test', + new PyStringNode(['{ "key": "value" }'], 1) + ); + } + + public function testThereIsCountMessagesInTransport(): void + { + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([1, 2, 3]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->messengerContext->thereIsCountMessagesInTransport(3, 'test'); + } + + public function testThereIsCountMessagesInTransportThrowsException(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('In transport exist actual count: 2, but expected count: 3'); + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([1, 2]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->messengerContext->thereIsCountMessagesInTransport(3, 'test'); + } + + public function testFailTransportShouldContainMessageWithJsonAndVariableFields(): void + { + $message = new \stdClass(); + $message->id = '123'; + $message->name = 'TestMessage'; + $message->timestamp = '2024-10-17T12:00:00Z'; // Example variable field + + $expectedMessage = ['name' => 'TestMessage', 'timestamp' => 'IGNORED']; + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([new \Symfony\Component\Messenger\Envelope($message)]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->normalizer + ->expects($this->once()) + ->method('normalize') + ->with($message) + ->willReturn(['id' => '123', 'name' => 'TestMessage', 'timestamp' => '2024-10-17T12:00:00Z']); + + $this->expectException(Exception::class); + $this->messengerContext->transportShouldContainMessageWithJsonAndVariableFields( + 'test', + 'timestamp', + new PyStringNode(['{ "name": "TestMessage", "timestamp": "IGNORED" }'], 1) + ); + } + + public function testTransportShouldContainMessageWithJsonAndVariableFields(): void + { + $message = new \stdClass(); + $message->id = 'unique'; + $message->name = 'TestMessage'; + $message->timestamp = '2024-10-17T12:00:00Z'; // Example variable field + + $expectedMessage = ['id' => 'unique', 'name' => 'TestMessage', 'timestamp' => 'IGNORED']; + + $this->inMemoryTransport + ->expects($this->once()) + ->method('get') + ->willReturn([new Envelope($message)]); + + $this->container + ->expects($this->once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects($this->once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->normalizer + ->expects($this->once()) + ->method('normalize') + ->with($message) + ->willReturn($expectedMessage); + + $this->messengerContext->transportShouldContainMessageWithJsonAndVariableFields( + 'test', + '', + new PyStringNode([json_encode($expectedMessage)], 1) + ); + } + + public function testTransportNotFoundThrowsException(): void + { + $this->container->method('has')->willReturn(false); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Transport messenger.transport.invalid not found'); + + $this->messengerContext->transportShouldContainMessageWithJson( + 'invalid', + new PyStringNode([json_encode([])], 1), + ); + } + + public function testAllTransportMessagesShouldBeJson(): void + { + $message = new \stdClass(); + $message->key = 'value'; + + $envelope = new Envelope($message); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->method('normalize') + ->with($message) + ->willReturn(['key' => 'value']); + + $expectedJson = new PyStringNode(['[{"key": "value"}]'], 1); + + $this->messengerContext->allTransportMessagesShouldBeJson('test', $expectedJson); + } + + public function testFailAllTransportMessagesShouldBeJson(): void + { + $message = new \stdClass(); + $message->key = 'value'; + + $envelope = new Envelope($message); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->method('normalize') + ->with($message) + ->willReturn(['key' => 'value']); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('The expected transport messages doesn\'t match actual'); + $expectedJson = new PyStringNode(['[{"key1": "value"}]'], 1); + + $this->messengerContext->allTransportMessagesShouldBeJson('test', $expectedJson); + } + + public function testAllTransportMessagesShouldBeJsonWithVariableFields(): void + { + $message1 = new \stdClass(); + $message1->id = 1; + $message1->name = 'Test'; + + $message2 = new \stdClass(); + $message2->id = 2; + $message2->name = 'Test'; + + $message3 = new \stdClass(); + $message3->id = 1000; + $message3->name = 'Test5'; + + $envelope1 = new Envelope($message1); + $envelope2 = new Envelope($message2); + $envelope3 = new Envelope($message3); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope1, $envelope2, $envelope3]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->expects($this->exactly(3)) + ->method('normalize') + ->willReturnOnConsecutiveCalls( + ['id' => 1, 'name' => 'Test'], + ['id' => 2, 'name' => 'Test'], + ['id' => 1000, 'name' => 'Test5'], + ); + + $expectedJson = new PyStringNode([ + '[{"id": "~[0-9]+", "name": "Test"}, {"id": "~[0-9]+", "name": "Test"}, {"id": "~[0-9]+", "name": "Test5"}]' + ], 1); + + $this->messengerContext->allTransportMessagesShouldBeJsonWithVariableFields( + 'test', + 'id', + $expectedJson + ); + } + + public function testFailAllTransportMessagesShouldBeJsonWithVariableFields(): void + { + $message1 = new \stdClass(); + $message1->id = 1; + $message1->name = 'Test'; + + $message2 = new \stdClass(); + $message2->id = 2; + $message2->name = 'Test'; + + $envelope1 = new Envelope($message1); + $envelope2 = new Envelope($message2); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope1, $envelope2]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->expects($this->exactly(2)) + ->method('normalize') + ->willReturnOnConsecutiveCalls( + ['id' => 1, 'name' => 'Test'], + ['id' => 'uuid', 'name' => 'Test'] + ); + + $expectedJson = new PyStringNode([ + '{"id": "~\\\\d+", "name": "Test"}' + ], 1); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('The expected transport messages doesn\'t match actual'); + $this->messengerContext->allTransportMessagesShouldBeJsonWithVariableFields( + 'test', + 'id', + $expectedJson + ); + } + + public function testAllTransportMessagesHaveJsonByFieldsWithMask(): void + { + $message1 = new \stdClass(); + $message1->id = 1; + $message1->name = 'Test'; + + $message2 = new \stdClass(); + $message2->id = 2; + $message2->name = 'Test'; + + $message3 = new \stdClass(); + $message3->id = 1000; + $message3->name = 'Test5'; + + $envelope1 = new Envelope($message1); + $envelope2 = new Envelope($message2); + $envelope3 = new Envelope($message3); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope1, $envelope2, $envelope3]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->expects($this->exactly(3)) + ->method('normalize') + ->willReturnOnConsecutiveCalls( + ['id' => 1, 'name' => 'Test'], + ['id' => 2, 'name' => 'Test'], + ['id' => 1000, 'name' => 'Test5'], + ); + + $expectedJson = new PyStringNode([ + '{"id": "~[0-9]+", "name": "~[a-zA-Z]+"}' + ], 1); + + $this->messengerContext->allTransportMessagesHaveJsonByFieldsWithMask( + 'test', + 'id, name', + $expectedJson + ); + } + + public function testFailAllTransportMessagesHaveJsonByFieldsWithMask(): void + { + $message1 = new \stdClass(); + $message1->id = 1; + $message1->name = 'Test'; + + $message2 = new \stdClass(); + $message2->id = 2; + $message2->name = 'Test'; + + $message3 = new \stdClass(); + $message3->id = 1000; + $message3->name = 'Test5'; + + $envelope1 = new Envelope($message1); + $envelope2 = new Envelope($message2); + $envelope3 = new Envelope($message3); + + $transport = $this->createMock(InMemoryTransport::class); + $transport->method('get')->willReturn([$envelope1, $envelope2, $envelope3]); + + $this->container + ->expects(self::once()) + ->method('has') + ->with('messenger.transport.test') + ->willReturn(true); + $this->container + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($transport); + + $this->normalizer + ->expects($this->exactly(3)) + ->method('normalize') + ->willReturnOnConsecutiveCalls( + ['id' => 1, 'name' => 'Test'], + ['id' => 2, 'name' => 'Test'], + ['id' => 1000, 'name' => 'Test5'], + ); + + $expectedJson = new PyStringNode([ + '{"id": "~[a-zA-Z]+", "name": "~[a-zA-Z]+"}' + ], 1); + + $this->expectException(Exception::class); + $this->messengerContext->allTransportMessagesHaveJsonByFieldsWithMask( + 'test', + 'id, name', + $expectedJson + ); + } + + public function testTransportWasReset(): void + { + $serviceProvider = $this->createMock(ServiceProviderInterface::class); + $serviceProvider + ->expects($this->once()) + ->method('getProvidedServices') + ->willReturn(['messenger.transport.test']); + + $serviceProvider + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($this->inMemoryTransport); + + $this->inMemoryTransport + ->expects($this->once()) + ->method('reset'); + + (new MessengerContext( + $this->container, + $this->normalizer, + new TransportRetriever($serviceProvider) + ))->clearMessengerBeforeScenario(); + } + + public function testTransportWasResetWithZentruck(): void + { + require_once __DIR__ . '/../Stub/Zentruck/TestBus.php'; + require_once __DIR__ . '/../Stub/Zentruck/TestTransport.php'; + + $transportClass = 'Zenstruck\Messenger\Test\Transport\TestTransport'; + + $transportClass::reset(); + $this->messengerContext::stopTrackMessages(); + + $this->assertEquals( + $transportClass::RESET_ALL, + $transportClass::getResult() & $transportClass::RESET_ALL + ); + + $this->assertNotEquals( + $transportClass::DISABLE_RESET_ON_KERNEL_SHUTDOWN, + $transportClass::getResult() & $transportClass::DISABLE_RESET_ON_KERNEL_SHUTDOWN + ); + + $this->assertNotEquals( + $transportClass::ENABLE_MESSAGES_COLLECTION, + $transportClass::getResult() & $transportClass::ENABLE_MESSAGES_COLLECTION + ); + } + + public function testClearWithZentruck(): void + { + require_once __DIR__ . '/../Stub/Zentruck/TestBus.php'; + require_once __DIR__ . '/../Stub/Zentruck/TestTransport.php'; + + $transportClass = 'Zenstruck\Messenger\Test\Transport\TestTransport'; + $busClass = 'Zenstruck\Messenger\Test\Bus\TestBus'; + + $transportClass::reset(); + $busClass::reset(); + $this->messengerContext::startTrackMessages(); + + $this->assertEquals( + $transportClass::RESET_ALL, + $transportClass::getResult() & $transportClass::RESET_ALL + ); + + $this->assertEquals( + $transportClass::DISABLE_RESET_ON_KERNEL_SHUTDOWN, + $transportClass::getResult() & $transportClass::DISABLE_RESET_ON_KERNEL_SHUTDOWN + ); + + $this->assertEquals( + $transportClass::ENABLE_MESSAGES_COLLECTION, + $transportClass::getResult() & $transportClass::ENABLE_MESSAGES_COLLECTION + ); + + $this->assertEquals( + $busClass::ENABLE_MESSAGES_COLLECTION, + $busClass::getResult() & $busClass::ENABLE_MESSAGES_COLLECTION + ); + } +} diff --git a/tests/Unit/TransportRetrieverTest.php b/tests/Unit/TransportRetrieverTest.php new file mode 100644 index 0000000..b71f3ab --- /dev/null +++ b/tests/Unit/TransportRetrieverTest.php @@ -0,0 +1,36 @@ +createMock(InMemoryTransport::class); + $serviceProvider = $this->createMock(ServiceProviderInterface::class); + $serviceProvider + ->expects($this->once()) + ->method('getProvidedServices') + ->willReturn(['messenger.transport.test']); + + $serviceProvider + ->expects(self::once()) + ->method('get') + ->with('messenger.transport.test') + ->willReturn($inMemoryTransport); + + $transportRetriever = new TransportRetriever($serviceProvider); + + self::assertEquals( + ['messenger.transport.test' => $inMemoryTransport], + $transportRetriever->getAllTransports(), + ); + } +}