From b45e9c011f62f2c08272f5f07c791e31afad1157 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 23 Dec 2022 21:47:55 +0700 Subject: [PATCH 001/298] Use Rust FFI: Consumer --- .github/workflows/build.yml | 2 +- CHANGELOG.md | 61 ++++ README.md | 103 +++--- UPGRADE-9.0.md | 20 ++ composer.json | 18 +- example/phpunit.all.xml | 2 - example/phpunit.consumer.xml | 1 - example/phpunit.core.xml | 2 - .../Consumer/Service/HttpClientService.php | 4 +- .../Service/ConsumerServiceGoodbyeTest.php | 3 +- .../Service/ConsumerServiceHelloTest.php | 7 +- phpstan.neon | 3 + phpunit.xml | 1 - ...nteractionRequestBodyNotAddedException.php | 12 + ...teractionResponseBodyNotAddedException.php | 12 + .../MockServerNotStartedException.php | 12 + .../Exception/PactFileNotWroteException.php | 12 + .../Consumer/Hook/ContractDownloader.php | 98 ------ src/PhpPact/Consumer/InteractionBuilder.php | 37 +-- .../Consumer/Listener/PactTestListener.php | 65 ++-- src/PhpPact/Consumer/Matcher/Matcher.php | 31 +- src/PhpPact/Consumer/Model/AbstractPact.php | 20 ++ .../Consumer/Model/ConsumerRequest.php | 68 ++-- src/PhpPact/Consumer/Model/Interaction.php | 36 ++- src/PhpPact/Consumer/Model/Pact.php | 183 +++++++++++ .../Consumer/Model/ProviderResponse.php | 38 ++- src/PhpPact/Standalone/Broker/Broker.php | 6 +- .../Standalone/Broker/BrokerConfig.php | 108 ++++--- .../Exception/HealthCheckFailedException.php | 17 - .../Standalone/Installer/Model/Scripts.php | 14 +- .../Standalone/MockService/MockServer.php | 149 --------- .../MockService/MockServerConfig.php | 292 +----------------- .../MockService/MockServerConfigInterface.php | 51 +-- .../MockService/MockServerEnvConfig.php | 17 +- .../Service/MockServerHttpService.php | 173 ----------- .../MockServerHttpServiceInterface.php | 51 --- src/PhpPact/Standalone/PactConfig.php | 219 +++++++++++++ .../Standalone/PactConfigInterface.php | 39 ++- .../PactMessage/PactMessageConfig.php | 162 +--------- .../Consumer/InteractionBuilderTest.php | 27 -- .../PhpPact/Consumer/Matcher/MatcherTest.php | 166 ++++------ .../Consumer/Model/ConsumerRequestTest.php | 22 +- .../Consumer/Model/ProviderResponseTest.php | 8 +- .../Standalone/Broker/BrokerConfigTest.php | 105 +++++-- .../MockServer/MockServerConfigTest.php | 9 +- .../Standalone/MockServer/MockServerTest.php | 68 ---- .../Service/MockServerHttpServiceTest.php | 210 ------------- tests/PhpPact/Standalone/PactConfigTest.php | 65 ++++ 48 files changed, 1092 insertions(+), 1737 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 UPGRADE-9.0.md create mode 100644 phpstan.neon create mode 100644 src/PhpPact/Consumer/Exception/InteractionRequestBodyNotAddedException.php create mode 100644 src/PhpPact/Consumer/Exception/InteractionResponseBodyNotAddedException.php create mode 100644 src/PhpPact/Consumer/Exception/MockServerNotStartedException.php create mode 100644 src/PhpPact/Consumer/Exception/PactFileNotWroteException.php delete mode 100644 src/PhpPact/Consumer/Hook/ContractDownloader.php create mode 100644 src/PhpPact/Consumer/Model/AbstractPact.php create mode 100644 src/PhpPact/Consumer/Model/Pact.php delete mode 100644 src/PhpPact/Standalone/Exception/HealthCheckFailedException.php delete mode 100644 src/PhpPact/Standalone/MockService/MockServer.php delete mode 100644 src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php delete mode 100644 src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php create mode 100644 src/PhpPact/Standalone/PactConfig.php delete mode 100644 tests/PhpPact/Standalone/MockServer/MockServerTest.php delete mode 100644 tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php create mode 100644 tests/PhpPact/Standalone/PactConfigTest.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7697056b..b6cd146b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: openssl, sockets, curl, zip + extensions: openssl, sockets, curl, zip, ffi php-version: ${{ matrix.php }} - name: Composer install diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6f130920 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +CHANGELOG +========= + +9.0 +--- + +* General + * [BC BREAK] Declare types for: + * Properties + * Arguments + * Return values + +* Matchers + * [BC BREAK] Update matcher implementations + +* Installers + * [BC BREAK] Removed `PhpPact\Standalone\Installer\Model\Scripts::getMockService` + * Added `PhpPact\Standalone\Installer\Model\Scripts::getCode` + * Added `PhpPact\Standalone\Installer\Model\Scripts::getLibrary` + +* Config + * [BC BREAK] Updated `PhpPact\Standalone\MockService\MockServerConfigInterface`, removed these methods: + * `hasCors` + * `setCors` + * `setHealthCheckTimeout` + * `getHealthCheckTimeout` + * `setHealthCheckRetrySec` + * `getHealthCheckRetrySec` + * [BC BREAK] Removed environments variables: + * PACT_CORS + * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT + * PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC + * Moved these methods from `PhpPact\Standalone\MockService\MockServerConfigInterface` to `PhpPact\Standalone\PactConfigInterface`: + * `getPactFileWriteMode` + * `setPactFileWriteMode` + * `PhpPact\Standalone\MockService\MockServerConfigInterface` now extends `PhpPact\Standalone\PactConfigInterface` + * Added `PhpPact\Standalone\PactConfig` + * `PhpPact\Standalone\PactMessage\PactMessageConfig` now extends `PhpPact\Standalone\PactConfig` + * `PhpPact\Standalone\MockService\MockServerConfig` now extends `PhpPact\Standalone\PactConfig` + * Change default specification version to `3.0.0` + +* Mock Server + * [BC BREAK] Removed `PhpPact\Standalone\MockService\MockServer` + * [BC BREAK] Removed `PhpPact\Standalone\MockService\Service\MockServerHttpService` + * [BC BREAK] Removed `PhpPact\Standalone\MockService\Service\MockServerHttpServiceInterface` + * [BC BREAK] Removed `PhpPact\Standalone\Exception\HealthCheckFailedException` + * Added `PhpPact\Consumer\Exception\MockServerNotStartedException` + +* Interaction Builder + * Added `PhpPact\Consumer\InteractionBuilder::createMockServer` + * [BC BREAK] It's now required to call `PhpPact\Consumer\InteractionBuilder::createMockServer` manually before `PhpPact\Consumer\InteractionBuilder::verify` + * [BC BREAK] Removed `PhpPact\Consumer\InteractionBuilder::finalize` + * [BC BREAK] `PhpPact\Consumer\Model\ConsumerRequest::getQuery` now return `array` instead of `string` + * [BC BREAK] `PhpPact\Consumer\Model\ConsumerRequest::setQuery` now accept argument `array` instead of `string` + * Added `PhpPact\Consumer\Exception\InteractionRequestBodyNotAddedException` + * Added `PhpPact\Consumer\Exception\InteractionResponseBodyNotAddedException` + * Added `PhpPact\Consumer\Exception\PactFileNotWroteException` + +* PHPUnit + * [BC BREAK] Removed `PhpPact\Consumer\Hook\ContractDownloader` + * Replace broker http client by broker cli in `PhpPact\Consumer\Listener\PactTestListener` diff --git a/README.md b/README.md index accc88dd..3fb015bd 100644 --- a/README.md +++ b/README.md @@ -11,31 +11,39 @@ PHP version of [Pact](https://pact.io). Enables consumer driven contract testing Table of contents ================= -* [Versions](#versions) -* [Specifications](#specifications) -* [Installation](#installation) -* [Basic Consumer Usage](#basic-consumer-usage) - * [Start and Stop the Mock Server](#start-and-stop-the-mock-server) - * [Create Consumer Unit Test](#create-consumer-unit-test) - * [Create Mock Request](#create-mock-request) - * [Create Mock Response](#create-mock-response) - * [Build the Interaction](#build-the-interaction) - * [Make the Request](#make-the-request) - * [Make Assertions](#make-assertions) -* [Basic Provider Usage](#basic-provider-usage) - * [Create Unit Tests](#create-unit-test) - * [Start API](#start-api) - * [Provider Verification](#provider-verification) - * [Verify From Pact Broker](#verify-from-pact-broker) - * [Verify All from Pact Broker](#verify-all-from-pact-broker) - * [Verify Files by Path](#verify-files-by-path) -* [Tips](#tips) - * [Starting API Asynchronously](#starting-api-asynchronously) - * [Set Up Provider State](#set-up-provider-state) - * [Examples](#additional-examples) +- [Pact PHP](#pact-php) +- [Table of contents](#table-of-contents) + - [Versions](#versions) + - [Specifications](#specifications) + - [Installation](#installation) + - [Basic Consumer Usage](#basic-consumer-usage) + - [Publish Contracts To Pact Broker](#publish-contracts-to-pact-broker) + - [Create Consumer Unit Test](#create-consumer-unit-test) + - [Create Mock Request](#create-mock-request) + - [Create Mock Response](#create-mock-response) + - [Build the Interaction](#build-the-interaction) + - [Start the Mock Server](#start-the-mock-server) + - [Make the Request](#make-the-request) + - [Verify Interactions](#verify-interactions) + - [Make Assertions](#make-assertions) + - [Basic Provider Usage](#basic-provider-usage) + - [Create Unit Test](#create-unit-test) + - [Start API](#start-api) + - [Provider Verification](#provider-verification) + - [Verify From Pact Broker](#verify-from-pact-broker) + - [Verify All from Pact Broker](#verify-all-from-pact-broker) + - [Verify Files by Path](#verify-files-by-path) + - [Tips](#tips) + - [Starting API Asynchronously](#starting-api-asynchronously) + - [Set Up Provider State](#set-up-provider-state) + - [Additional Examples](#additional-examples) + - [Message support](#message-support) + - [Consumer Side Message Processing](#consumer-side-message-processing) + - [Provider Side Message Validation](#provider-side-message-validation) + - [Usage for the optional `pact-stub-service`](#usage-for-the-optional-pact-stub-service) ## Versions -9.X updates internal dependencies and libraries. This results in dropping PHP 7.4 +9.X adds support for pact specification 3.X & 4.X via Pact FFI. This results in dropping PHP 7.4 8.X updates internal dependencies and libraries. This results in dropping PHP 7.3 @@ -52,7 +60,13 @@ If you wish to stick with the 2.X implementation, you can continue to pull from ## Specifications -The 3.X version is the version of Pact-PHP, not the pact specification version that it supports. Pact-Php 3.X supports [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2). +The 3.X version is the version of Pact-PHP, not the pact specification version that it supports. + +Pact-Php 3.X -> 8.X supports [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2). +Pact-Php 9.X supports: + * [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2) + * [Pact-Specification 3.X](https://github.com/pact-foundation/pact-specification/tree/version-3). + * [Pact-Specification 4.X](https://github.com/pact-foundation/pact-specification/tree/version-4). ## Installation @@ -68,39 +82,12 @@ Composer hosts older versions under `mattersight/phppact`, which is abandoned. P All of the following code will be used exclusively for the Consumer. -### Start and Stop the Mock Server +### Publish Contracts To Pact Broker -This library contains a wrapper for the [Ruby Standalone Mock Service](https://github.com/pact-foundation/pact-mock_service). +When all tests in test suite are passed, you may want to publish generated contract files to pact broker automatically. The easiest way to configure this is to use a [PHPUnit Listener](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.test-listeners). A default listener is included in this project, see [PactTestListener.php](/src/PhpPact/Consumer/Listener/PactTestListener.php). This utilizes environmental variables for configurations. These env variables can either be added to the system or to the phpunit.xml configuration file. Here is an example [phpunit.xml](/example/phpunit.consumer.xml) file configured to use the default. Keep in mind that both the test suite and the arguments array must be the same value. -Alternatively, you can start and stop as in whatever means you would like by following this example: - -```php -setHost('localhost'); - $config->setPort(7200); - $config->setConsumer('someConsumer'); - $config->setProvider('someProvider'); - $config->setCors(true); - - // Instantiate the mock server object with the config. This can be any - // instance of MockServerConfigInterface. - $server = new MockServer($config); - - // Create the process. - $server->start(); - - // Stop the process. - $server->stop(); -``` - ### Create Consumer Unit Test Create a standard PHPUnit test case class and function. @@ -191,6 +178,14 @@ $builder ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. ``` +### Start the Mock Server + +Mock server need to be started manually + +```php +$builder->createMockServer(); +``` + ### Make the Request ```php @@ -204,7 +199,7 @@ Verify that all interactions took place that were registered. This typically should be in each test, that way the test that failed to verify is marked correctly. ```php -$builder->verify(); +$this->assertTrue($builder->verify()); ``` ### Make Assertions diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md new file mode 100644 index 00000000..c1628bf4 --- /dev/null +++ b/UPGRADE-9.0.md @@ -0,0 +1,20 @@ +UPGRADE FROM 8.x to 9.0 +======================= + +* Interaction Builder + * It's now required to call `PhpPact\Consumer\InteractionBuilder::createMockServer` manually + + Example Usage: + ```php + $builder = new InteractionBuilder($config); + $builder + ->given('a person exists') + ->uponReceiving('a get request to /hello/{name}') + ->with($request) + ->willRespondWith($response); + $builder->createMockServer(); + + $apiClient->sendRequest(); + + $this->assertTrue($builder->verify()); + ``` diff --git a/composer.json b/composer.json index 8b040008..2bedcec0 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,7 @@ }, "scripts": { "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", - "static-code-analysis": "phpstan analyse src/ --level=5", + "static-code-analysis": "phpstan analyse src/ --level=5 -c phpstan.neon", "lint": "php-cs-fixer fix --config .php-cs-fixer.php --dry-run", "fix": "php-cs-fixer fix --config .php-cs-fixer.php", "test": "phpunit --debug -c example/phpunit.all.xml" @@ -87,6 +87,22 @@ }, "url": "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{$version}/pact-{$version}-{$os}{$architecture}.{$extension}", "path": "bin/pact-ruby-standalone" + }, + "pact-ffi-headers": { + "version": "0.3.19", + "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", + "path": "bin/pact-ffi-headers/pact.h" + }, + "pact-ffi-lib": { + "version": "0.3.19", + "variables": { + "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", + "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", + "{$architecture}": "in_array(php_uname('m'), ['arm64', 'aarch64']) ? (PHP_OS === 'Darwin' ? 'aarch64-apple-darwin' : 'aarch64') : 'x86_64'", + "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'dll' : (PHP_OS === 'Darwin' ? 'dylib' : 'so')" + }, + "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/{$prefix}-{$os}-{$architecture}.{$extension}.gz", + "path": "bin/pact-ffi-lib/pact.{$extension}" } } }, diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 554c8a46..02a047d5 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -36,7 +36,5 @@ - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml index 6e88b251..0e521db3 100644 --- a/example/phpunit.consumer.xml +++ b/example/phpunit.consumer.xml @@ -24,7 +24,6 @@ - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml index aefb2d26..57e6e2e4 100644 --- a/example/phpunit.core.xml +++ b/example/phpunit.core.xml @@ -24,7 +24,5 @@ - - diff --git a/example/src/Consumer/Service/HttpClientService.php b/example/src/Consumer/Service/HttpClientService.php index 668ba75b..3ae6ad9d 100644 --- a/example/src/Consumer/Service/HttpClientService.php +++ b/example/src/Consumer/Service/HttpClientService.php @@ -12,10 +12,10 @@ class HttpClientService { /** @var Client */ - private $httpClient; + private Client $httpClient; /** @var string */ - private $baseUri; + private string $baseUri; public function __construct(string $baseUri) { diff --git a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php index e5106f55..bc840bb2 100644 --- a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php @@ -37,11 +37,12 @@ public function testGetGoodbyeString() ->uponReceiving('A get request to /goodbye/{name}') ->with($request) ->willRespondWith($response); + $builder->createMockServer(); $service = new HttpClientService($config->getBaseUri()); $result = $service->getGoodbyeString('Bob'); - $builder->verify(); + $this->assertTrue($builder->verify()); $this->assertEquals('Goodbye, Bob', $result); } diff --git a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php index 2a80f394..d23df42c 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php @@ -33,7 +33,7 @@ public function testGetHelloString() ->setStatus(200) ->addHeader('Content-Type', 'application/json') ->setBody([ - 'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]') + 'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]+') ]); // Create a configuration that reflects the server that was started. You can create a custom MockServerConfigInterface if needed. @@ -42,12 +42,13 @@ public function testGetHelloString() $builder ->uponReceiving('A get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. + ->willRespondWith($response); // This has to be last. This is what makes a FFI call to the Mock Server to set the interaction. + $builder->createMockServer(); $service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. $result = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. - $builder->verify(); // This will verify that the interactions took place. + $this->assertTrue($builder->verify()); // This will verify that the interactions took place. $this->assertEquals('Hello, Bob', $result); // Make your assertions. } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..e0f2f0e8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,3 @@ +parameters: + excludePaths: + - src/PhpPact/Consumer/Model/Pact.php diff --git a/phpunit.xml b/phpunit.xml index 35d425c2..af9ebf68 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -26,6 +26,5 @@ - diff --git a/src/PhpPact/Consumer/Exception/InteractionRequestBodyNotAddedException.php b/src/PhpPact/Consumer/Exception/InteractionRequestBodyNotAddedException.php new file mode 100644 index 00000000..2f99fd3e --- /dev/null +++ b/src/PhpPact/Consumer/Exception/InteractionRequestBodyNotAddedException.php @@ -0,0 +1,12 @@ +mockServerConfig = new MockServerEnvConfig(); - } - - /** - * @throws AssertionFailedError - * @throws RuntimeException - */ - public function executeAfterLastTest(): void - { - try { - $this->getMockServerService()->verifyInteractions(); - } catch (Exception $e) { - throw new AssertionFailedError('Pact interaction verification failed', 0, $e); - } - - try { - \file_put_contents($this->getPactFilename(), $this->getPactJson()); - } catch (Exception $e) { - throw new RuntimeException('Pact contract generation failed', 0, $e); - } - } - - private function getMockServerService(): MockServerHttpService - { - return new MockServerHttpService( - $this->getClient(), - $this->mockServerConfig - ); - } - - private function getClient(): ClientInterface - { - if (!$this->client) { - $this->client = new GuzzleClient(); - } - - return $this->client; - } - - private function getPactFilename(): string - { - return $this->mockServerConfig->getPactDir() - . DIRECTORY_SEPARATOR - . $this->mockServerConfig->getConsumer() - . '-' - . $this->mockServerConfig->getProvider() . '.json'; - } - - private function getPactJson(): string - { - $uri = $this->mockServerConfig->getBaseUri()->withPath('/pact'); - $response = $this->getClient()->post( - $uri, - [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - 'body' => \json_encode([ - 'consumer' => ['name' => $this->mockServerConfig->getConsumer()], - 'provider' => ['name' => $this->mockServerConfig->getProvider()] - ]) - ] - ); - - return \json_encode(\json_decode($response->getBody()->getContents())); - } -} diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 93ec41d6..39eef2a8 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -4,10 +4,9 @@ use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\Interaction; +use PhpPact\Consumer\Model\Pact; use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Http\GuzzleClient; use PhpPact\Standalone\MockService\MockServerConfigInterface; -use PhpPact\Standalone\MockService\Service\MockServerHttpService; /** * Build an interaction and send it to the Ruby Standalone Mock Service @@ -15,13 +14,11 @@ */ class InteractionBuilder implements BuilderInterface { - /** @var MockServerHttpService */ - protected $mockServerHttpService; - - /** @var MockServerConfigInterface */ - protected $config; /** @var Interaction */ - private $interaction; + protected Interaction $interaction; + + /** @var Pact */ + protected Pact $pact; /** * InteractionBuilder constructor. @@ -30,9 +27,8 @@ class InteractionBuilder implements BuilderInterface */ public function __construct(MockServerConfigInterface $config) { - $this->config = $config; - $this->mockServerHttpService = new MockServerHttpService(new GuzzleClient(), $config); $this->interaction = new Interaction(); + $this->pact = new Pact($config); } /** @@ -72,8 +68,6 @@ public function with(ConsumerRequest $request): self } /** - * Make the http request to the Mock Service to register the interaction. - * * @param ProviderResponse $response mock of response received * * @return bool returns true on success @@ -82,7 +76,7 @@ public function willRespondWith(ProviderResponse $response): bool { $this->interaction->setResponse($response); - return $this->mockServerHttpService->registerInteraction($this->interaction); + return $this->pact->registerInteraction($this->interaction); } /** @@ -90,21 +84,15 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->mockServerHttpService->verifyInteractions(); + return $this->pact->verifyInteractions(); } /** - * Writes the file to disk and deletes interactions from mock server. + * Create mock server before verifying. */ - public function finalize(): bool + public function createMockServer(): void { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); - - // Delete the interactions. - $this->mockServerHttpService->deleteAllInteractions(); - - return true; + $this->pact->createMockServer(); } /** @@ -112,9 +100,6 @@ public function finalize(): bool */ public function writePact(): bool { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); - return true; } } diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php index 5d88ef18..af66e11f 100644 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ b/src/PhpPact/Consumer/Listener/PactTestListener.php @@ -3,12 +3,10 @@ namespace PhpPact\Consumer\Listener; use GuzzleHttp\Psr7\Uri; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Http\GuzzleClient; use PhpPact\Standalone\Exception\MissingEnvVariableException; -use PhpPact\Standalone\MockService\MockServer; +use PhpPact\Standalone\Broker\Broker; +use PhpPact\Standalone\Broker\BrokerConfig; use PhpPact\Standalone\MockService\MockServerEnvConfig; -use PhpPact\Standalone\MockService\Service\MockServerHttpService; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestListener; @@ -23,21 +21,18 @@ class PactTestListener implements TestListener { use TestListenerDefaultImplementation; - /** @var MockServer */ - private $server; - /** * Name of the test suite configured in your phpunit config. * * @var string[] */ - private $testSuiteNames; + private array $testSuiteNames; /** @var MockServerEnvConfig */ - private $mockServerConfig; + private MockServerEnvConfig $mockServerConfig; /** @var bool */ - private $failed; + private bool $failed = false; /** * PactTestListener constructor. @@ -52,19 +47,6 @@ public function __construct(array $testSuiteNames) $this->mockServerConfig = new MockServerEnvConfig(); } - /** - * @param TestSuite $suite - * - * @throws \Exception - */ - public function startTestSuite(TestSuite $suite): void - { - if (\in_array($suite->getName(), $this->testSuiteNames)) { - $this->server = new MockServer($this->mockServerConfig); - $this->server->start(); - } - } - public function addError(Test $test, \Throwable $t, float $time): void { $this->failed = true; @@ -83,15 +65,6 @@ public function addFailure(Test $test, AssertionFailedError $e, float $time): vo public function endTestSuite(TestSuite $suite): void { if (\in_array($suite->getName(), $this->testSuiteNames)) { - try { - $httpService = new MockServerHttpService(new GuzzleClient(), $this->mockServerConfig); - $httpService->verifyInteractions(); - - $json = $httpService->getPactJson(); - } finally { - $this->server->stop(); - } - if ($this->failed === true) { print 'A unit test has failed. Skipping PACT file upload.'; } elseif (!($pactBrokerUri = \getenv('PACT_BROKER_URI'))) { @@ -101,29 +74,27 @@ public function endTestSuite(TestSuite $suite): void } elseif (!($tag = \getenv('PACT_CONSUMER_TAG'))) { print 'PACT_CONSUMER_TAG environment variable was not set. Skipping PACT file upload.'; } else { - $clientConfig = []; + $brokerConfig = new BrokerConfig(); + $brokerConfig->setPacticipant($this->mockServerConfig->getConsumer()); + $brokerConfig->setPactLocations($this->mockServerConfig->getPactDir()); + $brokerConfig->setBrokerUri(new Uri($pactBrokerUri)); + $brokerConfig->setConsumerVersion($consumerVersion); + $brokerConfig->setVersion($consumerVersion); + $brokerConfig->setTag($tag); if (($user = \getenv('PACT_BROKER_HTTP_AUTH_USER')) && ($pass = \getenv('PACT_BROKER_HTTP_AUTH_PASS')) ) { - $clientConfig = [ - 'auth' => [$user, $pass], - ]; - } - - if (($sslVerify = \getenv('PACT_BROKER_SSL_VERIFY'))) { - $clientConfig['verify'] = $sslVerify !== 'no'; + $brokerConfig->setBrokerUsername($user); + $brokerConfig->setBrokerPassword($pass); } - $headers = []; if ($bearerToken = \getenv('PACT_BROKER_BEARER_TOKEN')) { - $headers['Authorization'] = 'Bearer ' . $bearerToken; + $brokerConfig->setBrokerToken($bearerToken); } - $client = new GuzzleClient($clientConfig); - - $brokerHttpService = new BrokerHttpClient($client, new Uri($pactBrokerUri), $headers); - $brokerHttpService->tag($this->mockServerConfig->getConsumer(), $consumerVersion, $tag); - $brokerHttpService->publishJson($consumerVersion, $json); + $broker = new Broker($brokerConfig); + $broker->createVersionTag(); + $broker->publish(); print 'Pact file has been uploaded to the Broker successfully.'; } } diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 3317c3d5..1fed12cc 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -3,7 +3,9 @@ namespace PhpPact\Consumer\Matcher; /** - * Matcher implementation. Builds the Ruby Mock Server specification json for interaction publishing. + * Matcher implementation. Builds the Pact FFI specification json for interaction publishing. + * @see https://docs.pact.io/implementation_guides/rust/pact_ffi/integrationjson + * * Class Matcher. */ class Matcher @@ -46,8 +48,8 @@ public function like($value): array } return [ - 'contents' => $value, - 'json_class' => 'Pact::SomethingLike', + 'value' => $value, + 'pact:matcher:type' => 'type', ]; } @@ -61,14 +63,11 @@ public function like($value): array */ public function eachLike($value, int $min = 1): array { - $result = [ - 'contents' => $value, - 'json_class' => 'Pact::ArrayLike', + return [ + 'value' => array_fill(0, $min, $value), + 'pact:matcher:type' => 'type', + 'min' => $min, ]; - - $result['min'] = $min; - - return $result; } /** @@ -92,15 +91,9 @@ public function term($value, string $pattern): array } return [ - 'data' => [ - 'generate' => $value, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => $pattern, - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $value, + 'regex' => $pattern, + 'pact:matcher:type' => 'regex', ]; } diff --git a/src/PhpPact/Consumer/Model/AbstractPact.php b/src/PhpPact/Consumer/Model/AbstractPact.php new file mode 100644 index 00000000..088f2160 --- /dev/null +++ b/src/PhpPact/Consumer/Model/AbstractPact.php @@ -0,0 +1,20 @@ +ffi = FFI::cdef(\file_get_contents(Scripts::getCode()), Scripts::getLibrary()); + } +} diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index b54ce104..784fd264 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -11,27 +11,27 @@ class ConsumerRequest implements \JsonSerializable /** * @var string */ - private $method; + private string $method; /** - * @var array|string + * @var string */ - private $path; + private string $path; /** * @var string[] */ - private $headers; + private array $headers = []; /** - * @var mixed + * @var null|string */ - private $body; + private ?string $body = null; /** - * @var string + * @var array */ - private $query; + private array $query = []; /** * @return string @@ -54,9 +54,9 @@ public function setMethod(string $method): self } /** - * @return array|string + * @return string */ - public function getPath() + public function getPath(): string { return $this->path; } @@ -66,17 +66,17 @@ public function getPath() * * @return ConsumerRequest */ - public function setPath($path): self + public function setPath(array|string $path): self { - $this->path = $path; + $this->path = is_array($path) ? json_encode($path) : $path; return $this; } /** - * @return string[] + * @return array */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } @@ -99,17 +99,17 @@ public function setHeaders(array $headers): self * * @return ConsumerRequest */ - public function addHeader(string $header, $value): self + public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = $value; + $this->headers[$header] = is_array($value) ? json_encode($value) : $value; return $this; } /** - * @return mixed + * @return null|string */ - public function getBody() + public function getBody(): ?string { return $this->body; } @@ -119,27 +119,34 @@ public function getBody() * * @return ConsumerRequest */ - public function setBody($body) + public function setBody(mixed $body): self { - $this->body = $body; + if (\is_string($body)) { + $this->body = $body; + } elseif (!\is_null($body)) { + $this->body = \json_encode($body); + $this->addHeader('Content-Type', 'application/json'); + } else { + $this->body = null; + } return $this; } /** - * @return null|string + * @return array */ - public function getQuery() + public function getQuery(): array { return $this->query; } /** - * @param string $query + * @param array $query * * @return ConsumerRequest */ - public function setQuery(string $query): self + public function setQuery(array $query): self { $this->query = $query; @@ -148,17 +155,13 @@ public function setQuery(string $query): self /** * @param string $key - * @param string $value + * @param array|string $value * * @return ConsumerRequest */ - public function addQueryParameter(string $key, string $value): self + public function addQueryParameter(string $key, array|string $value): self { - if ($this->query === null) { - $this->query = "{$key}={$value}"; - } else { - $this->query = "{$this->query}&{$key}={$value}"; - } + $this->query[$key] = is_array($value) ? json_encode($value) : $value; return $this; } @@ -166,8 +169,7 @@ public function addQueryParameter(string $key, string $value): self /** * {@inheritdoc} */ - #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): array { $results = []; diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 06577c06..e7485fea 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -8,25 +8,50 @@ */ class Interaction implements \JsonSerializable { + /** + * @var int + */ + private int $id; + /** * @var string */ - private $description; + private string $description; /** * @var null|string */ - private $providerState; + private ?string $providerState = null; /** * @var ConsumerRequest */ - private $request; + private ConsumerRequest $request; /** * @var ProviderResponse */ - private $response; + private ProviderResponse $response; + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @param int $id + * + * @return Interaction + */ + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } /** * @return string @@ -111,8 +136,7 @@ public function setResponse(ProviderResponse $response): self /** * {@inheritdoc} */ - #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): array { if ($this->getProviderState()) { return [ diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php new file mode 100644 index 00000000..84d55223 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -0,0 +1,183 @@ +initWithLogLevel() + ->newPact() + ->withSpecification(); + } + + public function createMockServer(): void + { + $port = $this->ffi->pactffi_create_mock_server_for_transport( + $this->id, + $this->config->getHost(), + $this->config->getPort(), + $this->config->isSecure() ? 'https' : 'http', + null + ); + + if ($port < 0) { + $message = match ($port) { + -1 => 'An invalid handle was received. Handles should be created with `pactffi_new_pact`', + -2 => 'Transport_config is not valid JSON', + -3 => 'The mock server could not be started', + -4 => 'The method panicked', + -5 => 'The address is not valid', + }; + throw new MockServerNotStartedException($message); + } + $this->config->setPort($port); + } + + public function verifyInteractions(): bool + { + $result = $this->ffi->pactffi_mock_server_matched($this->config->getPort()); + + if ($result) { + $error = $this->ffi->pactffi_write_pact_file( + $this->config->getPort(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + $message = match ($error) { + 1 => 'A general panic was caught', + 2 => 'The pact file was not able to be written', + 3 => 'A mock server with the provided port was not found', + }; + throw new PactFileNotWroteException($message); + } + } + + $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); + $this->ffi->pactffi_free_pact_handle($this->id); + + return $result; + } + + public function registerInteraction(Interaction $interaction): bool + { + $this + ->newInteraction($interaction) + ->given($interaction) + ->uponReceiving($interaction) + ->with($interaction) + ->willRespondWith($interaction); + + return true; + } + + private function initWithLogLevel(): self + { + $logLevel = $this->config->getLogLevel(); + if ($logLevel) { + $this->ffi->pactffi_init_with_log_level($logLevel); + } + + return $this; + } + + private function newPact(): self + { + $this->id = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); + + return $this; + } + + private function withSpecification(): self + { + $supportedVersions = [ + '1.0.0' => $this->ffi->PactSpecification_V1, + '1.1.0' => $this->ffi->PactSpecification_V1_1, + '2.0.0' => $this->ffi->PactSpecification_V2, + '3.0.0' => $this->ffi->PactSpecification_V3, + '4.0.0' => $this->ffi->PactSpecification_V4, + ]; + $version = $supportedVersions[$this->config->getPactSpecificationVersion()] ?? $this->ffi->PactSpecification_Unknown; + $this->ffi->pactffi_with_specification($this->id, $version); + + return $this; + } + + private function newInteraction(Interaction $interaction): self + { + $id = $this->ffi->pactffi_new_interaction($this->id, $interaction->getDescription()); + $interaction->setId($id); + + return $this; + } + + private function given(Interaction $interaction): self + { + $this->ffi->pactffi_given($interaction->getId(), $interaction->getProviderState()); + + return $this; + } + + private function uponReceiving(Interaction $interaction): self + { + $this->ffi->pactffi_upon_receiving($interaction->getId(), $interaction->getDescription()); + + return $this; + } + + private function with(Interaction $interaction): self + { + $id = $interaction->getId(); + $request = $interaction->getRequest(); + $this->ffi->pactffi_with_request($id, $request->getMethod(), $request->getPath()); + foreach ($request->getHeaders() as $header => $value) { + $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Request, $header, 0, $value); + } + foreach ($request->getQuery() as $key => $value) { + $this->ffi->pactffi_with_query_parameter_v2($id, $key, 0, $value); + } + if (!\is_null($request->getBody())) { + $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Request, null, $request->getBody()); + if (!$success) { + throw new InteractionRequestBodyNotAddedException(); + } + } + + return $this; + } + + private function willRespondWith(Interaction $interaction): self + { + $id = $interaction->getId(); + $response = $interaction->getResponse(); + $this->ffi->pactffi_response_status($id, $response->getStatus()); + foreach ($response->getHeaders() as $header => $value) { + $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Response, $header, 0, $value); + } + if (!\is_null($response->getBody())) { + $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Response, null, $response->getBody()); + if (!$success) { + throw new InteractionResponseBodyNotAddedException(); + } + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index d138f555..449f5dac 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -11,17 +11,17 @@ class ProviderResponse implements \JsonSerializable /** * @var int */ - private $status; + private int $status; /** - * @var null|string[] + * @var string[] */ - private $headers; + private array $headers = []; /** - * @var null|array + * @var null|string */ - private $body; + private ?string $body = null; /** * @return int @@ -44,9 +44,9 @@ public function setStatus(int $status): self } /** - * @return null|string[] + * @return string[] */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } @@ -69,29 +69,36 @@ public function setHeaders(array $headers): self * * @return ProviderResponse */ - public function addHeader(string $header, $value): self + public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = $value; + $this->headers[$header] = is_array($value) ? json_encode($value) : $value; return $this; } /** - * @return mixed + * @return null|string */ - public function getBody() + public function getBody(): ?string { return $this->body; } /** - * @param iterable $body + * @param mixed $body * * @return ProviderResponse */ - public function setBody($body): self + public function setBody(mixed $body): self { - $this->body = $body; + if (\is_string($body)) { + $this->body = $body; + } elseif (!\is_null($body)) { + $this->body = \json_encode($body); + $this->addHeader('Content-Type', 'application/json'); + } else { + $this->body = null; + } return $this; } @@ -99,8 +106,7 @@ public function setBody($body): self /** * {@inheritdoc} */ - #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): array { $results = [ 'status' => $this->getStatus(), diff --git a/src/PhpPact/Standalone/Broker/Broker.php b/src/PhpPact/Standalone/Broker/Broker.php index 910706be..2dd0e5b8 100644 --- a/src/PhpPact/Standalone/Broker/Broker.php +++ b/src/PhpPact/Standalone/Broker/Broker.php @@ -12,11 +12,11 @@ class Broker { /** @var Logger */ - private $logger; + private Logger $logger; /** @var BrokerConfig */ - private $config; + private BrokerConfig $config; /** @var string */ - private $command; + private string $command; public function __construct(BrokerConfig $config) { diff --git a/src/PhpPact/Standalone/Broker/BrokerConfig.php b/src/PhpPact/Standalone/Broker/BrokerConfig.php index e54f668e..8e12ee45 100644 --- a/src/PhpPact/Standalone/Broker/BrokerConfig.php +++ b/src/PhpPact/Standalone/Broker/BrokerConfig.php @@ -7,55 +7,55 @@ class BrokerConfig { /** @var null|UriInterface */ - private $brokerUri; + private ?UriInterface $brokerUri = null; /** @var null|string */ - private $brokerToken; + private ?string $brokerToken = null; /** @var null|string */ - private $brokerUsername; + private ?string $brokerUsername = null; /** @var null|string */ - private $brokerPassword; + private ?string $brokerPassword = null; /** @var bool */ - private $verbose = false; + private bool $verbose = false; /** @var null|string */ - private $pacticipant; + private ?string $pacticipant = null; /** @var null|string */ - private $request; + private ?string $request = null; /** @var null|string */ - private $header; + private ?string $header = null; /** @var null|string */ - private $data; + private ?string $data = null; /** @var null|string */ - private $user; + private ?string $user = null; /** @var null|string */ - private $consumer; + private ?string $consumer = null; /** @var null|string */ - private $provider; + private ?string $provider = null; /** @var null|string */ - private $description; + private ?string $description = null; /** @var null|string */ - private $uuid; + private ?string $uuid = null; /** @var null|string */ - private $version; + private ?string $version = null; /** @var null|string */ - private $branch = null; + private ?string $branch = null; /** @var null|string */ - private $tag = null; + private ?string $tag = null; /** @var null|string */ - private $name; + private ?string $name = null; /** @var null|string */ - private $repositoryUrl; + private ?string $repositoryUrl = null; /** @var null|string */ - private $url; + private ?string $url = null; /** @var null|string */ - private $consumerVersion; + private ?string $consumerVersion = null; /** @var null|string */ - private $pactLocations; + private ?string $pactLocations = null; /** * @return null|string @@ -68,7 +68,7 @@ public function getRepositoryUrl(): ?string /** * @param null|string $repositoryUrl * - * @return BrokerConfig + * @return $this */ public function setRepositoryUrl(?string $repositoryUrl): self { @@ -88,7 +88,7 @@ public function getUrl(): ?string /** * @param null|string $url * - * @return BrokerConfig + * @return $this */ public function setUrl(?string $url): self { @@ -108,7 +108,7 @@ public function getVersion(): ?string /** * @param null|string $version * - * @return BrokerConfig + * @return $this */ public function setVersion(?string $version): self { @@ -128,7 +128,7 @@ public function getBranch(): ?string /** * @param null|string $branch * - * @return BrokerConfig + * @return $this */ public function setBranch(?string $branch): self { @@ -148,7 +148,7 @@ public function getTag(): ?string /** * @param null|string $tag * - * @return BrokerConfig + * @return $this */ public function setTag(?string $tag): self { @@ -168,7 +168,7 @@ public function getName(): ?string /** * @param null|string $name * - * @return BrokerConfig + * @return $this */ public function setName(?string $name): self { @@ -188,7 +188,7 @@ public function getRequest(): ?string /** * @param null|string $request * - * @return BrokerConfig + * @return $this */ public function setRequest(?string $request): self { @@ -208,7 +208,7 @@ public function getHeader(): ?string /** * @param null|string $header * - * @return BrokerConfig + * @return $this */ public function setHeader(?string $header): self { @@ -228,7 +228,7 @@ public function getData(): ?string /** * @param null|string $data * - * @return BrokerConfig + * @return $this */ public function setData(?string $data): self { @@ -248,7 +248,7 @@ public function getUser(): ?string /** * @param null|string $user * - * @return BrokerConfig + * @return $this */ public function setUser(?string $user): self { @@ -268,7 +268,7 @@ public function getConsumer(): ?string /** * @param null|string $consumer * - * @return BrokerConfig + * @return $this */ public function setConsumer(?string $consumer): self { @@ -288,7 +288,7 @@ public function getProvider(): ?string /** * @param null|string $provider * - * @return BrokerConfig + * @return $this */ public function setProvider(?string $provider): self { @@ -308,7 +308,7 @@ public function getDescription(): ?string /** * @param null|string $description * - * @return BrokerConfig + * @return $this */ public function setDescription(?string $description): self { @@ -328,7 +328,7 @@ public function getUuid(): ?string /** * @param null|string $uuid * - * @return BrokerConfig + * @return $this */ public function setUuid(?string $uuid): self { @@ -337,11 +337,26 @@ public function setUuid(?string $uuid): self return $this; } - public function isVerbose() + /** + * @return bool + */ + public function isVerbose(): bool { return $this->verbose; } + /** + * @param bool $verbose + * + * @return $this + */ + public function setVerbose(bool $verbose): self + { + $this->verbose = $verbose; + + return $this; + } + /** * @return null|UriInterface */ @@ -352,6 +367,8 @@ public function getBrokerUri(): ?UriInterface /** * @param null|UriInterface $brokerUri + * + * @return $this */ public function setBrokerUri(?UriInterface $brokerUri): self { @@ -370,6 +387,8 @@ public function getBrokerToken(): ?string /** * @param null|string $brokerToken + * + * @return $this */ public function setBrokerToken(?string $brokerToken): self { @@ -388,6 +407,8 @@ public function getBrokerUsername(): ?string /** * @param null|string $brokerUsername + * + * @return $this */ public function setBrokerUsername(?string $brokerUsername): self { @@ -406,6 +427,8 @@ public function getBrokerPassword(): ?string /** * @param null|string $brokerPassword + * + * @return $this */ public function setBrokerPassword(?string $brokerPassword): self { @@ -414,13 +437,18 @@ public function setBrokerPassword(?string $brokerPassword): self return $this; } - public function getPacticipant() + /** + * @return null|string + */ + public function getPacticipant(): ?string { return $this->pacticipant; } /** * @param null|string $pacticipant + * + * @return $this */ public function setPacticipant(?string $pacticipant): self { @@ -440,7 +468,7 @@ public function getConsumerVersion(): ?string /** * @param null|string $consumerVersion * - * @return BrokerConfig + * @return $this */ public function setConsumerVersion(?string $consumerVersion): self { @@ -460,7 +488,7 @@ public function getPactLocations(): ?string /** * @param string $locations * - * @return BrokerConfig + * @return $this */ public function setPactLocations(string $locations): self { diff --git a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php b/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php deleted file mode 100644 index 665ad684..00000000 --- a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php +++ /dev/null @@ -1,17 +0,0 @@ -config = $config; - - if (!$httpService) { - $this->httpService = new MockServerHttpService(new GuzzleClient(), $this->config); - } else { - $this->httpService = $httpService; - } - } - - /** - * Start the Mock Server. Verify that it is running. - * - * @throws Exception - * - * @return int process ID of the started Mock Server - */ - public function start(): int - { - $this->processRunner = new ProcessRunner(Scripts::getMockService(), $this->getArguments()); - - $processId = $this->processRunner->run(); - - $result = $this->verifyHealthCheck(); - if ($result) { - $retrySec = $this->config->getHealthCheckRetrySec(); - \sleep($retrySec); - } - - return $processId; - } - - /** - * Stop the Mock Server process. - * - * @return bool Was stopping successful? - */ - public function stop(): bool - { - return $this->processRunner->stop(); - } - - /** - * Build an array of command arguments. - * - * @return array - */ - private function getArguments(): array - { - $results = []; - - $logLevel = $this->config->getLogLevel(); - $consumer = \escapeshellarg($this->config->getConsumer()); - $provider = \escapeshellarg($this->config->getProvider()); - $pactDir = \escapeshellarg($this->config->getPactDir()); - - $results[] = 'service'; - $results[] = "--consumer={$consumer}"; - $results[] = "--provider={$provider}"; - $results[] = "--pact-dir={$pactDir}"; - $results[] = "--pact-file-write-mode={$this->config->getPactFileWriteMode()}"; - $results[] = "--host={$this->config->getHost()}"; - $results[] = "--port={$this->config->getPort()}"; - - if ($logLevel) { - $results[] = \sprintf('--log-level=%s', \escapeshellarg($logLevel)); - } - - if ($this->config->hasCors()) { - $results[] = '--cors=true'; - } - - if ($this->config->getPactSpecificationVersion() !== null) { - $results[] = "--pact-specification-version={$this->config->getPactSpecificationVersion()}"; - } - - if (!empty($this->config->getLog())) { - $log = \escapeshellarg($this->config->getLog()); - $results[] = \sprintf('--log=%s', $log); - } - - return $results; - } - - /** - * Make sure the server starts as expected. - * - * @throws Exception - * - * @return bool - */ - private function verifyHealthCheck(): bool - { - $service = $this->httpService; - - // Verify that the service is up. - $tries = 0; - $maxTries = $this->config->getHealthCheckTimeout(); - $retrySec = $this->config->getHealthCheckRetrySec(); - do { - ++$tries; - - try { - $status = $service->healthCheck(); - - return $status; - } catch (ConnectException $e) { - \sleep($retrySec); - } - } while ($tries <= $maxTries); - - // @phpstan-ignore-next-line - throw new HealthCheckFailedException("Failed to make connection to Mock Server in {$maxTries} attempts."); - } -} diff --git a/src/PhpPact/Standalone/MockService/MockServerConfig.php b/src/PhpPact/Standalone/MockService/MockServerConfig.php index 3b5c7621..05a03364 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfig.php @@ -2,98 +2,34 @@ namespace PhpPact\Standalone\MockService; -use Composer\Semver\VersionParser; use GuzzleHttp\Psr7\Uri; -use PhpPact\Standalone\PactConfigInterface; +use PhpPact\Standalone\PactConfig; use Psr\Http\Message\UriInterface; /** * Configuration defining the default PhpPact Ruby Standalone server. * Class MockServerConfig. */ -class MockServerConfig implements MockServerConfigInterface, PactConfigInterface +class MockServerConfig extends PactConfig implements MockServerConfigInterface { /** * Host on which to bind the service. * * @var string */ - private $host = 'localhost'; + private string $host = 'localhost'; /** - * Port on which to run the service. + * Port on which to run the service. A value of zero will result in the operating system allocating an available port. * * @var int */ - private $port = 7200; + private int $port = 7200; /** * @var bool */ - private $secure = false; - - /** - * Consumer name. - * - * @var string - */ - private $consumer; - - /** - * Provider name. - * - * @var string - */ - private $provider; - - /** - * Directory to which the pacts will be written. - * - * @var string - */ - private $pactDir; - - /** - * `overwrite` or `merge`. Use `merge` when running multiple mock service - * instances in parallel for the same consumer/provider pair. Ensure the - * pact file is deleted before running tests when using this option so that - * interactions deleted from the code are not maintained in the file. - * - * @var string - */ - private $pactFileWriteMode = 'overwrite'; - - /** - * The pact specification version to use when writing the pact. Note that only versions 1 and 2 are currently supported. - * - * @var string - */ - private $pactSpecificationVersion; - - /** - * File to which to log output. - * - * @var string - */ - private $log; - - /** @var bool */ - private $cors = false; - - /** - * The max allowed attempts the mock server has to be available in. Otherwise it is considered as sick. - * - * @var int - */ - private $healthCheckTimeout; - - /** - * The seconds between health checks of mock server - * - * @var int - */ - private $healthCheckRetrySec; - private $logLevel; + private bool $secure = false; /** * {@inheritdoc} @@ -106,7 +42,7 @@ public function getHost(): string /** * {@inheritdoc} */ - public function setHost(string $host): MockServerConfigInterface + public function setHost(string $host): self { $this->host = $host; @@ -124,7 +60,7 @@ public function getPort(): int /** * {@inheritdoc} */ - public function setPort(int $port): MockServerConfigInterface + public function setPort(int $port): self { $this->port = $port; @@ -142,7 +78,7 @@ public function isSecure(): bool /** * {@inheritdoc} */ - public function setSecure(bool $secure): MockServerConfigInterface + public function setSecure(bool $secure): self { $this->secure = $secure; @@ -158,214 +94,4 @@ public function getBaseUri(): UriInterface return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); } - - /** - * {@inheritdoc} - */ - public function getConsumer(): string - { - return $this->consumer; - } - - /** - * {@inheritdoc} - */ - public function setConsumer(string $consumer): PactConfigInterface - { - $this->consumer = $consumer; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProvider(): string - { - return $this->provider; - } - - /** - * {@inheritdoc} - */ - public function setProvider(string $provider): PactConfigInterface - { - $this->provider = $provider; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactDir() - { - if ($this->pactDir === null) { - return \sys_get_temp_dir(); - } - - return $this->pactDir; - } - - /** - * {@inheritdoc} - */ - public function setPactDir($pactDir): PactConfigInterface - { - if ($pactDir === null) { - return $this; - } - - if ('\\' !== \DIRECTORY_SEPARATOR) { - $pactDir = \str_replace('\\', \DIRECTORY_SEPARATOR, $pactDir); - } - - $this->pactDir = $pactDir; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactFileWriteMode(): string - { - return $this->pactFileWriteMode; - } - - /** - * {@inheritdoc} - */ - public function setPactFileWriteMode(string $pactFileWriteMode): MockServerConfigInterface - { - $options = ['overwrite', 'merge']; - - if (!\in_array($pactFileWriteMode, $options)) { - $implodedOptions = \implode(', ', $options); - - throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); - } - - $this->pactFileWriteMode = $pactFileWriteMode; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactSpecificationVersion() - { - return $this->pactSpecificationVersion; - } - - /** - * {@inheritdoc} - */ - public function setPactSpecificationVersion($pactSpecificationVersion): PactConfigInterface - { - /* - * Parse the version but do not assign it. If it is an invalid version, an exception is thrown - */ - $parser = new VersionParser(); - $parser->normalize($pactSpecificationVersion); - - $this->pactSpecificationVersion = $pactSpecificationVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getLog() - { - return $this->log; - } - - /** - * {@inheritdoc} - */ - public function setLog(string $log): PactConfigInterface - { - $this->log = $log; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getLogLevel() - { - return $this->logLevel; - } - - /** - * {@inheritdoc} - */ - public function setLogLevel(string $logLevel): PactConfigInterface - { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); - } - $this->logLevel = $logLevel; - - return $this; - } - - public function hasCors(): bool - { - return $this->cors; - } - - public function setCors($flag): MockServerConfigInterface - { - if ($flag === 'true') { - $this->cors = true; - } elseif ($flag === 'false') { - $this->cors = false; - } else { - $this->cors = (bool) $flag; - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function setHealthCheckTimeout($timeout): MockServerConfigInterface - { - $this->healthCheckTimeout = $timeout; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getHealthCheckTimeout(): int - { - return $this->healthCheckTimeout; - } - - /** - * {@inheritdoc} - */ - public function setHealthCheckRetrySec($seconds): MockServerConfigInterface - { - $this->healthCheckRetrySec = $seconds; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getHealthCheckRetrySec(): int - { - return $this->healthCheckRetrySec; - } } diff --git a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php index a1066ab3..fc6eb64c 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php @@ -2,13 +2,14 @@ namespace PhpPact\Standalone\MockService; +use PhpPact\Standalone\PactConfigInterface; use Psr\Http\Message\UriInterface; /** * Mock Server configuration interface to allow for simple overrides that are reusable. * Interface MockServerConfigInterface. */ -interface MockServerConfigInterface +interface MockServerConfigInterface extends PactConfigInterface { /** * @return string the host of the mock service @@ -50,52 +51,4 @@ public function setSecure(bool $secure): self; * @return UriInterface */ public function getBaseUri(): UriInterface; - - /** - * @return string 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten - */ - public function getPactFileWriteMode(): string; - - /** - * @param string $pactFileWriteMode 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten - * - * @return MockServerConfigInterface - */ - public function setPactFileWriteMode(string $pactFileWriteMode): self; - - /** - * @return bool - */ - public function hasCors(): bool; - - /** - * @param bool|string $flag - * - * @return MockServerConfigInterface - */ - public function setCors($flag): self; - - /** - * @param int $timeout - * - * @return MockServerConfigInterface - */ - public function setHealthCheckTimeout($timeout): self; - - /** - * @return int - */ - public function getHealthCheckTimeout(): int; - - /** - * @param int $seconds - * - * @return MockServerConfigInterface - */ - public function setHealthCheckRetrySec($seconds): self; - - /** - * @return int - */ - public function getHealthCheckRetrySec(): int; } diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index 0edc8251..1f11a8a8 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -10,7 +10,7 @@ */ class MockServerEnvConfig extends MockServerConfig { - public const DEFAULT_SPECIFICATION_VERSION = '2.0.0'; + public const DEFAULT_SPECIFICATION_VERSION = '3.0.0'; /** * MockServerEnvConfig constructor. @@ -24,7 +24,6 @@ public function __construct() $this->setConsumer($this->parseEnv('PACT_CONSUMER_NAME')); $this->setProvider($this->parseEnv('PACT_PROVIDER_NAME')); $this->setPactDir($this->parseEnv('PACT_OUTPUT_DIR', false)); - $this->setCors($this->parseEnv('PACT_CORS', false)); if ($logDir = $this->parseEnv('PACT_LOG', false)) { $this->setLog($logDir); @@ -34,18 +33,6 @@ public function __construct() $this->setLogLevel($logLevel); } - $timeout = $this->parseEnv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT', false); - if (!$timeout) { - $timeout = 10; - } - $this->setHealthCheckTimeout($timeout); - - $seconds = $this->parseEnv('PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC', false); - if (!$seconds) { - $seconds = 1; - } - $this->setHealthCheckRetrySec($seconds); - $version = $this->parseEnv('PACT_SPECIFICATION_VERSION', false); if (!$version) { $version = static::DEFAULT_SPECIFICATION_VERSION; @@ -64,7 +51,7 @@ public function __construct() * * @return null|string */ - private function parseEnv(string $variableName, bool $required = true) + private function parseEnv(string $variableName, bool $required = true): ?string { $result = null; diff --git a/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php b/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php deleted file mode 100644 index 3d2ac348..00000000 --- a/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php +++ /dev/null @@ -1,173 +0,0 @@ -client = $client; - $this->config = $config; - } - - /** - * {@inheritdoc} - */ - public function healthCheck(): bool - { - $uri = $this->config->getBaseUri()->withPath('/'); - - $response = $this->client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - $body = $response->getBody()->getContents(); - - if ($response->getStatusCode() !== 200 - || $body !== "Mock service running\n") { - throw new ConnectionException('Failed to receive a successful response from the Mock Server.'); - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function deleteAllInteractions(): bool - { - $uri = $this->config->getBaseUri()->withPath('/interactions'); - - $response = $this->client->delete($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - if ($response->getStatusCode() !== 200) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function registerInteraction(Interaction $interaction): bool - { - $uri = $this->config->getBaseUri()->withPath('/interactions'); - - $body = \json_encode($interaction->jsonSerialize()); - - $this->client->post($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - 'body' => $body, - ]); - - return true; - } - - /** - * Separate function for messages, instead of interactions, as I am unsure what to do with the Ruby Standalone at the moment - * - * @param Message $message - * - * @return bool - */ - public function registerMessage(Message $message): bool - { - $uri = $this->config->getBaseUri()->withPath('/interactions'); - - $body = \json_encode($message->jsonSerialize()); - - $this->client->post($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - 'body' => $body, - ]); - - return true; - } - - /** - * {@inheritdoc} - */ - public function verifyInteractions(): bool - { - $uri = $this->config->getBaseUri()->withPath('/interactions/verification'); - - $this->client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - return true; - } - - /** - * {@inheritdoc} - */ - public function getPactJson(): string - { - $uri = $this->config->getBaseUri()->withPath('/pact'); - $response = $this->client->post($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - return \json_encode(\json_decode($response->getBody()->getContents())); - } - - /** - * Wrapper for getPactJson to force the Ruby server to write the pact file to disk - * - * If the Pact-PHP does not gracefully kill the Ruby Server, it will not write the - * file to disk. This enables a work around. - */ - public function writePact(): string - { - return $this->getPactJson(); - } -} diff --git a/src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php b/src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php deleted file mode 100644 index e3bfbe62..00000000 --- a/src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php +++ /dev/null @@ -1,51 +0,0 @@ -consumer; + } + + /** + * {@inheritdoc} + */ + public function setConsumer(string $consumer): self + { + $this->consumer = $consumer; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getProvider(): string + { + return $this->provider; + } + + /** + * {@inheritdoc} + */ + public function setProvider(string $provider): self + { + $this->provider = $provider; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactDir(): string + { + if ($this->pactDir === null) { + return \sys_get_temp_dir(); + } + + return $this->pactDir; + } + + /** + * {@inheritdoc} + */ + public function setPactDir(?string $pactDir): self + { + if ($pactDir === null) { + return $this; + } + + if ('\\' !== \DIRECTORY_SEPARATOR) { + $pactDir = \str_replace('\\', \DIRECTORY_SEPARATOR, $pactDir); + } + + $this->pactDir = $pactDir; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactSpecificationVersion(): string + { + return $this->pactSpecificationVersion; + } + + /** + * {@inheritdoc} + * + * @throws \UnexpectedValueException + */ + public function setPactSpecificationVersion(string $pactSpecificationVersion): self + { + /* + * Parse the version but do not assign it. If it is an invalid version, an exception is thrown + */ + $parser = new VersionParser(); + $parser->normalize($pactSpecificationVersion); + + $this->pactSpecificationVersion = $pactSpecificationVersion; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLog(): ?string + { + return $this->log; + } + + /** + * {@inheritdoc} + */ + public function setLog(string $log): self + { + $this->log = $log; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLogLevel(): ?string + { + return $this->logLevel; + } + + /** + * {@inheritdoc} + */ + public function setLogLevel(string $logLevel): self + { + $logLevel = \strtoupper($logLevel); + if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { + throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); + } + $this->logLevel = $logLevel; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactFileWriteMode(): string + { + return $this->pactFileWriteMode; + } + + /** + * {@inheritdoc} + */ + public function setPactFileWriteMode(string $pactFileWriteMode): self + { + $options = [self::MODE_OVERWRITE, self::MODE_MERGE]; + + if (!\in_array($pactFileWriteMode, $options)) { + $implodedOptions = \implode(', ', $options); + + throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); + } + + $this->pactFileWriteMode = $pactFileWriteMode; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/PactConfigInterface.php b/src/PhpPact/Standalone/PactConfigInterface.php index 3369e46e..b1d21344 100644 --- a/src/PhpPact/Standalone/PactConfigInterface.php +++ b/src/PhpPact/Standalone/PactConfigInterface.php @@ -8,6 +8,9 @@ */ interface PactConfigInterface { + public const MODE_OVERWRITE = 'overwrite'; + public const MODE_MERGE = 'merge'; + /** * @return string */ @@ -16,7 +19,7 @@ public function getConsumer(): string; /** * @param string $consumer consumers name * - * @return PactConfigInterface + * @return $this */ public function setConsumer(string $consumer): self; @@ -28,50 +31,50 @@ public function getProvider(): string; /** * @param string $provider providers name * - * @return PactConfigInterface + * @return $this */ public function setProvider(string $provider): self; /** * @return string url to place the pact files when written to disk */ - public function getPactDir(); + public function getPactDir(): string; /** * @param null|string $pactDir url to place the pact files when written to disk * - * @return PactConfigInterface + * @return $this */ - public function setPactDir($pactDir): self; + public function setPactDir(?string $pactDir): self; /** * @return string pact version */ - public function getPactSpecificationVersion(); + public function getPactSpecificationVersion(): string; /** * @param string $pactSpecificationVersion pact semver version * - * @return PactConfigInterface + * @return $this */ - public function setPactSpecificationVersion($pactSpecificationVersion): self; + public function setPactSpecificationVersion(string $pactSpecificationVersion): self; /** - * @return string directory for log output + * @return null|string directory for log output */ - public function getLog(); + public function getLog(): ?string; /** * @param string $log directory for log output * - * @return PactConfigInterface + * @return $this */ public function setLog(string $log): self; /** * @return null|string */ - public function getLogLevel(); + public function getLogLevel(): ?string; /** * @param string $logLevel @@ -79,4 +82,16 @@ public function getLogLevel(); * @return $this */ public function setLogLevel(string $logLevel): self; + + /** + * @return string 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten + */ + public function getPactFileWriteMode(): string; + + /** + * @param string $pactFileWriteMode 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten + * + * @return $this + */ + public function setPactFileWriteMode(string $pactFileWriteMode): self; } diff --git a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php b/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php index 2874e772..3ad3c782 100644 --- a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php +++ b/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php @@ -2,168 +2,12 @@ namespace PhpPact\Standalone\PactMessage; -use Composer\Semver\VersionParser; -use PhpPact\Standalone\PactConfigInterface; +use PhpPact\Standalone\PactConfig; /** * Configuration defining the default PhpPact Ruby Standalone server. - * Class MockServerConfig. + * Class PactMessageConfig. */ -class PactMessageConfig implements PactConfigInterface +class PactMessageConfig extends PactConfig { - /** - * Consumer name. - * - * @var string - */ - private $consumer; - - /** - * Provider name. - * - * @var string - */ - private $provider; - - /** - * Directory to which the pacts will be written. - * - * @var string - */ - private $pactDir; - - /** - * The pact specification version to use when writing the pact. Note that only versions 1 and 2 are currently supported. - * - * @var string - */ - private $pactSpecificationVersion; - - /** - * File to which to log output. - * - * @var string - */ - private $log; - - /** @var string */ - private $logLevel; - - /** - * {@inheritdoc} - */ - public function getConsumer(): string - { - return $this->consumer; - } - - /** - * {@inheritdoc} - */ - public function setConsumer(string $consumer): PactConfigInterface - { - $this->consumer = $consumer; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProvider(): string - { - return $this->provider; - } - - /** - * {@inheritdoc} - */ - public function setProvider(string $provider): PactConfigInterface - { - $this->provider = $provider; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactDir() - { - if ($this->pactDir === null) { - return \sys_get_temp_dir(); - } - - return $this->pactDir; - } - - /** - * {@inheritdoc} - */ - public function setPactDir($pactDir): PactConfigInterface - { - $this->pactDir = $pactDir; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactSpecificationVersion() - { - return $this->pactSpecificationVersion; - } - - /** - * {@inheritdoc} - * - * @throws \UnexpectedValueException - */ - public function setPactSpecificationVersion($pactSpecificationVersion): PactConfigInterface - { - /* - * Parse the version but do not assign it. If it is an invalid version, an exception is thrown - */ - $parser = new VersionParser(); - $parser->normalize($pactSpecificationVersion); - - $this->pactSpecificationVersion = $pactSpecificationVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getLog() - { - return $this->log; - } - - /** - * {@inheritdoc} - */ - public function setLog(string $log): PactConfigInterface - { - $this->log = $log; - - return $this; - } - - public function getLogLevel() - { - return $this->logLevel; - } - - public function setLogLevel(string $logLevel): PactConfigInterface - { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); - } - $this->logLevel = $logLevel; - - return $this; - } } diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index 06567a76..b897398c 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -5,39 +5,12 @@ use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Http\GuzzleClient; use PhpPact\Standalone\Exception\MissingEnvVariableException; -use PhpPact\Standalone\MockService\MockServer; use PhpPact\Standalone\MockService\MockServerEnvConfig; -use PhpPact\Standalone\MockService\Service\MockServerHttpService; -use PhpPact\Standalone\MockService\Service\MockServerHttpServiceInterface; use PHPUnit\Framework\TestCase; class InteractionBuilderTest extends TestCase { - /** @var MockServerHttpServiceInterface */ - private $service; - - /** @var MockServer */ - private $mockServer; - - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - protected function setUp(): void - { - $config = new MockServerEnvConfig(); - $this->mockServer = new MockServer($config); - $this->mockServer->start(); - $this->service = new MockServerHttpService(new GuzzleClient(), $config); - } - - protected function tearDown(): void - { - $this->mockServer->stop(); - } - /** * @throws MissingEnvVariableException * @throws \Exception diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 6db70501..55dffc4a 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -8,7 +8,7 @@ class MatcherTest extends TestCase { /** @var Matcher */ - private $matcher; + private Matcher $matcher; protected function setUp(): void { @@ -31,7 +31,7 @@ public function testLike() { $json = \json_encode($this->matcher->like(12)); - $this->assertEquals('{"contents":12,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":12,"pact:matcher:type":"type"}', $json); } /** @@ -44,15 +44,17 @@ public function testEachLikeStdClass() $object->value2 = 2; $expected = \json_encode([ - 'contents' => [ - 'value1' => [ - 'contents' => 1, - 'json_class' => 'Pact::SomethingLike', - ], - 'value2' => 2, + 'value' => [ + [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ] ], - 'json_class' => 'Pact::ArrayLike', - 'min' => 1, + 'pact:matcher:type' => 'type', + 'min' => 1, ]); $actual = \json_encode($this->matcher->eachLike($object, 1)); @@ -71,15 +73,17 @@ public function testEachLikeArray() ]; $expected = \json_encode([ - 'contents' => [ - 'value1' => [ - 'contents' => 1, - 'json_class' => 'Pact::SomethingLike', - ], - 'value2' => 2, + 'value' => [ + [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ] ], - 'json_class' => 'Pact::ArrayLike', - 'min' => 1, + 'pact:matcher:type' => 'type', + 'min' => 1, ]); $actual = \json_encode($this->matcher->eachLike($object, 1)); @@ -102,15 +106,9 @@ public function testRegexNoMatch() public function testRegex() { $expected = [ - 'data' => [ - 'generate' => 'Games', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => 'Games|Other', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'Games', + 'regex' => 'Games|Other', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->regex('Games', 'Games|Other'); @@ -124,15 +122,9 @@ public function testRegex() public function testDate() { $expected = [ - 'data' => [ - 'generate' => '2010-01-17', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '2010-01-17', + 'regex' => '^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateISO8601('2010-01-17'); @@ -148,15 +140,9 @@ public function testDate() public function testTime($time) { $expected = [ - 'data' => [ - 'generate' => $time, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $time, + 'regex' => '^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->timeISO8601($time); @@ -188,15 +174,9 @@ public function dataProviderForTimeTest() public function testDateTime($dateTime) { $expected = [ - 'data' => [ - 'generate' => $dateTime, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $dateTime, + 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateTimeISO8601($dateTime); @@ -226,15 +206,9 @@ public function dataProviderForDateTimeTest() public function testDateTimeWithMillis($dateTime) { $expected = [ - 'data' => [ - 'generate' => $dateTime, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $dateTime, + 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateTimeWithMillisISO8601($dateTime); @@ -262,15 +236,9 @@ public function dataProviderForDateTimeWithMillisTest() public function testTimestampRFC3339() { $expected = [ - 'data' => [ - 'generate' => 'Mon, 31 Oct 2016 15:21:41 -0400', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'Mon, 31 Oct 2016 15:21:41 -0400', + 'regex' => '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->timestampRFC3339('Mon, 31 Oct 2016 15:21:41 -0400'); @@ -285,7 +253,7 @@ public function testInteger() { $json = \json_encode($this->matcher->integer()); - $this->assertEquals('{"contents":13,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":13,"pact:matcher:type":"type"}', $json); } /** @@ -295,7 +263,7 @@ public function testBoolean() { $json = \json_encode($this->matcher->boolean()); - $this->assertEquals('{"contents":true,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":true,"pact:matcher:type":"type"}', $json); } /** @@ -305,7 +273,7 @@ public function testDecimal() { $json = \json_encode($this->matcher->decimal()); - $this->assertEquals('{"contents":13.01,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":13.01,"pact:matcher:type":"type"}', $json); } /** @@ -314,15 +282,9 @@ public function testDecimal() public function testHexadecimal() { $expected = [ - 'data' => [ - 'generate' => '3F', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^[0-9a-fA-F]+$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '3F', + 'regex' => '^[0-9a-fA-F]+$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->hexadecimal()); @@ -334,15 +296,9 @@ public function testHexadecimal() public function testUuid() { $expected = [ - 'data' => [ - 'generate' => 'ce118b6e-d8e1-11e7-9296-cec278b6b50a', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'ce118b6e-d8e1-11e7-9296-cec278b6b50a', + 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->uuid()); @@ -354,15 +310,9 @@ public function testUuid() public function testIpv4Address() { $expected = [ - 'data' => [ - 'generate' => '127.0.0.13', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(\\d{1,3}\\.)+\\d{1,3}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '127.0.0.13', + 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->ipv4Address()); @@ -374,15 +324,9 @@ public function testIpv4Address() public function testIpv6Address() { $expected = [ - 'data' => [ - 'generate' => '::ffff:192.0.2.128', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '::ffff:192.0.2.128', + 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->ipv6Address()); diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 073df261..55225152 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -18,12 +18,10 @@ public function testSerializing() 'currentCity' => 'Austin', ]); - $data = \json_decode(\json_encode($model->jsonSerialize()), true); - - $this->assertEquals('PUT', $data['method']); - $this->assertEquals('application/json', $data['headers']['Content-Type']); - $this->assertEquals('/somepath', $data['path']); - $this->assertEquals('Austin', $data['body']['currentCity']); + $this->assertEquals('PUT', $model->getMethod()); + $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals('/somepath', $model->getPath()); + $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); } public function testSerializingWhenPathUsingMatcher() @@ -39,13 +37,9 @@ public function testSerializingWhenPathUsingMatcher() 'status' => 'finished', ]); - $data = \json_decode(\json_encode($model->jsonSerialize()), true); - - $this->assertEquals('PATCH', $data['method']); - $this->assertEquals('application/json', $data['headers']['Content-Type']); - $this->assertIsArray($data['path']); - $this->assertArrayHasKey('data', $data['path']); - $this->assertArrayHasKey('json_class', $data['path']); - $this->assertEquals('finished', $data['body']['status']); + $this->assertEquals('PATCH', $model->getMethod()); + $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); + $this->assertEquals('{"status":"finished"}', $model->getBody()); } } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 17291378..35bdc2a9 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -16,10 +16,8 @@ public function testSerializing() 'currentCity' => 'Austin', ]); - $data = \json_decode(\json_encode($model->jsonSerialize()), true); - - $this->assertEquals(200, $data['status']); - $this->assertEquals('application/json', $data['headers']['Content-Type']); - $this->assertEquals('Austin', $data['body']['currentCity']); + $this->assertEquals(200, $model->getStatus()); + $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); } } diff --git a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php b/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php index 18e27893..2d53fd5e 100644 --- a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php +++ b/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php @@ -1,43 +1,88 @@ setHost($host) - ->setPort($port) - ->setProvider($provider) + $brokerUri = new Uri('http://localhost'); + $brokerToken = 'abc-123'; + $brokerUsername = 'user'; + $brokerPassword = 'pass'; + + $verbose = true; + $pacticipant = 'a pacticipant'; + + $request = 'POST'; + $header = 'Accept application/json'; + $data = '{"key": "value"}'; + $user = 'username:password'; + $url = 'https://example.org/webhook'; + $consumer = 'test-consumer'; + $provider = 'test-provider'; + $description = 'an example webhook'; + $uuid = 'd2181b32-8b03-4daf-8cc0-d9168b2f6fac'; + + $version = '1.2.3'; + $branch = 'new-feature'; + $tag = 'prod'; + + $name = 'My Project'; + $repositoryUrl = 'https://github.com/vendor/my-project'; + + $consumerVersion = '1.1.2'; + $pactLocations = '/path/to/pacts'; + + $subject = (new BrokerConfig()) + ->setBrokerUri($brokerUri) + ->setBrokerToken($brokerToken) + ->setBrokerUsername($brokerUsername) + ->setBrokerPassword($brokerPassword) + ->setVerbose($verbose) + ->setPacticipant($pacticipant) + ->setRequest($request) + ->setHeader($header) + ->setData($data) + ->setUser($user) + ->setUrl($url) ->setConsumer($consumer) - ->setPactDir($pactDir) - ->setPactFileWriteMode($pactFileWriteMode) - ->setLog($log) - ->setPactSpecificationVersion($pactSpecificationVersion) - ->setCors($cors); - - static::assertSame($host, $subject->getHost()); - static::assertSame($port, $subject->getPort()); - static::assertSame($provider, $subject->getProvider()); + ->setProvider($provider) + ->setDescription($description) + ->setUuid($uuid) + ->setVersion($version) + ->setBranch($branch) + ->setTag($tag) + ->setName($name) + ->setRepositoryUrl($repositoryUrl) + ->setConsumerVersion($consumerVersion) + ->setPactLocations($pactLocations); + + static::assertSame($brokerUri, $subject->getBrokerUri()); + static::assertSame($brokerToken, $subject->getBrokerToken()); + static::assertSame($brokerUsername, $subject->getBrokerUsername()); + static::assertSame($brokerPassword, $subject->getBrokerPassword()); + static::assertSame($verbose, $subject->isVerbose()); + static::assertSame($pacticipant, $subject->getPacticipant()); + static::assertSame($request, $subject->getRequest()); + static::assertSame($header, $subject->getHeader()); + static::assertSame($data, $subject->getData()); + static::assertSame($user, $subject->getUser()); + static::assertSame($url, $subject->getUrl()); static::assertSame($consumer, $subject->getConsumer()); - static::assertSame($pactDir, $subject->getPactDir()); - static::assertSame($pactFileWriteMode, $subject->getPactFileWriteMode()); - static::assertSame($log, $subject->getLog()); - static::assertSame($pactSpecificationVersion, $subject->getPactSpecificationVersion()); - static::assertSame($cors, $subject->hasCors()); + static::assertSame($provider, $subject->getProvider()); + static::assertSame($description, $subject->getDescription()); + static::assertSame($uuid, $subject->getUuid()); + static::assertSame($version, $subject->getVersion()); + static::assertSame($branch, $subject->getBranch()); + static::assertSame($tag, $subject->getTag()); + static::assertSame($name, $subject->getName()); + static::assertSame($repositoryUrl, $subject->getRepositoryUrl()); + static::assertSame($consumerVersion, $subject->getConsumerVersion()); + static::assertSame($pactLocations, $subject->getPactLocations()); } } diff --git a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php index 91efbfae..5ad4b80c 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php @@ -1,6 +1,6 @@ setHost($host) @@ -27,8 +26,7 @@ public function testSetters() ->setPactDir($pactDir) ->setPactFileWriteMode($pactFileWriteMode) ->setLog($log) - ->setPactSpecificationVersion($pactSpecificationVersion) - ->setCors($cors); + ->setPactSpecificationVersion($pactSpecificationVersion); static::assertSame($host, $subject->getHost()); static::assertSame($port, $subject->getPort()); @@ -38,6 +36,5 @@ public function testSetters() static::assertSame($pactFileWriteMode, $subject->getPactFileWriteMode()); static::assertSame($log, $subject->getLog()); static::assertSame($pactSpecificationVersion, $subject->getPactSpecificationVersion()); - static::assertSame($cors, $subject->hasCors()); } } diff --git a/tests/PhpPact/Standalone/MockServer/MockServerTest.php b/tests/PhpPact/Standalone/MockServer/MockServerTest.php deleted file mode 100644 index fe63811a..00000000 --- a/tests/PhpPact/Standalone/MockServer/MockServerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -start(); - $this->assertTrue(\is_int($pid)); - } finally { - $result = $mockServer->stop(); - $this->assertTrue($result); - } - } - - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - public function testStartAndStopWithRecognizedTimeout() - { - // the mock server actually takes more than one second to be ready - // we use this fact to test the timeout - $orig = \getenv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT'); - \putenv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT=1'); - - $httpService = $this->getMockBuilder(MockServerHttpService::class) - ->disableOriginalConstructor() - ->getMock(); - - $connectionException = $this->getMockBuilder(ConnectException::class) - ->disableOriginalConstructor() - ->getMock(); - - // take sth lower than the default value - $httpService->expects($this->atMost(5)) - ->method('healthCheck') - ->will($this->returnCallback(function () use ($connectionException) { - throw $connectionException; - })); - - try { - $mockServer = new MockServer(new MockServerEnvConfig(), $httpService); - $mockServer->start(); - $this->fail('MockServer should not pass defined health check.'); - } catch (HealthCheckFailedException $e) { - $this->assertTrue(true); - } finally { - $mockServer->stop(); - \putenv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT=' . $orig); - } - } -} diff --git a/tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php b/tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php deleted file mode 100644 index cfb86df6..00000000 --- a/tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php +++ /dev/null @@ -1,210 +0,0 @@ -config = new MockServerEnvConfig(); - $this->mockServer = new MockServer($this->config); - $this->mockServer->start(); - $this->service = new MockServerHttpService(new GuzzleClient(), $this->config); - } - - protected function tearDown(): void - { - $this->mockServer->stop(); - } - - /** - * @throws ConnectionException - */ - public function testHealthCheck() - { - $result = $this->service->healthCheck(); - $this->assertTrue($result); - } - - public function testRegisterInteraction() - { - $request = new ConsumerRequest(); - $request - ->setPath('/example') - ->setMethod('GET'); - $response = new ProviderResponse(); - $response->setStatus(200); - - $interaction = new Interaction(); - $interaction - ->setDescription('Fake description') - ->setProviderState('Fake provider state') - ->setRequest($request) - ->setResponse($response); - - $result = $this->service->registerInteraction($interaction); - - $this->assertTrue($result); - } - - public function testDeleteAllInteractions() - { - $result = $this->service->deleteAllInteractions(); - $this->assertTrue($result); - } - - public function testVerifyInteractions() - { - $result = $this->service->verifyInteractions(); - $this->assertTrue($result); - } - - public function testVerifyInteractionsFailure() - { - $request = new ConsumerRequest(); - $request - ->setPath('/example') - ->setMethod('GET'); - - $response = new ProviderResponse(); - $response->setStatus(200); - - $interaction = new Interaction(); - $interaction - ->setDescription('Some description') - ->setProviderState('Some state') - ->setRequest($request) - ->setResponse($response); - $this->service->registerInteraction($interaction); - - $this->expectException(ServerException::class); - $result = $this->service->verifyInteractions(); - $this->assertFalse($result); - } - - public function testGetPactJson() - { - $result = $this->service->getPactJson(); - $this->assertEquals('{"consumer":{"name":"someConsumer"},"provider":{"name":"someProvider"},"interactions":[],"metadata":{"pactSpecification":{"version":"2.0.0"}}}', $result); - } - - public function testFullGetInteraction() - { - $request = new ConsumerRequest(); - $request - ->setPath('/example') - ->setMethod('GET') - ->setQuery('enabled=true') - ->addQueryParameter('order', 'asc') - ->addQueryParameter('value', '12') - ->addHeader('Content-Type', 'application/json'); - - $expectedResponseBody = [ - 'message' => 'Hello, world!', - ]; - $response = new ProviderResponse(); - $response - ->setStatus(200) - ->setBody($expectedResponseBody) - ->addHeader('Content-Type', 'application/json'); - - $interaction = new Interaction(); - $interaction - ->setDescription('Fake description') - ->setProviderState('Fake provider state') - ->setRequest($request) - ->setResponse($response); - - $result = $this->service->registerInteraction($interaction); - - $this->assertTrue($result); - - $client = new GuzzleClient(); - $uri = $this->config->getBaseUri()->withPath('/example')->withQuery('enabled=true&order=asc&value=12'); - $response = $client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ]); - - $body = $response->getBody()->getContents(); - $this->assertEquals(\json_encode($expectedResponseBody), $body); - $this->assertEquals($response->getHeaderLine('Access-Control-Allow-Origin'), '*', 'CORS flag not set properly'); - $this->assertEquals(200, $response->getStatusCode()); - } - - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - public function testMatcherWithMockServer() - { - $matcher = new Matcher(); - - $category = new stdClass(); - $category->name = $matcher->term('Games', '[gbBG]'); - - $request = new ConsumerRequest(); - $request - ->setPath('/test') - ->setMethod('GET'); - - $response = new ProviderResponse(); - $response - ->setStatus(200) - ->addHeader('Content-Type', 'application/json') - ->setBody([ - 'results' => $matcher->eachLike($category), - ]); - - $config = new MockServerEnvConfig(); - $interaction = new InteractionBuilder($config); - $interaction - ->given('Something') - ->uponReceiving('Stuff') - ->with($request) - ->willRespondWith($response); - - $client = new GuzzleClient(); - $uri = $this->config->getBaseUri()->withPath('/test'); - $client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ]); - - $httpClient = new MockServerHttpService(new GuzzleClient(), $config); - - $pact = \json_decode($httpClient->getPactJson(), true); - - $this->assertArrayHasKey('$.body.results[*].name', $pact['interactions'][0]['response']['matchingRules']); - } -} diff --git a/tests/PhpPact/Standalone/PactConfigTest.php b/tests/PhpPact/Standalone/PactConfigTest.php new file mode 100644 index 00000000..aea909e2 --- /dev/null +++ b/tests/PhpPact/Standalone/PactConfigTest.php @@ -0,0 +1,65 @@ +config = new PactConfig(); + } + + public function testSetters(): void + { + $provider = 'test-provider'; + $consumer = 'test-consumer'; + $pactDir = 'test-pact-dir/'; + $pactSpecificationVersion = '2.0.0'; + $log = 'test-log-dir/'; + $logLevel = 'ERROR'; + $pactFileWriteMode = 'merge'; + + $this->config + ->setProvider($provider) + ->setConsumer($consumer) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($pactSpecificationVersion) + ->setLog($log) + ->setLogLevel($logLevel) + ->setPactFileWriteMode($pactFileWriteMode); + + static::assertSame($provider, $this->config->getProvider()); + static::assertSame($consumer, $this->config->getConsumer()); + static::assertSame($pactDir, $this->config->getPactDir()); + static::assertSame($pactSpecificationVersion, $this->config->getPactSpecificationVersion()); + static::assertSame($log, $this->config->getLog()); + static::assertSame($logLevel, $this->config->getLogLevel()); + static::assertSame($pactFileWriteMode, $this->config->getPactFileWriteMode()); + } + + public function testInvalidPactSpecificationVersion(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Invalid version string "invalid"'); + $this->config->setPactSpecificationVersion('invalid'); + } + + public function testInvalidLogLevel(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('LogLevel TRACE not supported.'); + $this->config->setLogLevel('TRACE'); + } + + public function testInvalidPactFileWriteMode(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid PhpPact File Write Mode, value must be one of the following: overwrite, merge."); + $this->config->setPactFileWriteMode('APPEND'); + } +} From e1d49cc7f58676959c288193df94bf1f60f25486 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 11 Jan 2023 20:51:56 +0700 Subject: [PATCH 002/298] Remove method BuilderInterface::writePact --- src/PhpPact/Consumer/BuilderInterface.php | 7 ------- src/PhpPact/Consumer/InteractionBuilder.php | 8 -------- 2 files changed, 15 deletions(-) diff --git a/src/PhpPact/Consumer/BuilderInterface.php b/src/PhpPact/Consumer/BuilderInterface.php index 0b58c219..406acd26 100644 --- a/src/PhpPact/Consumer/BuilderInterface.php +++ b/src/PhpPact/Consumer/BuilderInterface.php @@ -12,11 +12,4 @@ interface BuilderInterface * Verify that the interactions are valid. */ public function verify(): bool; - - /** - * Write the Pact without deleting the interactions. - * - * @return bool - */ - public function writePact(): bool; } diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 39eef2a8..55affa48 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -94,12 +94,4 @@ public function createMockServer(): void { $this->pact->createMockServer(); } - - /** - * {@inheritdoc} - */ - public function writePact(): bool - { - return true; - } } From 0d7fe0a8c0626420b1901aea33aa2c742ef579ab Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 15 Jan 2023 18:02:37 +0700 Subject: [PATCH 003/298] Remove CHANGELOG.md. Add '@internal' to classes are not supposed to be used directly --- CHANGELOG.md | 61 ------------------- .../Consumer/Listener/PactTestListener.php | 2 + .../Standalone/Installer/Model/Scripts.php | 2 + 3 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6f130920..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,61 +0,0 @@ -CHANGELOG -========= - -9.0 ---- - -* General - * [BC BREAK] Declare types for: - * Properties - * Arguments - * Return values - -* Matchers - * [BC BREAK] Update matcher implementations - -* Installers - * [BC BREAK] Removed `PhpPact\Standalone\Installer\Model\Scripts::getMockService` - * Added `PhpPact\Standalone\Installer\Model\Scripts::getCode` - * Added `PhpPact\Standalone\Installer\Model\Scripts::getLibrary` - -* Config - * [BC BREAK] Updated `PhpPact\Standalone\MockService\MockServerConfigInterface`, removed these methods: - * `hasCors` - * `setCors` - * `setHealthCheckTimeout` - * `getHealthCheckTimeout` - * `setHealthCheckRetrySec` - * `getHealthCheckRetrySec` - * [BC BREAK] Removed environments variables: - * PACT_CORS - * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT - * PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC - * Moved these methods from `PhpPact\Standalone\MockService\MockServerConfigInterface` to `PhpPact\Standalone\PactConfigInterface`: - * `getPactFileWriteMode` - * `setPactFileWriteMode` - * `PhpPact\Standalone\MockService\MockServerConfigInterface` now extends `PhpPact\Standalone\PactConfigInterface` - * Added `PhpPact\Standalone\PactConfig` - * `PhpPact\Standalone\PactMessage\PactMessageConfig` now extends `PhpPact\Standalone\PactConfig` - * `PhpPact\Standalone\MockService\MockServerConfig` now extends `PhpPact\Standalone\PactConfig` - * Change default specification version to `3.0.0` - -* Mock Server - * [BC BREAK] Removed `PhpPact\Standalone\MockService\MockServer` - * [BC BREAK] Removed `PhpPact\Standalone\MockService\Service\MockServerHttpService` - * [BC BREAK] Removed `PhpPact\Standalone\MockService\Service\MockServerHttpServiceInterface` - * [BC BREAK] Removed `PhpPact\Standalone\Exception\HealthCheckFailedException` - * Added `PhpPact\Consumer\Exception\MockServerNotStartedException` - -* Interaction Builder - * Added `PhpPact\Consumer\InteractionBuilder::createMockServer` - * [BC BREAK] It's now required to call `PhpPact\Consumer\InteractionBuilder::createMockServer` manually before `PhpPact\Consumer\InteractionBuilder::verify` - * [BC BREAK] Removed `PhpPact\Consumer\InteractionBuilder::finalize` - * [BC BREAK] `PhpPact\Consumer\Model\ConsumerRequest::getQuery` now return `array` instead of `string` - * [BC BREAK] `PhpPact\Consumer\Model\ConsumerRequest::setQuery` now accept argument `array` instead of `string` - * Added `PhpPact\Consumer\Exception\InteractionRequestBodyNotAddedException` - * Added `PhpPact\Consumer\Exception\InteractionResponseBodyNotAddedException` - * Added `PhpPact\Consumer\Exception\PactFileNotWroteException` - -* PHPUnit - * [BC BREAK] Removed `PhpPact\Consumer\Hook\ContractDownloader` - * Replace broker http client by broker cli in `PhpPact\Consumer\Listener\PactTestListener` diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php index af66e11f..6e25ccbf 100644 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ b/src/PhpPact/Consumer/Listener/PactTestListener.php @@ -16,6 +16,8 @@ /** * PACT listener that can be used with environment variables and easily attached to PHPUnit configuration. * Class PactTestListener + * + * @internal */ class PactTestListener implements TestListener { diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index fb4a173b..7bfc3e29 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -5,6 +5,8 @@ /** * Represents locations of Ruby Standalone full path and scripts. * Class Scripts. + * + * @internal */ class Scripts { From 0812b31031b2a0876ff1e9bfc7b0e30639733a0a Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 15 Jan 2023 18:06:01 +0700 Subject: [PATCH 004/298] Add missing return type to method Matcher::regex --- src/PhpPact/Consumer/Matcher/Matcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 1fed12cc..e8e9cf42 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -107,7 +107,7 @@ public function term($value, string $pattern): array * * @return array */ - public function regex($value, string $pattern) + public function regex($value, string $pattern): array { return $this->term($value, $pattern); } From cf5144b55c21b8349a4cf21a1980ef962e34a71c Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 15 Jan 2023 18:05:23 +0700 Subject: [PATCH 005/298] Add more details to UPGRADE-9.0.md --- UPGRADE-9.0.md | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md index c1628bf4..53672228 100644 --- a/UPGRADE-9.0.md +++ b/UPGRADE-9.0.md @@ -4,17 +4,22 @@ UPGRADE FROM 8.x to 9.0 * Interaction Builder * It's now required to call `PhpPact\Consumer\InteractionBuilder::createMockServer` manually - Example Usage: - ```php - $builder = new InteractionBuilder($config); - $builder - ->given('a person exists') - ->uponReceiving('a get request to /hello/{name}') - ->with($request) - ->willRespondWith($response); - $builder->createMockServer(); + Example Usage: + ```php + $builder = new InteractionBuilder($config); + $builder + ->given('a person exists') + ->uponReceiving('a get request to /hello/{name}') + ->with($request) + ->willRespondWith($response); + $builder->createMockServer(); - $apiClient->sendRequest(); + $apiClient->sendRequest(); - $this->assertTrue($builder->verify()); - ``` + $this->assertTrue($builder->verify()); + ``` + + * These environment variables can be removed: + * PACT_CORS + * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT + * PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC From d20f1f51c040b0a1ea6ffccac725d6e25feb7e05 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 16 Jan 2023 09:32:33 +0700 Subject: [PATCH 006/298] Revert multiple values support in header and query parameter --- .../Consumer/Model/ConsumerRequest.php | 24 ++++++++++++------- .../Consumer/Model/ProviderResponse.php | 13 ++++++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 784fd264..233ee646 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -88,20 +88,23 @@ public function getHeaders(): array */ public function setHeaders(array $headers): self { - $this->headers = $headers; + $this->headers = []; + foreach ($headers as $header => $value) { + $this->addHeader($header, $value); + } return $this; } /** - * @param string $header - * @param array|string $value + * @param string $header + * @param string $value * * @return ConsumerRequest */ - public function addHeader(string $header, array|string $value): self + public function addHeader(string $header, string $value): self { - $this->headers[$header] = is_array($value) ? json_encode($value) : $value; + $this->headers[$header] = $value; return $this; } @@ -148,20 +151,23 @@ public function getQuery(): array */ public function setQuery(array $query): self { - $this->query = $query; + $this->query = []; + foreach ($query as $key => $value) { + $this->addQueryParameter($key, $value); + } return $this; } /** * @param string $key - * @param array|string $value + * @param string $value * * @return ConsumerRequest */ - public function addQueryParameter(string $key, array|string $value): self + public function addQueryParameter(string $key, string $value): self { - $this->query[$key] = is_array($value) ? json_encode($value) : $value; + $this->query[$key] = $value; return $this; } diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 449f5dac..4e2a612c 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -58,20 +58,23 @@ public function getHeaders(): array */ public function setHeaders(array $headers): self { - $this->headers = $headers; + $this->headers = []; + foreach ($headers as $header => $value) { + $this->addHeader($header, $value); + } return $this; } /** - * @param string $header - * @param array|string $value + * @param string $header + * @param string $value * * @return ProviderResponse */ - public function addHeader(string $header, array|string $value): self + public function addHeader(string $header, string $value): self { - $this->headers[$header] = is_array($value) ? json_encode($value) : $value; + $this->headers[$header] = $value; return $this; } From bb256ef552c8d98860d601002a5382563130f237 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 1 Mar 2023 23:08:22 +0700 Subject: [PATCH 007/298] Support multiple values in header and query parameter --- src/PhpPact/Consumer/Model/ConsumerRequest.php | 16 ++++++++-------- src/PhpPact/Consumer/Model/Pact.php | 18 ++++++++++++------ .../Consumer/Model/ProviderResponse.php | 8 ++++---- .../Consumer/Model/ConsumerRequestTest.php | 8 ++++++-- .../Consumer/Model/ProviderResponseTest.php | 2 +- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 233ee646..f0561145 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -97,14 +97,14 @@ public function setHeaders(array $headers): self } /** - * @param string $header - * @param string $value + * @param string $header + * @param array|string $value * * @return ConsumerRequest */ - public function addHeader(string $header, string $value): self + public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = $value; + $this->headers[$header] = is_array($value) ? $value : [$value]; return $this; } @@ -160,14 +160,14 @@ public function setQuery(array $query): self } /** - * @param string $key - * @param string $value + * @param string $key + * @param array|string $value * * @return ConsumerRequest */ - public function addQueryParameter(string $key, string $value): self + public function addQueryParameter(string $key, array|string $value): self { - $this->query[$key] = $value; + $this->query[$key] = is_array($value) ? $value : [$value]; return $this; } diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 84d55223..997eee43 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -147,11 +147,15 @@ private function with(Interaction $interaction): self $id = $interaction->getId(); $request = $interaction->getRequest(); $this->ffi->pactffi_with_request($id, $request->getMethod(), $request->getPath()); - foreach ($request->getHeaders() as $header => $value) { - $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Request, $header, 0, $value); + foreach ($request->getHeaders() as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Request, $header, $index, $value); + } } - foreach ($request->getQuery() as $key => $value) { - $this->ffi->pactffi_with_query_parameter_v2($id, $key, 0, $value); + foreach ($request->getQuery() as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_query_parameter_v2($id, $key, $index, $value); + } } if (!\is_null($request->getBody())) { $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Request, null, $request->getBody()); @@ -168,8 +172,10 @@ private function willRespondWith(Interaction $interaction): self $id = $interaction->getId(); $response = $interaction->getResponse(); $this->ffi->pactffi_response_status($id, $response->getStatus()); - foreach ($response->getHeaders() as $header => $value) { - $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Response, $header, 0, $value); + foreach ($response->getHeaders() as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Response, $header, $index, $value); + } } if (!\is_null($response->getBody())) { $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Response, null, $response->getBody()); diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 4e2a612c..d3237e40 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -67,14 +67,14 @@ public function setHeaders(array $headers): self } /** - * @param string $header - * @param string $value + * @param string $header + * @param array|string $value * * @return ProviderResponse */ - public function addHeader(string $header, string $value): self + public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = $value; + $this->headers[$header] = is_array($value) ? $value : [$value]; return $this; } diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 5a4723a7..f30aeca7 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -15,12 +15,14 @@ public function testSerializing() ->setMethod('PUT') ->setPath('/somepath') ->addHeader('Content-Type', 'application/json') + ->addQueryParameter('fruit', ['apple', 'banana']) ->setBody([ 'currentCity' => 'Austin', ]); $this->assertEquals('PUT', $model->getMethod()); - $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); + $this->assertEquals(['fruit' => ['apple', 'banana']], $model->getQuery()); $this->assertEquals('/somepath', $model->getPath()); $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); } @@ -34,12 +36,14 @@ public function testSerializingWhenPathUsingMatcher() ->setMethod('PATCH') ->setPath($matcher->regex("/somepath/$pathVariable/status", '\/somepath\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\/status')) ->addHeader('Content-Type', 'application/json') + ->addQueryParameter('food', 'milk') ->setBody([ 'status' => 'finished', ]); $this->assertEquals('PATCH', $model->getMethod()); - $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); + $this->assertEquals(['food' => ['milk']], $model->getQuery()); $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); $this->assertEquals('{"status":"finished"}', $model->getBody()); } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 0e2628da..2da664be 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -18,7 +18,7 @@ public function testSerializing() ]); $this->assertEquals(200, $model->getStatus()); - $this->assertEquals(['Content-Type' => 'application/json'], $model->getHeaders()); + $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); } } From d6a9d0dc943bcfd11abb1da76f5c67408dfc6a1d Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 1 Mar 2023 23:43:18 +0700 Subject: [PATCH 008/298] Update pact ffi library --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ca0ce487..cfef1684 100644 --- a/composer.json +++ b/composer.json @@ -89,12 +89,12 @@ "path": "bin/pact-ruby-standalone" }, "pact-ffi-headers": { - "version": "0.3.19", + "version": "0.4.1", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.3.19", + "version": "0.4.1", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From ca46297a025e89e99a0312f3b64f03421178ed86 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 2 Mar 2023 10:30:01 +0700 Subject: [PATCH 009/298] Rename method getCode to getHeader --- src/PhpPact/Consumer/Model/AbstractPact.php | 2 +- src/PhpPact/Standalone/Installer/Model/Scripts.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Model/AbstractPact.php b/src/PhpPact/Consumer/Model/AbstractPact.php index 088f2160..a8abca50 100644 --- a/src/PhpPact/Consumer/Model/AbstractPact.php +++ b/src/PhpPact/Consumer/Model/AbstractPact.php @@ -15,6 +15,6 @@ abstract class AbstractPact public function __construct() { - $this->ffi = FFI::cdef(\file_get_contents(Scripts::getCode()), Scripts::getLibrary()); + $this->ffi = FFI::cdef(\file_get_contents(Scripts::getHeader()), Scripts::getLibrary()); } } diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 7bfc3e29..b6bcae11 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -20,7 +20,7 @@ class Scripts /** * @return string */ - public static function getCode(): string + public static function getHeader(): string { return self::$destinationDir . '/bin/pact-ffi-headers/pact.h'; } From 39718b9ca5ee7782b451ef2d2474fca3d3d26b1e Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 12 Mar 2023 14:42:54 +0700 Subject: [PATCH 010/298] Revert breaking change that require to call createMockServer manually --- README.md | 10 +--------- UPGRADE-9.0.md | 19 +------------------ .../Service/ConsumerServiceGoodbyeTest.php | 1 - .../Service/ConsumerServiceHelloTest.php | 3 +-- src/PhpPact/Consumer/InteractionBuilder.php | 8 -------- src/PhpPact/Consumer/Model/Pact.php | 5 +++-- .../Consumer/InteractionBuilderTest.php | 12 ++++++------ 7 files changed, 12 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3fb015bd..bbd0e008 100644 --- a/README.md +++ b/README.md @@ -175,15 +175,7 @@ $builder ->given('a person exists') ->uponReceiving('a get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. -``` - -### Start the Mock Server - -Mock server need to be started manually - -```php -$builder->createMockServer(); + ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. ``` ### Make the Request diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md index 53672228..0285dbe7 100644 --- a/UPGRADE-9.0.md +++ b/UPGRADE-9.0.md @@ -1,24 +1,7 @@ UPGRADE FROM 8.x to 9.0 ======================= -* Interaction Builder - * It's now required to call `PhpPact\Consumer\InteractionBuilder::createMockServer` manually - - Example Usage: - ```php - $builder = new InteractionBuilder($config); - $builder - ->given('a person exists') - ->uponReceiving('a get request to /hello/{name}') - ->with($request) - ->willRespondWith($response); - $builder->createMockServer(); - - $apiClient->sendRequest(); - - $this->assertTrue($builder->verify()); - ``` - +* Environment Variables * These environment variables can be removed: * PACT_CORS * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT diff --git a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php index bc840bb2..fcae2e02 100644 --- a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php @@ -37,7 +37,6 @@ public function testGetGoodbyeString() ->uponReceiving('A get request to /goodbye/{name}') ->with($request) ->willRespondWith($response); - $builder->createMockServer(); $service = new HttpClientService($config->getBaseUri()); $result = $service->getGoodbyeString('Bob'); diff --git a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php index d23df42c..73b6b6f2 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php @@ -42,8 +42,7 @@ public function testGetHelloString() $builder ->uponReceiving('A get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes a FFI call to the Mock Server to set the interaction. - $builder->createMockServer(); + ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. $service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. $result = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 55affa48..a59696f8 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -86,12 +86,4 @@ public function verify(): bool { return $this->pact->verifyInteractions(); } - - /** - * Create mock server before verifying. - */ - public function createMockServer(): void - { - $this->pact->createMockServer(); - } } diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 997eee43..0faf146f 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -27,7 +27,7 @@ public function __construct(private MockServerConfigInterface $config) ->withSpecification(); } - public function createMockServer(): void + private function createMockServer(): void { $port = $this->ffi->pactffi_create_mock_server_for_transport( $this->id, @@ -83,7 +83,8 @@ public function registerInteraction(Interaction $interaction): bool ->given($interaction) ->uponReceiving($interaction) ->with($interaction) - ->willRespondWith($interaction); + ->willRespondWith($interaction) + ->createMockServer(); return true; } diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index 33eaa902..713a2f15 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -36,13 +36,13 @@ public function testSimpleGet() ->addHeader('Content-Type', 'application/json'); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder + $builder ->given('A test request.') ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $this->assertFalse($builder->verify()); } /** @@ -74,13 +74,13 @@ public function testPostWithBody() ]); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder + $builder ->given('A test request.') ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $this->assertFalse($builder->verify()); } /** @@ -108,12 +108,12 @@ public function testBuildWithEachLikeMatcher() ]); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder + $builder ->given('A test request.') ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $this->assertFalse($builder->verify()); } } From ddc46e5965f29e2eadcc8221c69e3339e2181833 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 2 Mar 2023 11:03:21 +0700 Subject: [PATCH 011/298] Remove composer/semver --- composer.json | 1 - src/PhpPact/Consumer/Model/Pact.php | 10 ++++++++-- src/PhpPact/Standalone/PactConfig.php | 10 ---------- src/PhpPact/Standalone/PactConfigInterface.php | 2 +- tests/PhpPact/Standalone/PactConfigTest.php | 7 ------- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index cfef1684..c3595cc3 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,6 @@ "php": "^8.0", "ext-openssl": "*", "ext-json": "*", - "composer/semver": "^1.4.0|^3.2.0", "amphp/amp": "^2.5.1", "amphp/byte-stream": "^1.8", "amphp/dns": "^1.2.3", diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 0faf146f..44ebbd5b 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -115,8 +115,14 @@ private function withSpecification(): self '3.0.0' => $this->ffi->PactSpecification_V3, '4.0.0' => $this->ffi->PactSpecification_V4, ]; - $version = $supportedVersions[$this->config->getPactSpecificationVersion()] ?? $this->ffi->PactSpecification_Unknown; - $this->ffi->pactffi_with_specification($this->id, $version); + $version = $this->config->getPactSpecificationVersion(); + if (isset($supportedVersions[$version])) { + $specification = $supportedVersions[$version]; + } else { + trigger_error(sprintf("Specification version '%s' is unknown", $version), E_USER_WARNING); + $specification = $this->ffi->PactSpecification_Unknown; + } + $this->ffi->pactffi_with_specification($this->id, $specification); return $this; } diff --git a/src/PhpPact/Standalone/PactConfig.php b/src/PhpPact/Standalone/PactConfig.php index e5a526f9..a7b79b1d 100644 --- a/src/PhpPact/Standalone/PactConfig.php +++ b/src/PhpPact/Standalone/PactConfig.php @@ -2,8 +2,6 @@ namespace PhpPact\Standalone; -use Composer\Semver\VersionParser; - /** * Class PactConfig. */ @@ -135,17 +133,9 @@ public function getPactSpecificationVersion(): string /** * {@inheritdoc} - * - * @throws \UnexpectedValueException */ public function setPactSpecificationVersion(string $pactSpecificationVersion): self { - /* - * Parse the version but do not assign it. If it is an invalid version, an exception is thrown - */ - $parser = new VersionParser(); - $parser->normalize($pactSpecificationVersion); - $this->pactSpecificationVersion = $pactSpecificationVersion; return $this; diff --git a/src/PhpPact/Standalone/PactConfigInterface.php b/src/PhpPact/Standalone/PactConfigInterface.php index b1d21344..bcd37e24 100644 --- a/src/PhpPact/Standalone/PactConfigInterface.php +++ b/src/PhpPact/Standalone/PactConfigInterface.php @@ -53,7 +53,7 @@ public function setPactDir(?string $pactDir): self; public function getPactSpecificationVersion(): string; /** - * @param string $pactSpecificationVersion pact semver version + * @param string $pactSpecificationVersion pact version * * @return $this */ diff --git a/tests/PhpPact/Standalone/PactConfigTest.php b/tests/PhpPact/Standalone/PactConfigTest.php index 42db4851..ba374d4c 100644 --- a/tests/PhpPact/Standalone/PactConfigTest.php +++ b/tests/PhpPact/Standalone/PactConfigTest.php @@ -43,13 +43,6 @@ public function testSetters(): void static::assertSame($pactFileWriteMode, $this->config->getPactFileWriteMode()); } - public function testInvalidPactSpecificationVersion(): void - { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Invalid version string "invalid"'); - $this->config->setPactSpecificationVersion('invalid'); - } - public function testInvalidLogLevel(): void { $this->expectException(\InvalidArgumentException::class); From 5dae03ae6bb8022cb4c57ffb106a11805f62992f Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 14 Mar 2023 16:23:33 +0700 Subject: [PATCH 012/298] Make sure every value of header and query parameter is string --- .../Consumer/Model/ConsumerRequest.php | 32 +++++++++++++++---- .../Consumer/Model/ProviderResponse.php | 16 ++++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index f0561145..ae6f8daa 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -19,7 +19,7 @@ class ConsumerRequest implements \JsonSerializable private string $path; /** - * @var string[] + * @var array */ private array $headers = []; @@ -29,7 +29,7 @@ class ConsumerRequest implements \JsonSerializable private ?string $body = null; /** - * @var array + * @var array */ private array $query = []; @@ -74,7 +74,7 @@ public function setPath(array|string $path): self } /** - * @return array + * @return array */ public function getHeaders(): array { @@ -104,11 +104,21 @@ public function setHeaders(array $headers): self */ public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = is_array($value) ? $value : [$value]; + $this->headers[$header] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); + } else { + $this->addHeaderValue($header, $value); + } return $this; } + private function addHeaderValue(string $header, string $value): void + { + $this->headers[$header][] = $value; + } + /** * @return null|string */ @@ -137,7 +147,7 @@ public function setBody(mixed $body): self } /** - * @return array + * @return array */ public function getQuery(): array { @@ -167,11 +177,21 @@ public function setQuery(array $query): self */ public function addQueryParameter(string $key, array|string $value): self { - $this->query[$key] = is_array($value) ? $value : [$value]; + $this->query[$key] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addQueryParameterValue($key, $value)); + } else { + $this->addQueryParameterValue($key, $value); + } return $this; } + private function addQueryParameterValue(string $key, string $value): void + { + $this->query[$key][] = $value; + } + /** * {@inheritdoc} */ diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index d3237e40..6c3f95e4 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -14,7 +14,7 @@ class ProviderResponse implements \JsonSerializable private int $status; /** - * @var string[] + * @var array */ private array $headers = []; @@ -44,7 +44,7 @@ public function setStatus(int $status): self } /** - * @return string[] + * @return array */ public function getHeaders(): array { @@ -74,11 +74,21 @@ public function setHeaders(array $headers): self */ public function addHeader(string $header, array|string $value): self { - $this->headers[$header] = is_array($value) ? $value : [$value]; + $this->headers[$header] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); + } else { + $this->addHeaderValue($header, $value); + } return $this; } + private function addHeaderValue(string $header, string $value): void + { + $this->headers[$header][] = $value; + } + /** * @return null|string */ From 52e9a998542e0c1b19e1671da6913f64e628eaf5 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 14 Mar 2023 16:35:16 +0700 Subject: [PATCH 013/298] Move exception message to exception --- .../Exception/MockServerNotStartedException.php | 12 ++++++++++++ .../Exception/PactFileNotWroteException.php | 10 ++++++++++ src/PhpPact/Consumer/Model/Pact.php | 16 ++-------------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php b/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php index 68214f53..bbe54129 100644 --- a/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php +++ b/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php @@ -9,4 +9,16 @@ */ class MockServerNotStartedException extends Exception { + public function __construct(int $code) + { + $message = match ($code) { + -1 => 'An invalid handle was received. Handles should be created with `pactffi_new_pact`', + -2 => 'Transport_config is not valid JSON', + -3 => 'The mock server could not be started', + -4 => 'The method panicked', + -5 => 'The address is not valid', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } } diff --git a/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php b/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php index 8a30bc4b..09129687 100644 --- a/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php +++ b/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php @@ -9,4 +9,14 @@ */ class PactFileNotWroteException extends Exception { + public function __construct(int $code) + { + $message = match ($code) { + 1 => 'A general panic was caught', + 2 => 'The pact file was not able to be written', + 3 => 'A mock server with the provided port was not found', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } } diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 44ebbd5b..433a9a42 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -38,14 +38,7 @@ private function createMockServer(): void ); if ($port < 0) { - $message = match ($port) { - -1 => 'An invalid handle was received. Handles should be created with `pactffi_new_pact`', - -2 => 'Transport_config is not valid JSON', - -3 => 'The mock server could not be started', - -4 => 'The method panicked', - -5 => 'The address is not valid', - }; - throw new MockServerNotStartedException($message); + throw new MockServerNotStartedException($port); } $this->config->setPort($port); } @@ -61,12 +54,7 @@ public function verifyInteractions(): bool $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); if ($error) { - $message = match ($error) { - 1 => 'A general panic was caught', - 2 => 'The pact file was not able to be written', - 3 => 'A mock server with the provided port was not found', - }; - throw new PactFileNotWroteException($message); + throw new PactFileNotWroteException($error); } } From bff0f9b7fae622a22aadcebe20e1c7263c2c4baa Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 14 Mar 2023 16:43:51 +0700 Subject: [PATCH 014/298] Extract method getSpecification() to check for supporting plugin --- src/PhpPact/Consumer/Model/Pact.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 433a9a42..eee35750 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -94,7 +94,7 @@ private function newPact(): self return $this; } - private function withSpecification(): self + protected function getSpecification(): int { $supportedVersions = [ '1.0.0' => $this->ffi->PactSpecification_V1, @@ -110,7 +110,13 @@ private function withSpecification(): self trigger_error(sprintf("Specification version '%s' is unknown", $version), E_USER_WARNING); $specification = $this->ffi->PactSpecification_Unknown; } - $this->ffi->pactffi_with_specification($this->id, $specification); + + return $specification; + } + + private function withSpecification(): self + { + $this->ffi->pactffi_with_specification($this->id, $this->getSpecification()); return $this; } From 312c45cef0e1579aa171eac8dbd7833b357d1ed4 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 14 Mar 2023 23:22:42 +0700 Subject: [PATCH 015/298] Extract, change visibility, and move methods to abstract class to support plugin --- phpstan.neon | 1 + .../InteractionBodyNotAddedException.php | 12 ++ ...nteractionRequestBodyNotAddedException.php | 12 -- ...teractionResponseBodyNotAddedException.php | 12 -- .../Exception/PactFileNotWroteException.php | 6 +- src/PhpPact/Consumer/Model/AbstractPact.php | 82 ++++++++++- src/PhpPact/Consumer/Model/Pact.php | 128 +++++------------- .../MockService/MockServerEnvConfig.php | 2 - src/PhpPact/Standalone/PactConfig.php | 2 +- .../Standalone/PactConfigInterface.php | 2 + 10 files changed, 137 insertions(+), 122 deletions(-) create mode 100644 src/PhpPact/Consumer/Exception/InteractionBodyNotAddedException.php delete mode 100644 src/PhpPact/Consumer/Exception/InteractionRequestBodyNotAddedException.php delete mode 100644 src/PhpPact/Consumer/Exception/InteractionResponseBodyNotAddedException.php diff --git a/phpstan.neon b/phpstan.neon index e0f2f0e8..2704610d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,4 @@ parameters: excludePaths: + - src/PhpPact/Consumer/Model/AbstractPact.php - src/PhpPact/Consumer/Model/Pact.php diff --git a/src/PhpPact/Consumer/Exception/InteractionBodyNotAddedException.php b/src/PhpPact/Consumer/Exception/InteractionBodyNotAddedException.php new file mode 100644 index 00000000..8e36ae3a --- /dev/null +++ b/src/PhpPact/Consumer/Exception/InteractionBodyNotAddedException.php @@ -0,0 +1,12 @@ + 'A general panic was caught', - 2 => 'The pact file was not able to be written', - 3 => 'A mock server with the provided port was not found', + 1 => 'The function panicked.', + 2 => 'The pact file was not able to be written.', + 3 => 'The pact for the given handle was not found.', default => 'Unknown error', }; parent::__construct($message, $code); diff --git a/src/PhpPact/Consumer/Model/AbstractPact.php b/src/PhpPact/Consumer/Model/AbstractPact.php index a8abca50..564bc3c4 100644 --- a/src/PhpPact/Consumer/Model/AbstractPact.php +++ b/src/PhpPact/Consumer/Model/AbstractPact.php @@ -4,6 +4,9 @@ use FFI; use PhpPact\Standalone\Installer\Model\Scripts; +use PhpPact\Standalone\PactConfigInterface; +use PhpPact\Consumer\Exception\PactFileNotWroteException; +use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; /** * Class AbstractPact. @@ -13,8 +16,85 @@ abstract class AbstractPact protected FFI $ffi; protected int $id; - public function __construct() + public function __construct(protected PactConfigInterface $config) + { + $this + ->createFfi() + ->newPact() + ->withSpecification(); + } + + private function createFfi(): self { $this->ffi = FFI::cdef(\file_get_contents(Scripts::getHeader()), Scripts::getLibrary()); + + return $this; + } + + private function newPact(): self + { + $this->id = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); + + return $this; + } + + protected function getSpecification(): int + { + $supportedVersions = [ + '1.0.0' => $this->ffi->PactSpecification_V1, + '1.1.0' => $this->ffi->PactSpecification_V1_1, + '2.0.0' => $this->ffi->PactSpecification_V2, + '3.0.0' => $this->ffi->PactSpecification_V3, + '4.0.0' => $this->ffi->PactSpecification_V4, + ]; + $version = $this->config->getPactSpecificationVersion(); + if (isset($supportedVersions[$version])) { + $specification = $supportedVersions[$version]; + } else { + trigger_error(sprintf("Specification version '%s' is unknown", $version), E_USER_WARNING); + $specification = $this->ffi->PactSpecification_Unknown; + } + + return $specification; + } + + private function withSpecification(): self + { + $this->ffi->pactffi_with_specification($this->id, $this->getSpecification()); + + return $this; + } + + protected function newInteraction(?string $description): int + { + return $this->ffi->pactffi_new_interaction($this->id, $description); + } + + protected function cleanUp(): void + { + $this->ffi->pactffi_free_pact_handle($this->id); + } + + protected function writePact(): void + { + $error = $this->ffi->pactffi_pact_handle_write_file( + $this->id, + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new PactFileNotWroteException($error); + } + } + + protected function withBody(int $interaction, int $part, ?string $contentType = null, ?string $body = null): void + { + if (is_null($body)) { + return; + } + $success = $this->ffi->pactffi_with_body($interaction, $part, $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } } } diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index eee35750..19f5122b 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -2,13 +2,8 @@ namespace PhpPact\Consumer\Model; -use FFI; -use PhpPact\Consumer\Exception\InteractionRequestBodyNotAddedException; -use PhpPact\Consumer\Exception\InteractionResponseBodyNotAddedException; use PhpPact\Consumer\Exception\MockServerNotStartedException; -use PhpPact\Consumer\Exception\PactFileNotWroteException; use PhpPact\Standalone\MockService\MockServerConfigInterface; -use PhpPact\Standalone\PactConfigInterface; /** * Class Pact. @@ -18,13 +13,10 @@ class Pact extends AbstractPact /** * @param MockServerConfigInterface $config */ - public function __construct(private MockServerConfigInterface $config) + public function __construct(MockServerConfigInterface $config) { - parent::__construct(); - $this - ->initWithLogLevel() - ->newPact() - ->withSpecification(); + parent::__construct($config); + $this->initWithLogLevel(); } private function createMockServer(): void @@ -45,29 +37,29 @@ private function createMockServer(): void public function verifyInteractions(): bool { - $result = $this->ffi->pactffi_mock_server_matched($this->config->getPort()); - - if ($result) { - $error = $this->ffi->pactffi_write_pact_file( - $this->config->getPort(), - $this->config->getPactDir(), - $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE - ); - if ($error) { - throw new PactFileNotWroteException($error); + $matched = $this->ffi->pactffi_mock_server_matched($this->config->getPort()); + + try { + if ($matched) { + $this->writePact(); } + } finally { + $this->cleanUp(); } - $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); - $this->ffi->pactffi_free_pact_handle($this->id); + return $matched; + } - return $result; + protected function cleanUp(): void + { + parent::cleanUp(); + $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); } public function registerInteraction(Interaction $interaction): bool { + $interaction->setId($this->newInteraction($interaction->getDescription())); $this - ->newInteraction($interaction) ->given($interaction) ->uponReceiving($interaction) ->with($interaction) @@ -87,48 +79,6 @@ private function initWithLogLevel(): self return $this; } - private function newPact(): self - { - $this->id = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); - - return $this; - } - - protected function getSpecification(): int - { - $supportedVersions = [ - '1.0.0' => $this->ffi->PactSpecification_V1, - '1.1.0' => $this->ffi->PactSpecification_V1_1, - '2.0.0' => $this->ffi->PactSpecification_V2, - '3.0.0' => $this->ffi->PactSpecification_V3, - '4.0.0' => $this->ffi->PactSpecification_V4, - ]; - $version = $this->config->getPactSpecificationVersion(); - if (isset($supportedVersions[$version])) { - $specification = $supportedVersions[$version]; - } else { - trigger_error(sprintf("Specification version '%s' is unknown", $version), E_USER_WARNING); - $specification = $this->ffi->PactSpecification_Unknown; - } - - return $specification; - } - - private function withSpecification(): self - { - $this->ffi->pactffi_with_specification($this->id, $this->getSpecification()); - - return $this; - } - - private function newInteraction(Interaction $interaction): self - { - $id = $this->ffi->pactffi_new_interaction($this->id, $interaction->getDescription()); - $interaction->setId($id); - - return $this; - } - private function given(Interaction $interaction): self { $this->ffi->pactffi_given($interaction->getId(), $interaction->getProviderState()); @@ -148,22 +98,9 @@ private function with(Interaction $interaction): self $id = $interaction->getId(); $request = $interaction->getRequest(); $this->ffi->pactffi_with_request($id, $request->getMethod(), $request->getPath()); - foreach ($request->getHeaders() as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Request, $header, $index, $value); - } - } - foreach ($request->getQuery() as $key => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_query_parameter_v2($id, $key, $index, $value); - } - } - if (!\is_null($request->getBody())) { - $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Request, null, $request->getBody()); - if (!$success) { - throw new InteractionRequestBodyNotAddedException(); - } - } + $this->withHeaders($id, $this->ffi->InteractionPart_Request, $request->getHeaders()); + $this->withQuery($id, $request->getQuery()); + $this->withBody($id, $this->ffi->InteractionPart_Request, null, $request->getBody()); return $this; } @@ -173,18 +110,27 @@ private function willRespondWith(Interaction $interaction): self $id = $interaction->getId(); $response = $interaction->getResponse(); $this->ffi->pactffi_response_status($id, $response->getStatus()); - foreach ($response->getHeaders() as $header => $values) { + $this->withHeaders($id, $this->ffi->InteractionPart_Response, $response->getHeaders()); + $this->withBody($id, $this->ffi->InteractionPart_Response, null, $response->getBody()); + + return $this; + } + + private function withHeaders(int $interaction, int $part, array $headers): void + { + foreach ($headers as $header => $values) { foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_header_v2($id, $this->ffi->InteractionPart_Response, $header, $index, $value); + $this->ffi->pactffi_with_header_v2($interaction, $part, (string) $header, (int) $index, (string) $value); } } - if (!\is_null($response->getBody())) { - $success = $this->ffi->pactffi_with_body($id, $this->ffi->InteractionPart_Response, null, $response->getBody()); - if (!$success) { - throw new InteractionResponseBodyNotAddedException(); + } + + private function withQuery(int $interaction, array $query): void + { + foreach ($query as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_query_parameter_v2($interaction, (string) $key, (int) $index, (string) $value); } } - - return $this; } } diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index 1f11a8a8..f7f74a81 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -10,8 +10,6 @@ */ class MockServerEnvConfig extends MockServerConfig { - public const DEFAULT_SPECIFICATION_VERSION = '3.0.0'; - /** * MockServerEnvConfig constructor. * diff --git a/src/PhpPact/Standalone/PactConfig.php b/src/PhpPact/Standalone/PactConfig.php index a7b79b1d..fc0cee8a 100644 --- a/src/PhpPact/Standalone/PactConfig.php +++ b/src/PhpPact/Standalone/PactConfig.php @@ -33,7 +33,7 @@ class PactConfig implements PactConfigInterface * * @var string */ - private string $pactSpecificationVersion; + private string $pactSpecificationVersion = self::DEFAULT_SPECIFICATION_VERSION; /** * File to which to log output. diff --git a/src/PhpPact/Standalone/PactConfigInterface.php b/src/PhpPact/Standalone/PactConfigInterface.php index bcd37e24..e2febd82 100644 --- a/src/PhpPact/Standalone/PactConfigInterface.php +++ b/src/PhpPact/Standalone/PactConfigInterface.php @@ -8,6 +8,8 @@ */ interface PactConfigInterface { + public const DEFAULT_SPECIFICATION_VERSION = '3.0.0'; + public const MODE_OVERWRITE = 'overwrite'; public const MODE_MERGE = 'merge'; From e7675e6a6dffb28326cf8bcccee0b92d57158d70 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 7 Mar 2023 23:22:22 +0700 Subject: [PATCH 016/298] Use Rust FFI: Support multiple provider states with params for interaction --- README.md | 2 +- src/PhpPact/Consumer/InteractionBuilder.php | 6 +- src/PhpPact/Consumer/Model/Interaction.php | 27 +-------- src/PhpPact/Consumer/Model/Message.php | 51 +---------------- src/PhpPact/Consumer/Model/Pact.php | 6 +- src/PhpPact/Consumer/Model/ProviderState.php | 40 ++++++++++++++ src/PhpPact/Consumer/Model/ProviderStates.php | 55 +++++++++++++++++++ .../Consumer/InteractionBuilderTest.php | 6 +- 8 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 src/PhpPact/Consumer/Model/ProviderState.php create mode 100644 src/PhpPact/Consumer/Model/ProviderStates.php diff --git a/README.md b/README.md index bbd0e008..cfdb0396 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ Now that we have the request and response, we need to build the interaction and $config = new MockServerEnvConfig(); $builder = new InteractionBuilder($config); $builder - ->given('a person exists') + ->given('a person exists', ['name' => 'Bob']) ->uponReceiving('a get request to /hello/{name}') ->with($request) ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index a59696f8..0cbeec2d 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -33,12 +33,14 @@ public function __construct(MockServerConfigInterface $config) /** * @param string $providerState what is given to the request + * @param array $params for that request + * @param bool $overwrite clear pass states completely and start this array * * @return InteractionBuilder */ - public function given(string $providerState): self + public function given(string $providerState, array $params = [], bool $overwrite = false): self { - $this->interaction->setProviderState($providerState); + $this->interaction->setProviderState($providerState, $params, $overwrite); return $this; } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index e7485fea..48662593 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -8,6 +8,8 @@ */ class Interaction implements \JsonSerializable { + use ProviderStates; + /** * @var int */ @@ -18,11 +20,6 @@ class Interaction implements \JsonSerializable */ private string $description; - /** - * @var null|string - */ - private ?string $providerState = null; - /** * @var ConsumerRequest */ @@ -73,26 +70,6 @@ public function setDescription(string $description): self return $this; } - /** - * @return null|string - */ - public function getProviderState(): ?string - { - return $this->providerState; - } - - /** - * @param string $providerState - * - * @return Interaction - */ - public function setProviderState(string $providerState): self - { - $this->providerState = $providerState; - - return $this; - } - /** * @return ConsumerRequest */ diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index dbb1679b..febe1a55 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -8,6 +8,8 @@ */ class Message implements \JsonSerializable { + use ProviderStates; + /** * @var string */ @@ -16,12 +18,7 @@ class Message implements \JsonSerializable /** * @var array */ - private $providerStates = []; - - /** - * @var array - */ - private $metadata; + private array $metadata; /** * @var mixed @@ -48,48 +45,6 @@ public function setDescription(string $description): self return $this; } - /** - * @return array - */ - public function getProviderStates(): array - { - return $this->providerStates; - } - - /** - * @param mixed $overwrite - * - * @return array - */ - public function setProviderState(string $name, array $params = [], $overwrite = true): array - { - $this->addProviderState($name, $params, $overwrite); - - return $this->providerStates; - } - - /** - * @param string $name - * @param array $params - * @param bool $overwrite - if true reset the entire state - * - * @return Message - */ - public function addProviderState(string $name, array $params, $overwrite = false): self - { - $providerState = new \stdClass(); - $providerState->name = $name; - $providerState->params = $params; - - if ($overwrite === true) { - $this->providerStates = []; - } - - $this->providerStates[] = $providerState; - - return $this; - } - /** * @return array */ diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index 19f5122b..db82db28 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -81,7 +81,11 @@ private function initWithLogLevel(): self private function given(Interaction $interaction): self { - $this->ffi->pactffi_given($interaction->getId(), $interaction->getProviderState()); + foreach ($interaction->getProviderStates() as $providerState) { + foreach ($providerState->getParams() as $key => $value) { + $this->ffi->pactffi_given_with_param($interaction->getId(), $providerState->getName(), $key, $value); + } + } return $this; } diff --git a/src/PhpPact/Consumer/Model/ProviderState.php b/src/PhpPact/Consumer/Model/ProviderState.php new file mode 100644 index 00000000..48703a62 --- /dev/null +++ b/src/PhpPact/Consumer/Model/ProviderState.php @@ -0,0 +1,40 @@ +name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + public function setParams(array $params = []): void + { + foreach ($params as $key => $value) { + $this->addParam($key, $value); + } + } + + public function addParam(string $key, string $value): void + { + $this->params[$key] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/ProviderStates.php b/src/PhpPact/Consumer/Model/ProviderStates.php new file mode 100644 index 00000000..812d5a3a --- /dev/null +++ b/src/PhpPact/Consumer/Model/ProviderStates.php @@ -0,0 +1,55 @@ + + */ + public function getProviderStates(): array + { + return $this->providerStates; + } + + /** + * @param string $name + * @param array $params + * @param bool $overwrite + * + * @return array + */ + public function setProviderState(string $name, array $params = [], bool $overwrite = true): array + { + $this->addProviderState($name, $params, $overwrite); + + return $this->providerStates; + } + + /** + * @param string $name + * @param array $params + * @param bool $overwrite - if true reset the entire state + * + * @return $this + */ + public function addProviderState(string $name, array $params, bool $overwrite = false): self + { + $providerState = new ProviderState(); + $providerState->setName($name); + $providerState->setParams($params); + + if ($overwrite === true) { + $this->providerStates = []; + } + + $this->providerStates[] = $providerState; + + return $this; + } +} diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index 713a2f15..a385cf03 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -37,7 +37,7 @@ public function testSimpleGet() $builder = new InteractionBuilder(new MockServerEnvConfig()); $builder - ->given('A test request.') + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); @@ -75,7 +75,7 @@ public function testPostWithBody() $builder = new InteractionBuilder(new MockServerEnvConfig()); $builder - ->given('A test request.') + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); @@ -109,7 +109,7 @@ public function testBuildWithEachLikeMatcher() $builder = new InteractionBuilder(new MockServerEnvConfig()); $builder - ->given('A test request.') + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); From 1e0b4092d0c1da4d2658d6655454d11dad8cfea0 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 16 Mar 2023 10:25:25 +0700 Subject: [PATCH 017/298] Fix provider state without params not added --- src/PhpPact/Consumer/Model/Pact.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php index db82db28..b141c178 100644 --- a/src/PhpPact/Consumer/Model/Pact.php +++ b/src/PhpPact/Consumer/Model/Pact.php @@ -82,6 +82,7 @@ private function initWithLogLevel(): self private function given(Interaction $interaction): self { foreach ($interaction->getProviderStates() as $providerState) { + $this->ffi->pactffi_given($interaction->getId(), $providerState->getName()); foreach ($providerState->getParams() as $key => $value) { $this->ffi->pactffi_given_with_param($interaction->getId(), $providerState->getName(), $key, $value); } From 47b0a81c9acd6bc284fec202136f40b56537f401 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 5 Jan 2023 22:04:47 +0700 Subject: [PATCH 018/298] Remove jsonSerialize methods --- .../Consumer/Model/ConsumerRequest.php | 30 +------------------ src/PhpPact/Consumer/Model/Interaction.php | 23 +------------- src/PhpPact/Consumer/Model/Message.php | 26 +--------------- .../Consumer/Model/ProviderResponse.php | 22 +------------- 4 files changed, 4 insertions(+), 97 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index ae6f8daa..0efed279 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -6,7 +6,7 @@ * Request initiated by the consumer. * Class ConsumerRequest. */ -class ConsumerRequest implements \JsonSerializable +class ConsumerRequest { /** * @var string @@ -191,32 +191,4 @@ private function addQueryParameterValue(string $key, string $value): void { $this->query[$key][] = $value; } - - /** - * {@inheritdoc} - */ - public function jsonSerialize(): array - { - $results = []; - - $results['method'] = $this->getMethod(); - - if ($this->getHeaders() !== null) { - $results['headers'] = $this->getHeaders(); - } - - if ($this->getPath() !== null) { - $results['path'] = $this->getPath(); - } - - if ($this->getBody() !== null) { - $results['body'] = $this->getBody(); - } - - if ($this->getQuery() !== null) { - $results['query'] = $this->getQuery(); - } - - return $results; - } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 48662593..743befa8 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -6,7 +6,7 @@ * Request/Response Pair to be posted to the Ruby Standalone Mock Server for PACT tests. * Class Interaction. */ -class Interaction implements \JsonSerializable +class Interaction { use ProviderStates; @@ -109,25 +109,4 @@ public function setResponse(ProviderResponse $response): self return $this; } - - /** - * {@inheritdoc} - */ - public function jsonSerialize(): array - { - if ($this->getProviderState()) { - return [ - 'description' => $this->getDescription(), - 'providerState' => $this->getProviderState(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } - - return [ - 'description' => $this->getDescription(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } } diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index febe1a55..c56c3280 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -6,7 +6,7 @@ * Request/Response Pair to be posted to the Ruby Standalone Mock Server for PACT tests. * Class Interaction. */ -class Message implements \JsonSerializable +class Message { use ProviderStates; @@ -84,28 +84,4 @@ public function setContents($contents) return $this; } - - /** - * {@inheritdoc} - */ - #[\ReturnTypeWillChange] - public function jsonSerialize() - { - $out = []; - $out['description'] = $this->getDescription(); - - if (\count($this->providerStates) > 0) { - $out['providerStates'] = $this->getProviderStates(); - } - - if ($this->metadata) { - $out['metadata'] = $this->getMetadata(); - } - - if ($this->contents) { - $out['contents'] = $this->getContents(); - } - - return $out; - } } diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 6c3f95e4..abeaca25 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -6,7 +6,7 @@ * Response expectation that would be in response to a Consumer request from the Provider. * Class ProviderResponse. */ -class ProviderResponse implements \JsonSerializable +class ProviderResponse { /** * @var int @@ -115,24 +115,4 @@ public function setBody(mixed $body): self return $this; } - - /** - * {@inheritdoc} - */ - public function jsonSerialize(): array - { - $results = [ - 'status' => $this->getStatus(), - ]; - - if ($this->getHeaders() !== null) { - $results['headers'] = $this->getHeaders(); - } - - if ($this->getBody() !== null) { - $results['body'] = $this->getBody(); - } - - return $results; - } } From 2754f47504d65502b51fd7a16e83f5e17eaedf84 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 17 Mar 2023 22:39:34 +0700 Subject: [PATCH 019/298] Rename pact to driver --- phpstan.neon | 4 +- .../AbstractDriver.php} | 77 ++++++---- .../Consumer/Driver/DriverInterface.php | 7 + .../Consumer/Driver/HasMockServerTrait.php | 50 +++++++ .../Consumer/Driver/InteractionDriver.php | 103 +++++++++++++ .../Driver/InteractionDriverInterface.php | 12 ++ src/PhpPact/Consumer/InteractionBuilder.php | 13 +- src/PhpPact/Consumer/Model/Interaction.php | 25 ---- src/PhpPact/Consumer/Model/Pact.php | 141 ------------------ 9 files changed, 231 insertions(+), 201 deletions(-) rename src/PhpPact/Consumer/{Model/AbstractPact.php => Driver/AbstractDriver.php} (54%) create mode 100644 src/PhpPact/Consumer/Driver/DriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/HasMockServerTrait.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionDriver.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionDriverInterface.php delete mode 100644 src/PhpPact/Consumer/Model/Pact.php diff --git a/phpstan.neon b/phpstan.neon index 2704610d..408f763a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,4 @@ parameters: excludePaths: - - src/PhpPact/Consumer/Model/AbstractPact.php - - src/PhpPact/Consumer/Model/Pact.php + - src/PhpPact/Consumer/Driver/AbstractDriver.php + - src/PhpPact/Consumer/Driver/InteractionDriver.php diff --git a/src/PhpPact/Consumer/Model/AbstractPact.php b/src/PhpPact/Consumer/Driver/AbstractDriver.php similarity index 54% rename from src/PhpPact/Consumer/Model/AbstractPact.php rename to src/PhpPact/Consumer/Driver/AbstractDriver.php index 564bc3c4..f1f4b891 100644 --- a/src/PhpPact/Consumer/Model/AbstractPact.php +++ b/src/PhpPact/Consumer/Driver/AbstractDriver.php @@ -1,39 +1,46 @@ ffi = FFI::cdef(\file_get_contents(Scripts::getHeader()), Scripts::getLibrary()); $this - ->createFfi() + ->initWithLogLevel() ->newPact() ->withSpecification(); } - private function createFfi(): self + private function newPact(): self { - $this->ffi = FFI::cdef(\file_get_contents(Scripts::getHeader()), Scripts::getLibrary()); + $this->pactId = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); return $this; } - private function newPact(): self + protected function newInteraction(string $description): self + { + $this->interactionId = $this->ffi->pactffi_new_interaction($this->pactId, $description); + + return $this; + } + + private function withSpecification(): self { - $this->id = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); + $this->ffi->pactffi_with_specification($this->pactId, $this->getSpecification()); return $this; } @@ -58,27 +65,15 @@ protected function getSpecification(): int return $specification; } - private function withSpecification(): self - { - $this->ffi->pactffi_with_specification($this->id, $this->getSpecification()); - - return $this; - } - - protected function newInteraction(?string $description): int - { - return $this->ffi->pactffi_new_interaction($this->id, $description); - } - protected function cleanUp(): void { - $this->ffi->pactffi_free_pact_handle($this->id); + $this->ffi->pactffi_free_pact_handle($this->pactId); } protected function writePact(): void { $error = $this->ffi->pactffi_pact_handle_write_file( - $this->id, + $this->pactId, $this->config->getPactDir(), $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); @@ -87,14 +82,42 @@ protected function writePact(): void } } - protected function withBody(int $interaction, int $part, ?string $contentType = null, ?string $body = null): void + protected function withBody(int $part, ?string $contentType = null, ?string $body = null): void { if (is_null($body)) { return; } - $success = $this->ffi->pactffi_with_body($interaction, $part, $contentType, $body); + $success = $this->ffi->pactffi_with_body($this->interactionId, $part, $contentType, $body); if (!$success) { throw new InteractionBodyNotAddedException(); } } + + private function initWithLogLevel(): self + { + $logLevel = $this->config->getLogLevel(); + if ($logLevel) { + $this->ffi->pactffi_init_with_log_level($logLevel); + } + + return $this; + } + + /** + * @var array $providerStates + */ + protected function setProviderStates(array $providerStates): void + { + foreach ($providerStates as $providerState) { + $this->ffi->pactffi_given($this->interactionId, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->ffi->pactffi_given_with_param($this->interactionId, $providerState->getName(), $key, $value); + } + } + } + + protected function setDescription(string $description): void + { + $this->ffi->pactffi_upon_receiving($this->interactionId, $description); + } } diff --git a/src/PhpPact/Consumer/Driver/DriverInterface.php b/src/PhpPact/Consumer/Driver/DriverInterface.php new file mode 100644 index 00000000..70e13108 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/DriverInterface.php @@ -0,0 +1,7 @@ +ffi->pactffi_create_mock_server_for_transport( + $this->pactId, + $this->getMockServerConfig()->getHost(), + $this->getMockServerConfig()->getPort(), + $this->getMockServerTransport(), + null + ); + + if ($port < 0) { + throw new MockServerNotStartedException($port); + } + $this->getMockServerConfig()->setPort($port); + } + + protected function mockServerMatched(): bool + { + $matched = $this->ffi->pactffi_mock_server_matched($this->getMockServerConfig()->getPort()); + + try { + if ($matched) { + $this->writePact(); + } + } finally { + $this->cleanUp(); + } + + return $matched; + } + + protected function cleanUpMockServer(): void + { + $this->ffi->pactffi_cleanup_mock_server($this->getMockServerConfig()->getPort()); + } + + abstract protected function getMockServerTransport(): string; + + abstract protected function getMockServerConfig(): MockServerConfigInterface; +} diff --git a/src/PhpPact/Consumer/Driver/InteractionDriver.php b/src/PhpPact/Consumer/Driver/InteractionDriver.php new file mode 100644 index 00000000..c973071f --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionDriver.php @@ -0,0 +1,103 @@ +config->isSecure() ? 'https' : 'http'; + } + + protected function getMockServerConfig(): MockServerConfigInterface + { + return $this->config; + } + + public function verifyInteractions(): bool + { + return $this->mockServerMatched(); + } + + protected function cleanUp(): void + { + $this->cleanUpMockServer(); + parent::cleanUp(); + } + + public function registerInteraction(Interaction $interaction): bool + { + $this + ->newInteraction($interaction->getDescription()) + ->given($interaction) + ->uponReceiving($interaction) + ->with($interaction) + ->willRespondWith($interaction) + ->createMockServer(); + + return true; + } + + private function given(Interaction $interaction): self + { + $this->setProviderStates($interaction->getProviderStates()); + + return $this; + } + + private function uponReceiving(Interaction $interaction): self + { + $this->setDescription($interaction->getDescription()); + + return $this; + } + + private function with(Interaction $interaction): self + { + $request = $interaction->getRequest(); + $this->ffi->pactffi_with_request($this->interactionId, $request->getMethod(), $request->getPath()); + $this->withHeaders($this->ffi->InteractionPart_Request, $request->getHeaders()); + $this->withQuery($request->getQuery()); + $this->withBody($this->ffi->InteractionPart_Request, null, $request->getBody()); + + return $this; + } + + private function willRespondWith(Interaction $interaction): self + { + $response = $interaction->getResponse(); + $this->ffi->pactffi_response_status($this->interactionId, $response->getStatus()); + $this->withHeaders($this->ffi->InteractionPart_Response, $response->getHeaders()); + $this->withBody($this->ffi->InteractionPart_Response, null, $response->getBody()); + + return $this; + } + + private function withHeaders(int $part, array $headers): void + { + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_header_v2($this->interactionId, $part, (string) $header, (int) $index, (string) $value); + } + } + } + + private function withQuery(array $query): void + { + foreach ($query as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_query_parameter_v2($this->interactionId, (string) $key, (int) $index, (string) $value); + } + } + } +} diff --git a/src/PhpPact/Consumer/Driver/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/InteractionDriverInterface.php new file mode 100644 index 00000000..6255bb05 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionDriverInterface.php @@ -0,0 +1,12 @@ +interaction = new Interaction(); - $this->pact = new Pact($config); + $this->driver = new InteractionDriver($config); } /** @@ -78,7 +79,7 @@ public function willRespondWith(ProviderResponse $response): bool { $this->interaction->setResponse($response); - return $this->pact->registerInteraction($this->interaction); + return $this->driver->registerInteraction($this->interaction); } /** @@ -86,6 +87,6 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->pact->verifyInteractions(); + return $this->driver->verifyInteractions(); } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 743befa8..5759b6fa 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -10,11 +10,6 @@ class Interaction { use ProviderStates; - /** - * @var int - */ - private int $id; - /** * @var string */ @@ -30,26 +25,6 @@ class Interaction */ private ProviderResponse $response; - /** - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * @param int $id - * - * @return Interaction - */ - public function setId(int $id): self - { - $this->id = $id; - - return $this; - } - /** * @return string */ diff --git a/src/PhpPact/Consumer/Model/Pact.php b/src/PhpPact/Consumer/Model/Pact.php deleted file mode 100644 index b141c178..00000000 --- a/src/PhpPact/Consumer/Model/Pact.php +++ /dev/null @@ -1,141 +0,0 @@ -initWithLogLevel(); - } - - private function createMockServer(): void - { - $port = $this->ffi->pactffi_create_mock_server_for_transport( - $this->id, - $this->config->getHost(), - $this->config->getPort(), - $this->config->isSecure() ? 'https' : 'http', - null - ); - - if ($port < 0) { - throw new MockServerNotStartedException($port); - } - $this->config->setPort($port); - } - - public function verifyInteractions(): bool - { - $matched = $this->ffi->pactffi_mock_server_matched($this->config->getPort()); - - try { - if ($matched) { - $this->writePact(); - } - } finally { - $this->cleanUp(); - } - - return $matched; - } - - protected function cleanUp(): void - { - parent::cleanUp(); - $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); - } - - public function registerInteraction(Interaction $interaction): bool - { - $interaction->setId($this->newInteraction($interaction->getDescription())); - $this - ->given($interaction) - ->uponReceiving($interaction) - ->with($interaction) - ->willRespondWith($interaction) - ->createMockServer(); - - return true; - } - - private function initWithLogLevel(): self - { - $logLevel = $this->config->getLogLevel(); - if ($logLevel) { - $this->ffi->pactffi_init_with_log_level($logLevel); - } - - return $this; - } - - private function given(Interaction $interaction): self - { - foreach ($interaction->getProviderStates() as $providerState) { - $this->ffi->pactffi_given($interaction->getId(), $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->ffi->pactffi_given_with_param($interaction->getId(), $providerState->getName(), $key, $value); - } - } - - return $this; - } - - private function uponReceiving(Interaction $interaction): self - { - $this->ffi->pactffi_upon_receiving($interaction->getId(), $interaction->getDescription()); - - return $this; - } - - private function with(Interaction $interaction): self - { - $id = $interaction->getId(); - $request = $interaction->getRequest(); - $this->ffi->pactffi_with_request($id, $request->getMethod(), $request->getPath()); - $this->withHeaders($id, $this->ffi->InteractionPart_Request, $request->getHeaders()); - $this->withQuery($id, $request->getQuery()); - $this->withBody($id, $this->ffi->InteractionPart_Request, null, $request->getBody()); - - return $this; - } - - private function willRespondWith(Interaction $interaction): self - { - $id = $interaction->getId(); - $response = $interaction->getResponse(); - $this->ffi->pactffi_response_status($id, $response->getStatus()); - $this->withHeaders($id, $this->ffi->InteractionPart_Response, $response->getHeaders()); - $this->withBody($id, $this->ffi->InteractionPart_Response, null, $response->getBody()); - - return $this; - } - - private function withHeaders(int $interaction, int $part, array $headers): void - { - foreach ($headers as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_header_v2($interaction, $part, (string) $header, (int) $index, (string) $value); - } - } - } - - private function withQuery(int $interaction, array $query): void - { - foreach ($query as $key => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_query_parameter_v2($interaction, (string) $key, (int) $index, (string) $value); - } - } - } -} From 18b85b24ff741b3d795e0b38067bf23ef905244a Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 23 Mar 2023 11:04:27 +0700 Subject: [PATCH 020/298] Move log level config to trait for reusing --- src/PhpPact/Config/LogLevelTrait.php | 24 ++++++++++++++ .../{Standalone => Config}/PactConfig.php | 31 ++----------------- .../PactConfigInterface.php | 2 +- .../Consumer/Driver/AbstractDriver.php | 2 +- src/PhpPact/Consumer/MessageBuilder.php | 2 +- .../MockService/MockServerConfig.php | 2 +- .../MockService/MockServerConfigInterface.php | 2 +- .../PactMessage/PactMessageConfig.php | 2 +- .../{Standalone => Config}/PactConfigTest.php | 10 +++--- 9 files changed, 38 insertions(+), 39 deletions(-) create mode 100644 src/PhpPact/Config/LogLevelTrait.php rename src/PhpPact/{Standalone => Config}/PactConfig.php (85%) rename src/PhpPact/{Standalone => Config}/PactConfigInterface.php (98%) rename tests/PhpPact/{Standalone => Config}/PactConfigTest.php (89%) diff --git a/src/PhpPact/Config/LogLevelTrait.php b/src/PhpPact/Config/LogLevelTrait.php new file mode 100644 index 00000000..de5265ce --- /dev/null +++ b/src/PhpPact/Config/LogLevelTrait.php @@ -0,0 +1,24 @@ +logLevel; + } + + public function setLogLevel(string $logLevel): self + { + $logLevel = \strtoupper($logLevel); + if (!\in_array($logLevel, ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'])) { + throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); + } + $this->logLevel = $logLevel; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/PactConfig.php b/src/PhpPact/Config/PactConfig.php similarity index 85% rename from src/PhpPact/Standalone/PactConfig.php rename to src/PhpPact/Config/PactConfig.php index fc0cee8a..59d85358 100644 --- a/src/PhpPact/Standalone/PactConfig.php +++ b/src/PhpPact/Config/PactConfig.php @@ -1,12 +1,14 @@ logLevel; - } - - /** - * {@inheritdoc} - */ - public function setLogLevel(string $logLevel): self - { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); - } - $this->logLevel = $logLevel; - - return $this; - } - /** * {@inheritdoc} */ diff --git a/src/PhpPact/Standalone/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php similarity index 98% rename from src/PhpPact/Standalone/PactConfigInterface.php rename to src/PhpPact/Config/PactConfigInterface.php index e2febd82..c4307581 100644 --- a/src/PhpPact/Standalone/PactConfigInterface.php +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -1,6 +1,6 @@ expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('LogLevel TRACE not supported.'); - $this->config->setLogLevel('TRACE'); + $this->expectExceptionMessage('LogLevel VERBOSE not supported.'); + $this->config->setLogLevel('VERBOSE'); } public function testInvalidPactFileWriteMode(): void From 5a4b15f5cd8c9abdfb9c398c3f2c1db46db09e2c Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 23 Mar 2023 22:11:43 +0700 Subject: [PATCH 021/298] Allow mock server write pact file --- .../Consumer/Driver/HasMockServerTrait.php | 14 ++++++++++++++ .../Consumer/Driver/InteractionDriver.php | 5 +++++ .../MockServerNotWrotePactFileException.php | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php diff --git a/src/PhpPact/Consumer/Driver/HasMockServerTrait.php b/src/PhpPact/Consumer/Driver/HasMockServerTrait.php index fb273692..9a9e66e5 100644 --- a/src/PhpPact/Consumer/Driver/HasMockServerTrait.php +++ b/src/PhpPact/Consumer/Driver/HasMockServerTrait.php @@ -3,7 +3,9 @@ namespace PhpPact\Consumer\Driver; use FFI; +use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; +use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; use PhpPact\Standalone\MockService\MockServerConfigInterface; trait HasMockServerTrait @@ -47,4 +49,16 @@ protected function cleanUpMockServer(): void abstract protected function getMockServerTransport(): string; abstract protected function getMockServerConfig(): MockServerConfigInterface; + + protected function mockServerWritePact(): void + { + $error = $this->ffi->pactffi_write_pact_file( + $this->getMockServerConfig()->getPort(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new MockServerNotWrotePactFileException($error); + } + } } diff --git a/src/PhpPact/Consumer/Driver/InteractionDriver.php b/src/PhpPact/Consumer/Driver/InteractionDriver.php index c973071f..ad80aba7 100644 --- a/src/PhpPact/Consumer/Driver/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/InteractionDriver.php @@ -35,6 +35,11 @@ protected function cleanUp(): void parent::cleanUp(); } + protected function writePact(): void + { + $this->mockServerWritePact(); + } + public function registerInteraction(Interaction $interaction): bool { $this diff --git a/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php b/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php new file mode 100644 index 00000000..01209897 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php @@ -0,0 +1,19 @@ + 'A general panic was caught', + 2 => 'The pact file was not able to be written', + 3 => 'A mock server with the provided port was not found', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } +} From 29b33ec24e2baefc170223078df60c5da62c8a3b Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 3 May 2023 16:50:18 +0700 Subject: [PATCH 022/298] Revert removing semver --- composer.json | 1 + src/PhpPact/Config/PactConfig.php | 10 ++++++ src/PhpPact/Config/PactConfigInterface.php | 2 +- .../Consumer/Driver/AbstractDriver.php | 34 ++++++++++--------- tests/PhpPact/Config/PactConfigTest.php | 7 ++++ 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 36924890..7eddb66b 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "php": "^8.0", "ext-openssl": "*", "ext-json": "*", + "composer/semver": "^1.4.0|^3.2.0", "amphp/amp": "^2.5.1", "amphp/byte-stream": "^1.8", "amphp/dns": "^1.2.3", diff --git a/src/PhpPact/Config/PactConfig.php b/src/PhpPact/Config/PactConfig.php index a623de86..833f91fb 100644 --- a/src/PhpPact/Config/PactConfig.php +++ b/src/PhpPact/Config/PactConfig.php @@ -2,6 +2,8 @@ namespace PhpPact\Config; +use Composer\Semver\VersionParser; + class PactConfig implements PactConfigInterface { use LogLevelTrait; @@ -115,9 +117,17 @@ public function getPactSpecificationVersion(): string /** * {@inheritdoc} + * + * @throws \UnexpectedValueException */ public function setPactSpecificationVersion(string $pactSpecificationVersion): self { + /* + * Parse the version but do not assign it. If it is an invalid version, an exception is thrown + */ + $parser = new VersionParser(); + $parser->normalize($pactSpecificationVersion); + $this->pactSpecificationVersion = $pactSpecificationVersion; return $this; diff --git a/src/PhpPact/Config/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php index c4307581..fe6b4463 100644 --- a/src/PhpPact/Config/PactConfigInterface.php +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -55,7 +55,7 @@ public function setPactDir(?string $pactDir): self; public function getPactSpecificationVersion(): string; /** - * @param string $pactSpecificationVersion pact version + * @param string $pactSpecificationVersion pact semver version * * @return $this */ diff --git a/src/PhpPact/Consumer/Driver/AbstractDriver.php b/src/PhpPact/Consumer/Driver/AbstractDriver.php index 3a226390..dae01def 100644 --- a/src/PhpPact/Consumer/Driver/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/AbstractDriver.php @@ -2,6 +2,7 @@ namespace PhpPact\Consumer\Driver; +use Composer\Semver\Comparator; use FFI; use PhpPact\Standalone\Installer\Model\Scripts; use PhpPact\Config\PactConfigInterface; @@ -47,22 +48,18 @@ private function withSpecification(): self protected function getSpecification(): int { - $supportedVersions = [ - '1.0.0' => $this->ffi->PactSpecification_V1, - '1.1.0' => $this->ffi->PactSpecification_V1_1, - '2.0.0' => $this->ffi->PactSpecification_V2, - '3.0.0' => $this->ffi->PactSpecification_V3, - '4.0.0' => $this->ffi->PactSpecification_V4, - ]; - $version = $this->config->getPactSpecificationVersion(); - if (isset($supportedVersions[$version])) { - $specification = $supportedVersions[$version]; - } else { - trigger_error(sprintf("Specification version '%s' is unknown", $version), E_USER_WARNING); - $specification = $this->ffi->PactSpecification_Unknown; - } - - return $specification; + return match (true) { + $this->versionEqualTo('1.0.0') => $this->ffi->PactSpecification_V1, + $this->versionEqualTo('1.1.0') => $this->ffi->PactSpecification_V1_1, + $this->versionEqualTo('2.0.0') => $this->ffi->PactSpecification_V2, + $this->versionEqualTo('3.0.0') => $this->ffi->PactSpecification_V3, + $this->versionEqualTo('4.0.0') => $this->ffi->PactSpecification_V4, + default => function () { + trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); + + return $this->ffi->PactSpecification_Unknown; + }, + }; } protected function cleanUp(): void @@ -120,4 +117,9 @@ protected function setDescription(string $description): void { $this->ffi->pactffi_upon_receiving($this->interactionId, $description); } + + private function versionEqualTo(string $version): bool + { + return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); + } } diff --git a/tests/PhpPact/Config/PactConfigTest.php b/tests/PhpPact/Config/PactConfigTest.php index c542336e..105586d3 100644 --- a/tests/PhpPact/Config/PactConfigTest.php +++ b/tests/PhpPact/Config/PactConfigTest.php @@ -43,6 +43,13 @@ public function testSetters(): void static::assertSame($pactFileWriteMode, $this->config->getPactFileWriteMode()); } + public function testInvalidPactSpecificationVersion(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Invalid version string "invalid"'); + $this->config->setPactSpecificationVersion('invalid'); + } + public function testInvalidLogLevel(): void { $this->expectException(\InvalidArgumentException::class); From a0302bcec4838761bfc6870e42467d686e3b3621 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 4 May 2023 14:08:43 +0700 Subject: [PATCH 023/298] Extract services and helpers --- composer.json | 2 +- phpstan.neon | 11 +- .../Consumer/Driver/AbstractDriver.php | 125 ----------------- .../Consumer/Driver/DriverInterface.php | 7 - .../Consumer/Driver/HasMockServerTrait.php | 64 --------- .../Consumer/Driver/InteractionDriver.php | 108 --------------- .../Exception/HeaderNotReadException.php | 9 ++ .../InteractionBodyNotAddedException.php | 3 - .../MockServerNotStartedException.php | 3 - .../Exception/PactFileNotWroteException.php | 3 - src/PhpPact/Consumer/InteractionBuilder.php | 27 ++-- .../Consumer/Model/ConsumerRequest.php | 7 + .../Consumer/Model/ProviderResponse.php | 5 + .../Consumer/Service/Helper/BodyTrait.php | 22 +++ .../Service/Helper/DescriptionTrait.php | 16 +++ .../Consumer/Service/Helper/FFITrait.php | 21 +++ .../Service/Helper/InteractionTrait.php | 13 ++ .../Service/Helper/ProviderStatesTrait.php | 24 ++++ .../Service/Helper/SpecificationTrait.php | 24 ++++ .../Consumer/Service/InteractionRegistry.php | 127 ++++++++++++++++++ .../InteractionRegistryInterface.php} | 4 +- src/PhpPact/Consumer/Service/MockServer.php | 77 +++++++++++ .../Consumer/Service/MockServerInterface.php | 16 +++ src/PhpPact/Consumer/Service/PactRegistry.php | 80 +++++++++++ .../Service/PactRegistryInterface.php | 14 ++ 25 files changed, 484 insertions(+), 328 deletions(-) delete mode 100644 src/PhpPact/Consumer/Driver/AbstractDriver.php delete mode 100644 src/PhpPact/Consumer/Driver/DriverInterface.php delete mode 100644 src/PhpPact/Consumer/Driver/HasMockServerTrait.php delete mode 100644 src/PhpPact/Consumer/Driver/InteractionDriver.php create mode 100644 src/PhpPact/Consumer/Exception/HeaderNotReadException.php create mode 100644 src/PhpPact/Consumer/Service/Helper/BodyTrait.php create mode 100644 src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php create mode 100644 src/PhpPact/Consumer/Service/Helper/FFITrait.php create mode 100644 src/PhpPact/Consumer/Service/Helper/InteractionTrait.php create mode 100644 src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php create mode 100644 src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php create mode 100644 src/PhpPact/Consumer/Service/InteractionRegistry.php rename src/PhpPact/Consumer/{Driver/InteractionDriverInterface.php => Service/InteractionRegistryInterface.php} (64%) create mode 100644 src/PhpPact/Consumer/Service/MockServer.php create mode 100644 src/PhpPact/Consumer/Service/MockServerInterface.php create mode 100644 src/PhpPact/Consumer/Service/PactRegistry.php create mode 100644 src/PhpPact/Consumer/Service/PactRegistryInterface.php diff --git a/composer.json b/composer.json index 7eddb66b..f2c08650 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,7 @@ }, "scripts": { "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", - "static-code-analysis": "phpstan analyse src/ --level=7 -c phpstan.neon", + "static-code-analysis": "phpstan analyse src/", "lint": "php-cs-fixer fix --config .php-cs-fixer.php --dry-run", "fix": "php-cs-fixer fix --config .php-cs-fixer.php", "test": "phpunit --debug -c example/phpunit.all.xml" diff --git a/phpstan.neon b/phpstan.neon index 408f763a..3c391252 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,9 @@ parameters: - excludePaths: - - src/PhpPact/Consumer/Driver/AbstractDriver.php - - src/PhpPact/Consumer/Driver/InteractionDriver.php + level: 7 + ignoreErrors: + - + messages: + - '#Call to an undefined method FFI::[a-zA-Z0-9\\_]+\(\)#' + - '#Access to an undefined property FFI::\$[a-zA-Z0-9\\_]+#' + paths: + - src/PhpPact/Consumer/Service/* diff --git a/src/PhpPact/Consumer/Driver/AbstractDriver.php b/src/PhpPact/Consumer/Driver/AbstractDriver.php deleted file mode 100644 index dae01def..00000000 --- a/src/PhpPact/Consumer/Driver/AbstractDriver.php +++ /dev/null @@ -1,125 +0,0 @@ -ffi = FFI::cdef(\file_get_contents(Scripts::getHeader()), Scripts::getLibrary()); - $this - ->initWithLogLevel() - ->newPact() - ->withSpecification(); - } - - private function newPact(): self - { - $this->pactId = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); - - return $this; - } - - protected function newInteraction(string $description): self - { - $this->interactionId = $this->ffi->pactffi_new_interaction($this->pactId, $description); - - return $this; - } - - private function withSpecification(): self - { - $this->ffi->pactffi_with_specification($this->pactId, $this->getSpecification()); - - return $this; - } - - protected function getSpecification(): int - { - return match (true) { - $this->versionEqualTo('1.0.0') => $this->ffi->PactSpecification_V1, - $this->versionEqualTo('1.1.0') => $this->ffi->PactSpecification_V1_1, - $this->versionEqualTo('2.0.0') => $this->ffi->PactSpecification_V2, - $this->versionEqualTo('3.0.0') => $this->ffi->PactSpecification_V3, - $this->versionEqualTo('4.0.0') => $this->ffi->PactSpecification_V4, - default => function () { - trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); - - return $this->ffi->PactSpecification_Unknown; - }, - }; - } - - protected function cleanUp(): void - { - $this->ffi->pactffi_free_pact_handle($this->pactId); - } - - protected function writePact(): void - { - $error = $this->ffi->pactffi_pact_handle_write_file( - $this->pactId, - $this->config->getPactDir(), - $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE - ); - if ($error) { - throw new PactFileNotWroteException($error); - } - } - - protected function withBody(int $part, ?string $contentType = null, ?string $body = null): void - { - if (is_null($body)) { - return; - } - $success = $this->ffi->pactffi_with_body($this->interactionId, $part, $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } - - private function initWithLogLevel(): self - { - $logLevel = $this->config->getLogLevel(); - if ($logLevel) { - $this->ffi->pactffi_init_with_log_level($logLevel); - } - - return $this; - } - - /** - * @var array $providerStates - */ - protected function setProviderStates(array $providerStates): void - { - foreach ($providerStates as $providerState) { - $this->ffi->pactffi_given($this->interactionId, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->ffi->pactffi_given_with_param($this->interactionId, $providerState->getName(), $key, $value); - } - } - } - - protected function setDescription(string $description): void - { - $this->ffi->pactffi_upon_receiving($this->interactionId, $description); - } - - private function versionEqualTo(string $version): bool - { - return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); - } -} diff --git a/src/PhpPact/Consumer/Driver/DriverInterface.php b/src/PhpPact/Consumer/Driver/DriverInterface.php deleted file mode 100644 index 70e13108..00000000 --- a/src/PhpPact/Consumer/Driver/DriverInterface.php +++ /dev/null @@ -1,7 +0,0 @@ -ffi->pactffi_create_mock_server_for_transport( - $this->pactId, - $this->getMockServerConfig()->getHost(), - $this->getMockServerConfig()->getPort(), - $this->getMockServerTransport(), - null - ); - - if ($port < 0) { - throw new MockServerNotStartedException($port); - } - $this->getMockServerConfig()->setPort($port); - } - - protected function mockServerMatched(): bool - { - $matched = $this->ffi->pactffi_mock_server_matched($this->getMockServerConfig()->getPort()); - - try { - if ($matched) { - $this->writePact(); - } - } finally { - $this->cleanUp(); - } - - return $matched; - } - - protected function cleanUpMockServer(): void - { - $this->ffi->pactffi_cleanup_mock_server($this->getMockServerConfig()->getPort()); - } - - abstract protected function getMockServerTransport(): string; - - abstract protected function getMockServerConfig(): MockServerConfigInterface; - - protected function mockServerWritePact(): void - { - $error = $this->ffi->pactffi_write_pact_file( - $this->getMockServerConfig()->getPort(), - $this->config->getPactDir(), - $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE - ); - if ($error) { - throw new MockServerNotWrotePactFileException($error); - } - } -} diff --git a/src/PhpPact/Consumer/Driver/InteractionDriver.php b/src/PhpPact/Consumer/Driver/InteractionDriver.php deleted file mode 100644 index ad80aba7..00000000 --- a/src/PhpPact/Consumer/Driver/InteractionDriver.php +++ /dev/null @@ -1,108 +0,0 @@ -config->isSecure() ? 'https' : 'http'; - } - - protected function getMockServerConfig(): MockServerConfigInterface - { - return $this->config; - } - - public function verifyInteractions(): bool - { - return $this->mockServerMatched(); - } - - protected function cleanUp(): void - { - $this->cleanUpMockServer(); - parent::cleanUp(); - } - - protected function writePact(): void - { - $this->mockServerWritePact(); - } - - public function registerInteraction(Interaction $interaction): bool - { - $this - ->newInteraction($interaction->getDescription()) - ->given($interaction) - ->uponReceiving($interaction) - ->with($interaction) - ->willRespondWith($interaction) - ->createMockServer(); - - return true; - } - - private function given(Interaction $interaction): self - { - $this->setProviderStates($interaction->getProviderStates()); - - return $this; - } - - private function uponReceiving(Interaction $interaction): self - { - $this->setDescription($interaction->getDescription()); - - return $this; - } - - private function with(Interaction $interaction): self - { - $request = $interaction->getRequest(); - $this->ffi->pactffi_with_request($this->interactionId, $request->getMethod(), $request->getPath()); - $this->withHeaders($this->ffi->InteractionPart_Request, $request->getHeaders()); - $this->withQuery($request->getQuery()); - $this->withBody($this->ffi->InteractionPart_Request, null, $request->getBody()); - - return $this; - } - - private function willRespondWith(Interaction $interaction): self - { - $response = $interaction->getResponse(); - $this->ffi->pactffi_response_status($this->interactionId, $response->getStatus()); - $this->withHeaders($this->ffi->InteractionPart_Response, $response->getHeaders()); - $this->withBody($this->ffi->InteractionPart_Response, null, $response->getBody()); - - return $this; - } - - private function withHeaders(int $part, array $headers): void - { - foreach ($headers as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_header_v2($this->interactionId, $part, (string) $header, (int) $index, (string) $value); - } - } - } - - private function withQuery(array $query): void - { - foreach ($query as $key => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_query_parameter_v2($this->interactionId, (string) $key, (int) $index, (string) $value); - } - } - } -} diff --git a/src/PhpPact/Consumer/Exception/HeaderNotReadException.php b/src/PhpPact/Consumer/Exception/HeaderNotReadException.php new file mode 100644 index 00000000..95655deb --- /dev/null +++ b/src/PhpPact/Consumer/Exception/HeaderNotReadException.php @@ -0,0 +1,9 @@ +interaction = new Interaction(); - $this->driver = new InteractionDriver($config); + $this->interaction = new Interaction(); + $this->registry = $this->createRegistry($config); } /** @@ -59,13 +61,12 @@ public function with(ConsumerRequest $request): self * @param ProviderResponse $response mock of response received * * @return bool returns true on success - * @throws \JsonException */ public function willRespondWith(ProviderResponse $response): bool { $this->interaction->setResponse($response); - return $this->driver->registerInteraction($this->interaction); + return $this->registry->registerInteraction($this->interaction); } /** @@ -73,6 +74,14 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->driver->verifyInteractions(); + return $this->registry->verifyInteractions(); + } + + protected function createRegistry(MockServerConfigInterface $config): InteractionRegistryInterface + { + $pactRegistry = new PactRegistry($config); + $mockServer = new MockServer($pactRegistry, $config); + + return new InteractionRegistry($mockServer); } } diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index e1a8ccf3..3e98bae1 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -2,6 +2,8 @@ namespace PhpPact\Consumer\Model; +use JsonException; + /** * Request initiated by the consumer. */ @@ -42,6 +44,8 @@ public function getPath(): string /** * @param string|array $path + * + * @throws JsonException */ public function setPath(string|array $path): self { @@ -96,6 +100,9 @@ public function getBody(): ?string return $this->body; } + /** + * @throws JsonException + */ public function setBody(mixed $body): self { if (\is_string($body)) { diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index df370f8b..b4459384 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -2,6 +2,8 @@ namespace PhpPact\Consumer\Model; +use JsonException; + /** * Response expectation that would be in response to a Consumer request from the Provider. */ @@ -74,6 +76,9 @@ public function getBody(): ?string return $this->body; } + /** + * @throws JsonException + */ public function setBody(mixed $body): self { if (\is_string($body) || \is_null($body)) { diff --git a/src/PhpPact/Consumer/Service/Helper/BodyTrait.php b/src/PhpPact/Consumer/Service/Helper/BodyTrait.php new file mode 100644 index 00000000..815dafe9 --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/BodyTrait.php @@ -0,0 +1,22 @@ +ffi->pactffi_with_body($this->getId(), $part, $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } +} diff --git a/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php b/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php new file mode 100644 index 00000000..d83903fc --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php @@ -0,0 +1,16 @@ +ffi->pactffi_upon_receiving($this->getId(), $description); + } +} diff --git a/src/PhpPact/Consumer/Service/Helper/FFITrait.php b/src/PhpPact/Consumer/Service/Helper/FFITrait.php new file mode 100644 index 00000000..710299cf --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/FFITrait.php @@ -0,0 +1,21 @@ +ffi = FFI::cdef($code, Scripts::getLibrary()); + } +} diff --git a/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php b/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php new file mode 100644 index 00000000..a4b457ee --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php @@ -0,0 +1,13 @@ +interactionId; + } +} diff --git a/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php b/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php new file mode 100644 index 00000000..776da088 --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php @@ -0,0 +1,24 @@ +ffi->pactffi_given($this->getId(), $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->ffi->pactffi_given_with_param($this->getId(), $providerState->getName(), $key, $value); + } + } + } +} diff --git a/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php b/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php new file mode 100644 index 00000000..42358d88 --- /dev/null +++ b/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php @@ -0,0 +1,24 @@ +versionEqualTo('1.0.0') => $this->ffi->PactSpecification_V1, + $this->versionEqualTo('1.1.0') => $this->ffi->PactSpecification_V1_1, + $this->versionEqualTo('2.0.0') => $this->ffi->PactSpecification_V2, + $this->versionEqualTo('3.0.0') => $this->ffi->PactSpecification_V3, + $this->versionEqualTo('4.0.0') => $this->ffi->PactSpecification_V4, + default => function () { + trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); + + return $this->ffi->PactSpecification_Unknown; + }, + }; + } +} diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php new file mode 100644 index 00000000..f39390a0 --- /dev/null +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -0,0 +1,127 @@ +createFFI(); + } + + public function verifyInteractions(): bool + { + $matched = $this->mockServer->isMatched(); + + try { + if ($matched) { + $this->writePact(); + } + } finally { + $this->cleanUp(); + } + + return $matched; + } + + public function registerInteraction(Interaction $interaction): bool + { + $pactId = $this->mockServer->init(); + + $this + ->newInteraction($pactId, $interaction->getDescription()) + ->given($interaction) + ->uponReceiving($interaction) + ->with($interaction) + ->willRespondWith($interaction); + + $this->mockServer->start(); + + return true; + } + + private function cleanUp(): void + { + $this->mockServer->cleanUp(); + } + + private function writePact(): void + { + $this->mockServer->writePact(); + } + + private function newInteraction(int $pactId, string $description): self + { + $this->interactionId = $this->ffi->pactffi_new_interaction($pactId, $description); + + return $this; + } + + private function given(Interaction $interaction): self + { + $this->setProviderStates($interaction->getProviderStates()); + + return $this; + } + + private function uponReceiving(Interaction $interaction): self + { + $this->setDescription($interaction->getDescription()); + + return $this; + } + + private function with(Interaction $interaction): self + { + $request = $interaction->getRequest(); + $this->ffi->pactffi_with_request($this->getId(), $request->getMethod(), $request->getPath()); + $this->setHeaders($this->ffi->InteractionPart_Request, $request->getHeaders()); + $this->setQuery($request->getQuery()); + $this->setBody($this->ffi->InteractionPart_Request, null, $request->getBody()); + + return $this; + } + + private function willRespondWith(Interaction $interaction): self + { + $response = $interaction->getResponse(); + $this->ffi->pactffi_response_status($this->getId(), $response->getStatus()); + $this->setHeaders($this->ffi->InteractionPart_Response, $response->getHeaders()); + $this->setBody($this->ffi->InteractionPart_Response, null, $response->getBody()); + + return $this; + } + + /** + * @param array $headers + */ + private function setHeaders(int $part, array $headers): void + { + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_header_v2($this->getId(), $part, (string) $header, (int) $index, (string) $value); + } + } + } + + /** + * @param array $query + */ + private function setQuery(array $query): void + { + foreach ($query as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->pactffi_with_query_parameter_v2($this->getId(), (string) $key, (int) $index, (string) $value); + } + } + } +} diff --git a/src/PhpPact/Consumer/Driver/InteractionDriverInterface.php b/src/PhpPact/Consumer/Service/InteractionRegistryInterface.php similarity index 64% rename from src/PhpPact/Consumer/Driver/InteractionDriverInterface.php rename to src/PhpPact/Consumer/Service/InteractionRegistryInterface.php index 6255bb05..54159b90 100644 --- a/src/PhpPact/Consumer/Driver/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistryInterface.php @@ -1,10 +1,10 @@ createFFI(); + } + + public function init(): int + { + $this->pactRegistry->registerPact(); + + return $this->pactRegistry->getId(); + } + + public function start(): void + { + $port = $this->ffi->pactffi_create_mock_server_for_transport( + $this->pactRegistry->getId(), + $this->config->getHost(), + $this->config->getPort(), + $this->getTransport(), + $this->getTransportConfig() + ); + + if ($port < 0) { + throw new MockServerNotStartedException($port); + } + $this->config->setPort($port); + } + + public function isMatched(): bool + { + return $this->ffi->pactffi_mock_server_matched($this->config->getPort()); + } + + public function writePact(): void + { + $error = $this->ffi->pactffi_write_pact_file( + $this->config->getPort(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new MockServerNotWrotePactFileException($error); + } + } + + public function cleanUp(): void + { + $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); + $this->pactRegistry->cleanUp(); + } + + protected function getTransport(): string + { + return $this->config->isSecure() ? 'https' : 'http'; + } + + protected function getTransportConfig(): ?string + { + return null; + } +} diff --git a/src/PhpPact/Consumer/Service/MockServerInterface.php b/src/PhpPact/Consumer/Service/MockServerInterface.php new file mode 100644 index 00000000..fd115d87 --- /dev/null +++ b/src/PhpPact/Consumer/Service/MockServerInterface.php @@ -0,0 +1,16 @@ +createFFI(); + $this->initWithLogLevel(); + } + + public function registerPact(): void + { + $this + ->newPact() + ->withSpecification(); + } + + public function getId(): int + { + return $this->pactId; + } + + public function cleanUp(): void + { + $this->ffi->pactffi_free_pact_handle($this->getId()); + unset($this->pactId); + } + + public function writePact(): void + { + $error = $this->ffi->pactffi_pact_handle_write_file( + $this->getId(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new PactFileNotWroteException($error); + } + } + + private function initWithLogLevel(): self + { + $logLevel = $this->config->getLogLevel(); + if ($logLevel) { + $this->ffi->pactffi_init_with_log_level($logLevel); + } + + return $this; + } + + private function newPact(): self + { + $this->pactId = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); + + return $this; + } + + private function withSpecification(): self + { + $this->ffi->pactffi_with_specification($this->getId(), $this->getSpecification()); + + return $this; + } + + private function versionEqualTo(string $version): bool + { + return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); + } +} diff --git a/src/PhpPact/Consumer/Service/PactRegistryInterface.php b/src/PhpPact/Consumer/Service/PactRegistryInterface.php new file mode 100644 index 00000000..26c6c9c0 --- /dev/null +++ b/src/PhpPact/Consumer/Service/PactRegistryInterface.php @@ -0,0 +1,14 @@ + Date: Thu, 4 May 2023 14:42:24 +0700 Subject: [PATCH 024/298] Make PR smaller --- README.md | 2 +- src/PhpPact/Config/PactConfigInterface.php | 27 --------------------- src/PhpPact/Consumer/InteractionBuilder.php | 2 +- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a0e11fd8..115e75a9 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Table of contents - [Usage for the optional `pact-stub-service`](#usage-for-the-optional-pact-stub-service) ## Versions -9.X adds support for pact specification 3.X & 4.X via Pact FFI. This results in dropping PHP 7.4 +9.X updates internal dependencies and libraries + adds support for pact specification 3.X & 4.X via Pact FFI. This results in dropping PHP 7.4 8.X updates internal dependencies and libraries. This results in dropping PHP 7.3 diff --git a/src/PhpPact/Config/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php index fe6b4463..d952a725 100644 --- a/src/PhpPact/Config/PactConfigInterface.php +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -4,7 +4,6 @@ /** * Mock Server configuration interface to allow for simple overrides that are reusable. - * Interface PactConfigInterface. */ interface PactConfigInterface { @@ -13,27 +12,17 @@ interface PactConfigInterface public const MODE_OVERWRITE = 'overwrite'; public const MODE_MERGE = 'merge'; - /** - * @return string - */ public function getConsumer(): string; /** * @param string $consumer consumers name - * - * @return $this */ public function setConsumer(string $consumer): self; - /** - * @return string - */ public function getProvider(): string; /** * @param string $provider providers name - * - * @return $this */ public function setProvider(string $provider): self; @@ -44,8 +33,6 @@ public function getPactDir(): string; /** * @param null|string $pactDir url to place the pact files when written to disk - * - * @return $this */ public function setPactDir(?string $pactDir): self; @@ -56,8 +43,6 @@ public function getPactSpecificationVersion(): string; /** * @param string $pactSpecificationVersion pact semver version - * - * @return $this */ public function setPactSpecificationVersion(string $pactSpecificationVersion): self; @@ -68,21 +53,11 @@ public function getLog(): ?string; /** * @param string $log directory for log output - * - * @return $this */ public function setLog(string $log): self; - /** - * @return null|string - */ public function getLogLevel(): ?string; - /** - * @param string $logLevel - * - * @return $this - */ public function setLogLevel(string $logLevel): self; /** @@ -92,8 +67,6 @@ public function getPactFileWriteMode(): string; /** * @param string $pactFileWriteMode 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten - * - * @return $this */ public function setPactFileWriteMode(string $pactFileWriteMode): self; } diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index dc8b350b..5a08a917 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -21,8 +21,8 @@ class InteractionBuilder implements BuilderInterface public function __construct(MockServerConfigInterface $config) { - $this->interaction = new Interaction(); $this->registry = $this->createRegistry($config); + $this->interaction = new Interaction(); } /** From ff2120f70a1f7a7f2245e419a89b7fbcc03c0426 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 00:02:12 +0700 Subject: [PATCH 025/298] Extract drivers and factories --- .php-cs-fixer.php | 3 +- composer.json | 6 +- phpstan.neon | 9 +- .../Driver/Interaction/AbstractDriver.php | 47 ++++++++++ .../Driver/Interaction/DriverInterface.php | 22 +++++ .../Driver/Interaction/InteractionDriver.php | 45 +++++++++ .../InteractionDriverInterface.php | 20 ++++ .../Consumer/Driver/Pact/PactDriver.php | 92 +++++++++++++++++++ .../Pact/PactDriverInterface.php} | 10 +- .../Factory/InteractionRegistryFactory.php | 27 ++++++ src/PhpPact/Consumer/InteractionBuilder.php | 16 +--- src/PhpPact/Consumer/Service/FFI.php | 34 +++++++ src/PhpPact/Consumer/Service/FFIInterface.php | 13 +++ .../Consumer/Service/Helper/BodyTrait.php | 22 ----- .../Service/Helper/DescriptionTrait.php | 16 ---- .../Consumer/Service/Helper/FFITrait.php | 21 ----- .../Service/Helper/InteractionTrait.php | 13 --- .../Service/Helper/ProviderStatesTrait.php | 24 ----- .../Service/Helper/SpecificationTrait.php | 24 ----- .../Consumer/Service/InteractionRegistry.php | 69 +++++--------- src/PhpPact/Consumer/Service/MockServer.php | 29 +++--- .../Consumer/Service/MockServerInterface.php | 2 - src/PhpPact/Consumer/Service/PactRegistry.php | 80 ---------------- 23 files changed, 347 insertions(+), 297 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Pact/PactDriver.php rename src/PhpPact/Consumer/{Service/PactRegistryInterface.php => Driver/Pact/PactDriverInterface.php} (52%) create mode 100644 src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php create mode 100644 src/PhpPact/Consumer/Service/FFI.php create mode 100644 src/PhpPact/Consumer/Service/FFIInterface.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/BodyTrait.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/FFITrait.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/InteractionTrait.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php delete mode 100644 src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php delete mode 100644 src/PhpPact/Consumer/Service/PactRegistry.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index cac6eabb..cf318188 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -7,7 +7,8 @@ ->name('*.php'); $config = new PhpCsFixer\Config(); -$config->setRules(['@PSR12' => true, +$config->setRules([ + '@PSR12' => true, 'strict_param' => false, 'array_syntax' => ['syntax' => 'short'], ]) diff --git a/composer.json b/composer.json index f2c08650..c5270b90 100644 --- a/composer.json +++ b/composer.json @@ -71,9 +71,9 @@ }, "scripts": { "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", - "static-code-analysis": "phpstan analyse src/", - "lint": "php-cs-fixer fix --config .php-cs-fixer.php --dry-run", - "fix": "php-cs-fixer fix --config .php-cs-fixer.php", + "static-code-analysis": "phpstan", + "lint": "php-cs-fixer fix --dry-run", + "fix": "php-cs-fixer fix", "test": "phpunit --debug -c example/phpunit.all.xml" }, "extra": { diff --git a/phpstan.neon b/phpstan.neon index 3c391252..1d5823a9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,4 @@ parameters: level: 7 - ignoreErrors: - - - messages: - - '#Call to an undefined method FFI::[a-zA-Z0-9\\_]+\(\)#' - - '#Access to an undefined property FFI::\$[a-zA-Z0-9\\_]+#' - paths: - - src/PhpPact/Consumer/Service/* + paths: + - src diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php new file mode 100644 index 00000000..2cc6249a --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -0,0 +1,47 @@ +ffi->call('pactffi_with_body', $this->id, $this->ffi->get($part), $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + public function setDescription(string $description): void + { + $this->ffi->call('pactffi_upon_receiving', $this->id, $description); + } + + /** + * {@inheritdoc} + */ + public function setProviderStates(array $providerStates): void + { + foreach ($providerStates as $providerState) { + $this->ffi->call('pactffi_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->ffi->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php new file mode 100644 index 00000000..0f806476 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -0,0 +1,22 @@ +id = $this->ffi->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); + } + + /** + * {@inheritdoc} + */ + public function setHeaders(string $part, array $headers): void + { + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->call('pactffi_with_header_v2', $this->id, $this->ffi->get($part), (string) $header, (int) $index, (string) $value); + } + } + } + + /** + * {@inheritdoc} + */ + public function setQuery(array $query): void + { + foreach ($query as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->ffi->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); + } + } + } + + public function setRequest(string $method, string $path): void + { + $this->ffi->call('pactffi_with_request', $this->id, $method, $path); + } + + public function setResponse(int $status): void + { + $this->ffi->call('pactffi_response_status', $this->id, $status); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php new file mode 100644 index 00000000..3ae38be8 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -0,0 +1,20 @@ + $headers + */ + public function setHeaders(string $part, array $headers): void; + + /** + * @param array $query + */ + public function setQuery(array $query): void; + + public function setRequest(string $method, string $path): void; + + public function setResponse(int $status): void; +} diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php new file mode 100644 index 00000000..b1dd6549 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -0,0 +1,92 @@ +initWithLogLevel() + ->newPact() + ->withSpecification(); + } + + public function getId(): int + { + return $this->id; + } + + public function cleanUp(): void + { + $this->ffi->call('pactffi_free_pact_handle', $this->id); + unset($this->id); + } + + public function writePact(): void + { + $error = $this->ffi->call( + 'pactffi_pact_handle_write_file', + $this->id, + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new PactFileNotWroteException($error); + } + } + + protected function getSpecification(): int + { + return match (true) { + $this->versionEqualTo('1.0.0') => $this->ffi->get('PactSpecification_V1'), + $this->versionEqualTo('1.1.0') => $this->ffi->get('PactSpecification_V1_1'), + $this->versionEqualTo('2.0.0') => $this->ffi->get('PactSpecification_V2'), + $this->versionEqualTo('3.0.0') => $this->ffi->get('PactSpecification_V3'), + $this->versionEqualTo('4.0.0') => $this->ffi->get('PactSpecification_V4'), + default => function () { + trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); + + return $this->ffi->get('PactSpecification_Unknown'); + }, + }; + } + + private function versionEqualTo(string $version): bool + { + return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); + } + + private function initWithLogLevel(): self + { + $logLevel = $this->config->getLogLevel(); + if ($logLevel) { + $this->ffi->call('pactffi_init_with_log_level', $logLevel); + } + + return $this; + } + + private function newPact(): self + { + $this->id = $this->ffi->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); + + return $this; + } + + private function withSpecification(): self + { + $this->ffi->call('pactffi_with_specification', $this->id, $this->getSpecification()); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Service/PactRegistryInterface.php b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php similarity index 52% rename from src/PhpPact/Consumer/Service/PactRegistryInterface.php rename to src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php index 26c6c9c0..6b06d013 100644 --- a/src/PhpPact/Consumer/Service/PactRegistryInterface.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php @@ -1,14 +1,12 @@ registry = $this->createRegistry($config); + $this->registry = $registry instanceof InteractionRegistryInterface ? $registry : InteractionRegistryFactory::create($registry, $registry); $this->interaction = new Interaction(); } @@ -76,12 +74,4 @@ public function verify(): bool { return $this->registry->verifyInteractions(); } - - protected function createRegistry(MockServerConfigInterface $config): InteractionRegistryInterface - { - $pactRegistry = new PactRegistry($config); - $mockServer = new MockServer($pactRegistry, $config); - - return new InteractionRegistry($mockServer); - } } diff --git a/src/PhpPact/Consumer/Service/FFI.php b/src/PhpPact/Consumer/Service/FFI.php new file mode 100644 index 00000000..8b70b800 --- /dev/null +++ b/src/PhpPact/Consumer/Service/FFI.php @@ -0,0 +1,34 @@ +ffi = CoreFFI::cdef($code, Scripts::getLibrary()); + } + + /** + * {@inheritdoc} + */ + public function call(string $name, ...$arguments): mixed + { + return $this->ffi->{$name}(...$arguments); + } + + public function get(string $name): mixed + { + return $this->ffi->{$name}; + } +} diff --git a/src/PhpPact/Consumer/Service/FFIInterface.php b/src/PhpPact/Consumer/Service/FFIInterface.php new file mode 100644 index 00000000..c3b5e20a --- /dev/null +++ b/src/PhpPact/Consumer/Service/FFIInterface.php @@ -0,0 +1,13 @@ + $arguments + */ + public function call(string $name, ...$arguments): mixed; + + public function get(string $name): mixed; +} diff --git a/src/PhpPact/Consumer/Service/Helper/BodyTrait.php b/src/PhpPact/Consumer/Service/Helper/BodyTrait.php deleted file mode 100644 index 815dafe9..00000000 --- a/src/PhpPact/Consumer/Service/Helper/BodyTrait.php +++ /dev/null @@ -1,22 +0,0 @@ -ffi->pactffi_with_body($this->getId(), $part, $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } -} diff --git a/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php b/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php deleted file mode 100644 index d83903fc..00000000 --- a/src/PhpPact/Consumer/Service/Helper/DescriptionTrait.php +++ /dev/null @@ -1,16 +0,0 @@ -ffi->pactffi_upon_receiving($this->getId(), $description); - } -} diff --git a/src/PhpPact/Consumer/Service/Helper/FFITrait.php b/src/PhpPact/Consumer/Service/Helper/FFITrait.php deleted file mode 100644 index 710299cf..00000000 --- a/src/PhpPact/Consumer/Service/Helper/FFITrait.php +++ /dev/null @@ -1,21 +0,0 @@ -ffi = FFI::cdef($code, Scripts::getLibrary()); - } -} diff --git a/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php b/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php deleted file mode 100644 index a4b457ee..00000000 --- a/src/PhpPact/Consumer/Service/Helper/InteractionTrait.php +++ /dev/null @@ -1,13 +0,0 @@ -interactionId; - } -} diff --git a/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php b/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php deleted file mode 100644 index 776da088..00000000 --- a/src/PhpPact/Consumer/Service/Helper/ProviderStatesTrait.php +++ /dev/null @@ -1,24 +0,0 @@ -ffi->pactffi_given($this->getId(), $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->ffi->pactffi_given_with_param($this->getId(), $providerState->getName(), $key, $value); - } - } - } -} diff --git a/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php b/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php deleted file mode 100644 index 42358d88..00000000 --- a/src/PhpPact/Consumer/Service/Helper/SpecificationTrait.php +++ /dev/null @@ -1,24 +0,0 @@ -versionEqualTo('1.0.0') => $this->ffi->PactSpecification_V1, - $this->versionEqualTo('1.1.0') => $this->ffi->PactSpecification_V1_1, - $this->versionEqualTo('2.0.0') => $this->ffi->PactSpecification_V2, - $this->versionEqualTo('3.0.0') => $this->ffi->PactSpecification_V3, - $this->versionEqualTo('4.0.0') => $this->ffi->PactSpecification_V4, - default => function () { - trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); - - return $this->ffi->PactSpecification_Unknown; - }, - }; - } -} diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index f39390a0..1d892d81 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -2,20 +2,16 @@ namespace PhpPact\Consumer\Service; +use PhpPact\Consumer\Driver\Interaction\DriverInterface; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Model\Interaction; -use PhpPact\Consumer\Service\Helper\BodyTrait; -use PhpPact\Consumer\Service\Helper\DescriptionTrait; -use PhpPact\Consumer\Service\Helper\ProviderStatesTrait; class InteractionRegistry implements InteractionRegistryInterface { - use ProviderStatesTrait; - use DescriptionTrait; - use BodyTrait; - - public function __construct(private MockServerInterface $mockServer) - { - $this->createFFI(); + public function __construct( + private InteractionDriverInterface $driver, + private MockServerInterface $mockServer + ) { } public function verifyInteractions(): bool @@ -35,16 +31,14 @@ public function verifyInteractions(): bool public function registerInteraction(Interaction $interaction): bool { - $pactId = $this->mockServer->init(); $this - ->newInteraction($pactId, $interaction->getDescription()) + ->newInteraction($interaction) ->given($interaction) ->uponReceiving($interaction) ->with($interaction) - ->willRespondWith($interaction); - - $this->mockServer->start(); + ->willRespondWith($interaction) + ->startMockServer(); return true; } @@ -59,23 +53,23 @@ private function writePact(): void $this->mockServer->writePact(); } - private function newInteraction(int $pactId, string $description): self + private function newInteraction(Interaction $interaction): self { - $this->interactionId = $this->ffi->pactffi_new_interaction($pactId, $description); + $this->driver->newInteraction($interaction->getDescription()); return $this; } private function given(Interaction $interaction): self { - $this->setProviderStates($interaction->getProviderStates()); + $this->driver->setProviderStates($interaction->getProviderStates()); return $this; } private function uponReceiving(Interaction $interaction): self { - $this->setDescription($interaction->getDescription()); + $this->driver->setDescription($interaction->getDescription()); return $this; } @@ -83,10 +77,10 @@ private function uponReceiving(Interaction $interaction): self private function with(Interaction $interaction): self { $request = $interaction->getRequest(); - $this->ffi->pactffi_with_request($this->getId(), $request->getMethod(), $request->getPath()); - $this->setHeaders($this->ffi->InteractionPart_Request, $request->getHeaders()); - $this->setQuery($request->getQuery()); - $this->setBody($this->ffi->InteractionPart_Request, null, $request->getBody()); + $this->driver->setRequest($request->getMethod(), $request->getPath()); + $this->driver->setHeaders(DriverInterface::REQUEST, $request->getHeaders()); + $this->driver->setQuery($request->getQuery()); + $this->driver->setBody(DriverInterface::REQUEST, null, $request->getBody()); return $this; } @@ -94,34 +88,15 @@ private function with(Interaction $interaction): self private function willRespondWith(Interaction $interaction): self { $response = $interaction->getResponse(); - $this->ffi->pactffi_response_status($this->getId(), $response->getStatus()); - $this->setHeaders($this->ffi->InteractionPart_Response, $response->getHeaders()); - $this->setBody($this->ffi->InteractionPart_Response, null, $response->getBody()); + $this->driver->setResponse($response->getStatus()); + $this->driver->setHeaders(DriverInterface::RESPONSE, $response->getHeaders()); + $this->driver->setBody(DriverInterface::RESPONSE, null, $response->getBody()); return $this; } - /** - * @param array $headers - */ - private function setHeaders(int $part, array $headers): void - { - foreach ($headers as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_header_v2($this->getId(), $part, (string) $header, (int) $index, (string) $value); - } - } - } - - /** - * @param array $query - */ - private function setQuery(array $query): void + private function startMockServer(): void { - foreach ($query as $key => $values) { - foreach (array_values($values) as $index => $value) { - $this->ffi->pactffi_with_query_parameter_v2($this->getId(), (string) $key, (int) $index, (string) $value); - } - } + $this->mockServer->start(); } } diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index d2e7d841..4b4ecc8d 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -3,33 +3,25 @@ namespace PhpPact\Consumer\Service; use PhpPact\Config\PactConfigInterface; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; -use PhpPact\Consumer\Service\Helper\FFITrait; use PhpPact\Standalone\MockService\MockServerConfigInterface; class MockServer implements MockServerInterface { - use FFITrait; - public function __construct( - private PactRegistryInterface $pactRegistry, + private FFIInterface $ffi, + private PactDriverInterface $pactDriver, private MockServerConfigInterface $config ) { - $this->createFFI(); - } - - public function init(): int - { - $this->pactRegistry->registerPact(); - - return $this->pactRegistry->getId(); } public function start(): void { - $port = $this->ffi->pactffi_create_mock_server_for_transport( - $this->pactRegistry->getId(), + $port = $this->ffi->call( + 'pactffi_create_mock_server_for_transport', + $this->pactDriver->getId(), $this->config->getHost(), $this->config->getPort(), $this->getTransport(), @@ -44,12 +36,13 @@ public function start(): void public function isMatched(): bool { - return $this->ffi->pactffi_mock_server_matched($this->config->getPort()); + return $this->ffi->call('pactffi_mock_server_matched', $this->config->getPort()); } public function writePact(): void { - $error = $this->ffi->pactffi_write_pact_file( + $error = $this->ffi->call( + 'pactffi_write_pact_file', $this->config->getPort(), $this->config->getPactDir(), $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE @@ -61,8 +54,8 @@ public function writePact(): void public function cleanUp(): void { - $this->ffi->pactffi_cleanup_mock_server($this->config->getPort()); - $this->pactRegistry->cleanUp(); + $this->ffi->call('pactffi_cleanup_mock_server', $this->config->getPort()); + $this->pactDriver->cleanUp(); } protected function getTransport(): string diff --git a/src/PhpPact/Consumer/Service/MockServerInterface.php b/src/PhpPact/Consumer/Service/MockServerInterface.php index fd115d87..c1670d38 100644 --- a/src/PhpPact/Consumer/Service/MockServerInterface.php +++ b/src/PhpPact/Consumer/Service/MockServerInterface.php @@ -4,8 +4,6 @@ interface MockServerInterface { - public function init(): int; - public function start(): void; public function isMatched(): bool; diff --git a/src/PhpPact/Consumer/Service/PactRegistry.php b/src/PhpPact/Consumer/Service/PactRegistry.php deleted file mode 100644 index 32905048..00000000 --- a/src/PhpPact/Consumer/Service/PactRegistry.php +++ /dev/null @@ -1,80 +0,0 @@ -createFFI(); - $this->initWithLogLevel(); - } - - public function registerPact(): void - { - $this - ->newPact() - ->withSpecification(); - } - - public function getId(): int - { - return $this->pactId; - } - - public function cleanUp(): void - { - $this->ffi->pactffi_free_pact_handle($this->getId()); - unset($this->pactId); - } - - public function writePact(): void - { - $error = $this->ffi->pactffi_pact_handle_write_file( - $this->getId(), - $this->config->getPactDir(), - $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE - ); - if ($error) { - throw new PactFileNotWroteException($error); - } - } - - private function initWithLogLevel(): self - { - $logLevel = $this->config->getLogLevel(); - if ($logLevel) { - $this->ffi->pactffi_init_with_log_level($logLevel); - } - - return $this; - } - - private function newPact(): self - { - $this->pactId = $this->ffi->pactffi_new_pact($this->config->getConsumer(), $this->config->getProvider()); - - return $this; - } - - private function withSpecification(): self - { - $this->ffi->pactffi_with_specification($this->getId(), $this->getSpecification()); - - return $this; - } - - private function versionEqualTo(string $version): bool - { - return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); - } -} From 420f4c2f079e768217517e141112f46691c0e70a Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 14:21:00 +0700 Subject: [PATCH 026/298] Extract set up method --- src/PhpPact/Consumer/Driver/Pact/PactDriver.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index b1dd6549..bfeb006f 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -15,10 +15,7 @@ public function __construct( private FFIInterface $ffi, private PactConfigInterface $config ) { - $this - ->initWithLogLevel() - ->newPact() - ->withSpecification(); + $this->setUp(); } public function getId(): int @@ -45,6 +42,14 @@ public function writePact(): void } } + protected function setUp(): void + { + $this + ->initWithLogLevel() + ->newPact() + ->withSpecification(); + } + protected function getSpecification(): int { return match (true) { From 7f472fe530bbbf687a43db685497158557827e4d Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 14:21:44 +0700 Subject: [PATCH 027/298] Simplify default interaction registry factory --- .../Consumer/Factory/InteractionRegistryFactory.php | 10 ++++------ src/PhpPact/Consumer/InteractionBuilder.php | 2 +- src/PhpPact/Consumer/Service/InteractionRegistry.php | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php index 8dd13a9d..cea4c011 100644 --- a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php @@ -13,14 +13,12 @@ class InteractionRegistryFactory { - public static function create( - PactConfigInterface $pactConfig, - MockServerConfigInterface $mockServerConfig - ): InteractionRegistryInterface { + public static function create(MockServerConfigInterface $config): InteractionRegistryInterface + { $ffi = new FFI(); - $pactDriver = new PactDriver($ffi, $pactConfig); + $pactDriver = new PactDriver($ffi, $config); $interactionDriver = new InteractionDriver($ffi, $pactDriver); - $mockServer = new MockServer($ffi, $pactDriver, $mockServerConfig); + $mockServer = new MockServer($ffi, $pactDriver, $config); return new InteractionRegistry($interactionDriver, $mockServer); } diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 28e49e12..8480cd26 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -19,7 +19,7 @@ class InteractionBuilder implements BuilderInterface public function __construct(MockServerConfigInterface|InteractionRegistryInterface $registry) { - $this->registry = $registry instanceof InteractionRegistryInterface ? $registry : InteractionRegistryFactory::create($registry, $registry); + $this->registry = $registry instanceof InteractionRegistryInterface ? $registry : InteractionRegistryFactory::create($registry); $this->interaction = new Interaction(); } diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index 1d892d81..88f69370 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -31,7 +31,6 @@ public function verifyInteractions(): bool public function registerInteraction(Interaction $interaction): bool { - $this ->newInteraction($interaction) ->given($interaction) From c87dd62aca00a71e62ea85f02b6c5e50e35d53be Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 14:26:53 +0700 Subject: [PATCH 028/298] Move methods from AbstractDriver to InteractionDriver and rename --- .../Driver/Interaction/AbstractDriver.php | 30 -------------- .../Driver/Interaction/DriverInterface.php | 11 ----- .../Driver/Interaction/InteractionDriver.php | 41 ++++++++++++++++--- .../InteractionDriverInterface.php | 21 +++++++--- .../Consumer/Service/InteractionRegistry.php | 18 ++++---- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php index 2cc6249a..c6fac62b 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; -use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; use PhpPact\Consumer\Service\FFIInterface; abstract class AbstractDriver implements DriverInterface @@ -15,33 +14,4 @@ public function __construct( protected PactDriverInterface $pactDriver ) { } - - public function setBody(string $part, ?string $contentType = null, ?string $body = null): void - { - if (is_null($body)) { - return; - } - $success = $this->ffi->call('pactffi_with_body', $this->id, $this->ffi->get($part), $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } - - public function setDescription(string $description): void - { - $this->ffi->call('pactffi_upon_receiving', $this->id, $description); - } - - /** - * {@inheritdoc} - */ - public function setProviderStates(array $providerStates): void - { - foreach ($providerStates as $providerState) { - $this->ffi->call('pactffi_given', $this->id, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->ffi->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); - } - } - } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php index 0f806476..f31cface 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -2,21 +2,10 @@ namespace PhpPact\Consumer\Driver\Interaction; -use PhpPact\Consumer\Model\ProviderState; - interface DriverInterface { public const REQUEST = 'InteractionPart_Request'; public const RESPONSE = 'InteractionPart_Response'; public function newInteraction(string $description): void; - - public function setBody(string $part, ?string $contentType = null, ?string $body = null): void; - - public function setDescription(string $description): void; - - /** - * @param ProviderState[] $providerStates - */ - public function setProviderStates(array $providerStates): void; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index 7e838354..3ac8248b 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -2,6 +2,8 @@ namespace PhpPact\Consumer\Driver\Interaction; +use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; + class InteractionDriver extends AbstractDriver implements InteractionDriverInterface { public function newInteraction(string $description): void @@ -9,10 +11,39 @@ public function newInteraction(string $description): void $this->id = $this->ffi->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); } + public function uponReceiving(string $description): void + { + $this->ffi->call('pactffi_upon_receiving', $this->id, $description); + } + + public function withBody(string $part, ?string $contentType = null, ?string $body = null): void + { + if (is_null($body)) { + return; + } + $success = $this->ffi->call('pactffi_with_body', $this->id, $this->ffi->get($part), $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + /** + * {@inheritdoc} + */ + public function given(array $providerStates): void + { + foreach ($providerStates as $providerState) { + $this->ffi->call('pactffi_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->ffi->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + } + /** * {@inheritdoc} */ - public function setHeaders(string $part, array $headers): void + public function withHeaders(string $part, array $headers): void { foreach ($headers as $header => $values) { foreach (array_values($values) as $index => $value) { @@ -24,21 +55,21 @@ public function setHeaders(string $part, array $headers): void /** * {@inheritdoc} */ - public function setQuery(array $query): void + public function withQueryParameters(array $queryParams): void { - foreach ($query as $key => $values) { + foreach ($queryParams as $key => $values) { foreach (array_values($values) as $index => $value) { $this->ffi->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); } } } - public function setRequest(string $method, string $path): void + public function withRequest(string $method, string $path): void { $this->ffi->call('pactffi_with_request', $this->id, $method, $path); } - public function setResponse(int $status): void + public function withResponse(int $status): void { $this->ffi->call('pactffi_response_status', $this->id, $status); } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index 3ae38be8..f81047f6 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -2,19 +2,30 @@ namespace PhpPact\Consumer\Driver\Interaction; +use PhpPact\Consumer\Model\ProviderState; + interface InteractionDriverInterface extends DriverInterface { + public function uponReceiving(string $description): void; + + public function withBody(string $part, ?string $contentType = null, ?string $body = null): void; + + /** + * @param ProviderState[] $providerStates + */ + public function given(array $providerStates): void; + /** * @param array $headers */ - public function setHeaders(string $part, array $headers): void; + public function withHeaders(string $part, array $headers): void; /** - * @param array $query + * @param array $queryParams */ - public function setQuery(array $query): void; + public function withQueryParameters(array $queryParams): void; - public function setRequest(string $method, string $path): void; + public function withRequest(string $method, string $path): void; - public function setResponse(int $status): void; + public function withResponse(int $status): void; } diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index 88f69370..72822cfb 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -61,14 +61,14 @@ private function newInteraction(Interaction $interaction): self private function given(Interaction $interaction): self { - $this->driver->setProviderStates($interaction->getProviderStates()); + $this->driver->given($interaction->getProviderStates()); return $this; } private function uponReceiving(Interaction $interaction): self { - $this->driver->setDescription($interaction->getDescription()); + $this->driver->uponReceiving($interaction->getDescription()); return $this; } @@ -76,10 +76,10 @@ private function uponReceiving(Interaction $interaction): self private function with(Interaction $interaction): self { $request = $interaction->getRequest(); - $this->driver->setRequest($request->getMethod(), $request->getPath()); - $this->driver->setHeaders(DriverInterface::REQUEST, $request->getHeaders()); - $this->driver->setQuery($request->getQuery()); - $this->driver->setBody(DriverInterface::REQUEST, null, $request->getBody()); + $this->driver->withRequest($request->getMethod(), $request->getPath()); + $this->driver->withHeaders(DriverInterface::REQUEST, $request->getHeaders()); + $this->driver->withQueryParameters($request->getQuery()); + $this->driver->withBody(DriverInterface::REQUEST, null, $request->getBody()); return $this; } @@ -87,9 +87,9 @@ private function with(Interaction $interaction): self private function willRespondWith(Interaction $interaction): self { $response = $interaction->getResponse(); - $this->driver->setResponse($response->getStatus()); - $this->driver->setHeaders(DriverInterface::RESPONSE, $response->getHeaders()); - $this->driver->setBody(DriverInterface::RESPONSE, null, $response->getBody()); + $this->driver->withResponse($response->getStatus()); + $this->driver->withHeaders(DriverInterface::RESPONSE, $response->getHeaders()); + $this->driver->withBody(DriverInterface::RESPONSE, null, $response->getBody()); return $this; } From 53cef3d10012d248a6c2c6973a35f5bfa308a0ef Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 14:38:17 +0700 Subject: [PATCH 029/298] Group assertions together for better readability --- .../tests/Consumer/Service/ConsumerServiceGoodbyeTest.php | 8 ++++---- .../tests/Consumer/Service/ConsumerServiceHelloTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php index fcae2e02..b3140154 100644 --- a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php @@ -39,10 +39,10 @@ public function testGetGoodbyeString() ->willRespondWith($response); $service = new HttpClientService($config->getBaseUri()); - $result = $service->getGoodbyeString('Bob'); + $goodbyeResult = $service->getGoodbyeString('Bob'); + $verifyResult = $builder->verify(); - $this->assertTrue($builder->verify()); - - $this->assertEquals('Goodbye, Bob', $result); + $this->assertTrue($verifyResult); + $this->assertEquals('Goodbye, Bob', $goodbyeResult); } } diff --git a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php index 73b6b6f2..d10782ff 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php @@ -45,10 +45,10 @@ public function testGetHelloString() ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. $service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. - $result = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. + $helloResult = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. + $verifyResult = $builder->verify(); // This will verify that the interactions took place. - $this->assertTrue($builder->verify()); // This will verify that the interactions took place. - - $this->assertEquals('Hello, Bob', $result); // Make your assertions. + $this->assertTrue($verifyResult); // Make your assertions. + $this->assertEquals('Hello, Bob', $helloResult); } } From d5a4b831b8b24b88f414edc8ee2cce2143f1a38b Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 14:52:01 +0700 Subject: [PATCH 030/298] Remove condition to make code shorter --- src/PhpPact/Consumer/Model/ConsumerRequest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 3e98bae1..f8e11ae8 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -105,13 +105,11 @@ public function getBody(): ?string */ public function setBody(mixed $body): self { - if (\is_string($body)) { + if (\is_string($body) || \is_null($body)) { $this->body = $body; - } elseif (!\is_null($body)) { + } else { $this->body = \json_encode($body, JSON_THROW_ON_ERROR); $this->addHeader('Content-Type', 'application/json'); - } else { - $this->body = null; } return $this; From 6cf81286ed9e618efbc66fe0f8ee58b31f13583d Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 19:43:34 +0700 Subject: [PATCH 031/298] Update ffi library to 0.4.4 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c5270b90..3deedf5e 100644 --- a/composer.json +++ b/composer.json @@ -89,12 +89,12 @@ "path": "bin/pact-ruby-standalone" }, "pact-ffi-headers": { - "version": "0.4.1", + "version": "0.4.4", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.1", + "version": "0.4.4", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From 156f989f25a7f0101985231cabec7fb1ff7c9a54 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 5 May 2023 19:44:28 +0700 Subject: [PATCH 032/298] Remove not useful annotation --- src/PhpPact/Standalone/Installer/Model/Scripts.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 824577ea..243bf45a 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -19,9 +19,6 @@ public static function getHeader(): string return self::$destinationDir . '/bin/pact-ffi-headers/pact.h'; } - /** - * @return string - */ public static function getLibrary(): string { $extension = PHP_OS_FAMILY === 'Windows' ? 'dll' : (PHP_OS === 'Darwin' ? 'dylib' : 'so'); From bd8b7510ccbe0b8d89de83b1a4a4ff51bff1b71c Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 6 May 2023 21:50:05 +0700 Subject: [PATCH 033/298] Rename ffi service and move it to FFI namespace --- .../Driver/Interaction/AbstractDriver.php | 4 +- .../Driver/Interaction/DriverInterface.php | 3 -- .../Driver/Interaction/InteractionDriver.php | 39 +++++++++---------- .../InteractionDriverInterface.php | 4 +- .../Consumer/Driver/Pact/PactDriver.php | 26 ++++++------- .../Factory/InteractionRegistryFactory.php | 11 +++--- .../Consumer/Service/InteractionRegistry.php | 9 ++--- src/PhpPact/Consumer/Service/MockServer.php | 11 +++--- .../Exception/HeaderNotReadException.php | 2 +- .../Service/FFI.php => FFI/Proxy.php} | 15 +++---- .../ProxyInterface.php} | 4 +- 11 files changed, 60 insertions(+), 68 deletions(-) rename src/PhpPact/{Consumer => FFI}/Exception/HeaderNotReadException.php (66%) rename src/PhpPact/{Consumer/Service/FFI.php => FFI/Proxy.php} (63%) rename src/PhpPact/{Consumer/Service/FFIInterface.php => FFI/ProxyInterface.php} (75%) diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php index c6fac62b..887e9c78 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -3,14 +3,14 @@ namespace PhpPact\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; -use PhpPact\Consumer\Service\FFIInterface; +use PhpPact\FFI\ProxyInterface; abstract class AbstractDriver implements DriverInterface { protected int $id; public function __construct( - protected FFIInterface $ffi, + protected ProxyInterface $proxy, protected PactDriverInterface $pactDriver ) { } diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php index f31cface..d5a87739 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -4,8 +4,5 @@ interface DriverInterface { - public const REQUEST = 'InteractionPart_Request'; - public const RESPONSE = 'InteractionPart_Response'; - public function newInteraction(string $description): void; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index 3ac8248b..6b4692de 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -6,71 +6,70 @@ class InteractionDriver extends AbstractDriver implements InteractionDriverInterface { + private const REQUEST = 'InteractionPart_Request'; + private const RESPONSE = 'InteractionPart_Response'; + public function newInteraction(string $description): void { - $this->id = $this->ffi->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); + $this->id = $this->proxy->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); } public function uponReceiving(string $description): void { - $this->ffi->call('pactffi_upon_receiving', $this->id, $description); + $this->proxy->call('pactffi_upon_receiving', $this->id, $description); } - public function withBody(string $part, ?string $contentType = null, ?string $body = null): void + public function withBody(bool $isRequest, ?string $contentType = null, ?string $body = null): void { if (is_null($body)) { return; } - $success = $this->ffi->call('pactffi_with_body', $this->id, $this->ffi->get($part), $contentType, $body); + $success = $this->proxy->call('pactffi_with_body', $this->id, $this->getPart($isRequest), $contentType, $body); if (!$success) { throw new InteractionBodyNotAddedException(); } } - /** - * {@inheritdoc} - */ public function given(array $providerStates): void { foreach ($providerStates as $providerState) { - $this->ffi->call('pactffi_given', $this->id, $providerState->getName()); + $this->proxy->call('pactffi_given', $this->id, $providerState->getName()); foreach ($providerState->getParams() as $key => $value) { - $this->ffi->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + $this->proxy->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); } } } - /** - * {@inheritdoc} - */ - public function withHeaders(string $part, array $headers): void + public function withHeaders(bool $isRequest, array $headers): void { foreach ($headers as $header => $values) { foreach (array_values($values) as $index => $value) { - $this->ffi->call('pactffi_with_header_v2', $this->id, $this->ffi->get($part), (string) $header, (int) $index, (string) $value); + $this->proxy->call('pactffi_with_header_v2', $this->id, $this->getPart($isRequest), (string) $header, (int) $index, (string) $value); } } } - /** - * {@inheritdoc} - */ public function withQueryParameters(array $queryParams): void { foreach ($queryParams as $key => $values) { foreach (array_values($values) as $index => $value) { - $this->ffi->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); + $this->proxy->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); } } } public function withRequest(string $method, string $path): void { - $this->ffi->call('pactffi_with_request', $this->id, $method, $path); + $this->proxy->call('pactffi_with_request', $this->id, $method, $path); } public function withResponse(int $status): void { - $this->ffi->call('pactffi_response_status', $this->id, $status); + $this->proxy->call('pactffi_response_status', $this->id, $status); + } + + private function getPart(bool $isRequest): int + { + return $this->proxy->get($isRequest ? self::REQUEST : self::RESPONSE); } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index f81047f6..d868cc14 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -8,7 +8,7 @@ interface InteractionDriverInterface extends DriverInterface { public function uponReceiving(string $description): void; - public function withBody(string $part, ?string $contentType = null, ?string $body = null): void; + public function withBody(bool $isRequest, ?string $contentType = null, ?string $body = null): void; /** * @param ProviderState[] $providerStates @@ -18,7 +18,7 @@ public function given(array $providerStates): void; /** * @param array $headers */ - public function withHeaders(string $part, array $headers): void; + public function withHeaders(bool $isRequest, array $headers): void; /** * @param array $queryParams diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index bfeb006f..02197081 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -5,14 +5,14 @@ use Composer\Semver\Comparator; use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Exception\PactFileNotWroteException; -use PhpPact\Consumer\Service\FFIInterface; +use PhpPact\FFI\ProxyInterface; class PactDriver implements PactDriverInterface { protected int $id; public function __construct( - private FFIInterface $ffi, + private ProxyInterface $proxy, private PactConfigInterface $config ) { $this->setUp(); @@ -25,13 +25,13 @@ public function getId(): int public function cleanUp(): void { - $this->ffi->call('pactffi_free_pact_handle', $this->id); + $this->proxy->call('pactffi_free_pact_handle', $this->id); unset($this->id); } public function writePact(): void { - $error = $this->ffi->call( + $error = $this->proxy->call( 'pactffi_pact_handle_write_file', $this->id, $this->config->getPactDir(), @@ -53,15 +53,15 @@ protected function setUp(): void protected function getSpecification(): int { return match (true) { - $this->versionEqualTo('1.0.0') => $this->ffi->get('PactSpecification_V1'), - $this->versionEqualTo('1.1.0') => $this->ffi->get('PactSpecification_V1_1'), - $this->versionEqualTo('2.0.0') => $this->ffi->get('PactSpecification_V2'), - $this->versionEqualTo('3.0.0') => $this->ffi->get('PactSpecification_V3'), - $this->versionEqualTo('4.0.0') => $this->ffi->get('PactSpecification_V4'), + $this->versionEqualTo('1.0.0') => $this->proxy->get('PactSpecification_V1'), + $this->versionEqualTo('1.1.0') => $this->proxy->get('PactSpecification_V1_1'), + $this->versionEqualTo('2.0.0') => $this->proxy->get('PactSpecification_V2'), + $this->versionEqualTo('3.0.0') => $this->proxy->get('PactSpecification_V3'), + $this->versionEqualTo('4.0.0') => $this->proxy->get('PactSpecification_V4'), default => function () { trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); - return $this->ffi->get('PactSpecification_Unknown'); + return $this->proxy->get('PactSpecification_Unknown'); }, }; } @@ -75,7 +75,7 @@ private function initWithLogLevel(): self { $logLevel = $this->config->getLogLevel(); if ($logLevel) { - $this->ffi->call('pactffi_init_with_log_level', $logLevel); + $this->proxy->call('pactffi_init_with_log_level', $logLevel); } return $this; @@ -83,14 +83,14 @@ private function initWithLogLevel(): self private function newPact(): self { - $this->id = $this->ffi->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); + $this->id = $this->proxy->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); return $this; } private function withSpecification(): self { - $this->ffi->call('pactffi_with_specification', $this->id, $this->getSpecification()); + $this->proxy->call('pactffi_with_specification', $this->id, $this->getSpecification()); return $this; } diff --git a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php index cea4c011..b7a9ccf5 100644 --- a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php @@ -4,10 +4,9 @@ use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Pact\PactDriver; -use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Service\FFI; use PhpPact\Consumer\Service\InteractionRegistry; use PhpPact\Consumer\Service\InteractionRegistryInterface; +use PhpPact\FFI\Proxy; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; @@ -15,10 +14,10 @@ class InteractionRegistryFactory { public static function create(MockServerConfigInterface $config): InteractionRegistryInterface { - $ffi = new FFI(); - $pactDriver = new PactDriver($ffi, $config); - $interactionDriver = new InteractionDriver($ffi, $pactDriver); - $mockServer = new MockServer($ffi, $pactDriver, $config); + $proxy = new Proxy(); + $pactDriver = new PactDriver($proxy, $config); + $interactionDriver = new InteractionDriver($proxy, $pactDriver); + $mockServer = new MockServer($proxy, $pactDriver, $config); return new InteractionRegistry($interactionDriver, $mockServer); } diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index 72822cfb..2bf1871e 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -2,7 +2,6 @@ namespace PhpPact\Consumer\Service; -use PhpPact\Consumer\Driver\Interaction\DriverInterface; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Model\Interaction; @@ -77,9 +76,9 @@ private function with(Interaction $interaction): self { $request = $interaction->getRequest(); $this->driver->withRequest($request->getMethod(), $request->getPath()); - $this->driver->withHeaders(DriverInterface::REQUEST, $request->getHeaders()); + $this->driver->withHeaders(true, $request->getHeaders()); $this->driver->withQueryParameters($request->getQuery()); - $this->driver->withBody(DriverInterface::REQUEST, null, $request->getBody()); + $this->driver->withBody(true, null, $request->getBody()); return $this; } @@ -88,8 +87,8 @@ private function willRespondWith(Interaction $interaction): self { $response = $interaction->getResponse(); $this->driver->withResponse($response->getStatus()); - $this->driver->withHeaders(DriverInterface::RESPONSE, $response->getHeaders()); - $this->driver->withBody(DriverInterface::RESPONSE, null, $response->getBody()); + $this->driver->withHeaders(false, $response->getHeaders()); + $this->driver->withBody(false, null, $response->getBody()); return $this; } diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index 4b4ecc8d..edbc5158 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -6,12 +6,13 @@ use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; +use PhpPact\FFI\ProxyInterface; use PhpPact\Standalone\MockService\MockServerConfigInterface; class MockServer implements MockServerInterface { public function __construct( - private FFIInterface $ffi, + private ProxyInterface $proxy, private PactDriverInterface $pactDriver, private MockServerConfigInterface $config ) { @@ -19,7 +20,7 @@ public function __construct( public function start(): void { - $port = $this->ffi->call( + $port = $this->proxy->call( 'pactffi_create_mock_server_for_transport', $this->pactDriver->getId(), $this->config->getHost(), @@ -36,12 +37,12 @@ public function start(): void public function isMatched(): bool { - return $this->ffi->call('pactffi_mock_server_matched', $this->config->getPort()); + return $this->proxy->call('pactffi_mock_server_matched', $this->config->getPort()); } public function writePact(): void { - $error = $this->ffi->call( + $error = $this->proxy->call( 'pactffi_write_pact_file', $this->config->getPort(), $this->config->getPactDir(), @@ -54,7 +55,7 @@ public function writePact(): void public function cleanUp(): void { - $this->ffi->call('pactffi_cleanup_mock_server', $this->config->getPort()); + $this->proxy->call('pactffi_cleanup_mock_server', $this->config->getPort()); $this->pactDriver->cleanUp(); } diff --git a/src/PhpPact/Consumer/Exception/HeaderNotReadException.php b/src/PhpPact/FFI/Exception/HeaderNotReadException.php similarity index 66% rename from src/PhpPact/Consumer/Exception/HeaderNotReadException.php rename to src/PhpPact/FFI/Exception/HeaderNotReadException.php index 95655deb..d521f224 100644 --- a/src/PhpPact/Consumer/Exception/HeaderNotReadException.php +++ b/src/PhpPact/FFI/Exception/HeaderNotReadException.php @@ -1,6 +1,6 @@ ffi = CoreFFI::cdef($code, Scripts::getLibrary()); + $this->ffi = FFI::cdef($code, Scripts::getLibrary()); } - /** - * {@inheritdoc} - */ public function call(string $name, ...$arguments): mixed { return $this->ffi->{$name}(...$arguments); diff --git a/src/PhpPact/Consumer/Service/FFIInterface.php b/src/PhpPact/FFI/ProxyInterface.php similarity index 75% rename from src/PhpPact/Consumer/Service/FFIInterface.php rename to src/PhpPact/FFI/ProxyInterface.php index c3b5e20a..9f4a1a29 100644 --- a/src/PhpPact/Consumer/Service/FFIInterface.php +++ b/src/PhpPact/FFI/ProxyInterface.php @@ -1,8 +1,8 @@ $arguments From d41e1ac04dcb779b5b3d669d6910d2890e35e8d1 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 8 May 2023 12:01:07 +0700 Subject: [PATCH 034/298] Assign result to variable before asserting --- README.md | 3 ++- tests/PhpPact/Consumer/InteractionBuilderTest.php | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 115e75a9..e208e86d 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,8 @@ Verify that all interactions took place that were registered. This typically should be in each test, that way the test that failed to verify is marked correctly. ```php -$this->assertTrue($builder->verify()); +$verifyResult = $builder->verify(); +$this->assertTrue($verifyResult); ``` ### Make Assertions diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index a385cf03..6d97ccbf 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -42,7 +42,9 @@ public function testSimpleGet() ->with($request) ->willRespondWith($response); - $this->assertFalse($builder->verify()); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } /** @@ -80,7 +82,9 @@ public function testPostWithBody() ->with($request) ->willRespondWith($response); - $this->assertFalse($builder->verify()); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } /** @@ -114,6 +118,8 @@ public function testBuildWithEachLikeMatcher() ->with($request) ->willRespondWith($response); - $this->assertFalse($builder->verify()); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } } From cc7f14c62c47531ab49608f38cf7502d463019a0 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 8 May 2023 12:19:20 +0700 Subject: [PATCH 035/298] Change type hint from 'mixed' to 'string|array|null' --- src/PhpPact/Consumer/Model/ConsumerRequest.php | 4 +++- src/PhpPact/Consumer/Model/ProviderResponse.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index f8e11ae8..4632dee1 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -101,9 +101,11 @@ public function getBody(): ?string } /** + * @param array|string|null $body + * * @throws JsonException */ - public function setBody(mixed $body): self + public function setBody(array|string|null $body): self { if (\is_string($body) || \is_null($body)) { $this->body = $body; diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index b4459384..4ec6ba51 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -77,9 +77,11 @@ public function getBody(): ?string } /** + * @param array|string|null $body + * * @throws JsonException */ - public function setBody(mixed $body): self + public function setBody(array|string|null $body): self { if (\is_string($body) || \is_null($body)) { $this->body = $body; From dfaa021edb3772fa38e270484f2f81a43c1bbaf7 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 8 May 2023 12:24:55 +0700 Subject: [PATCH 036/298] Add @throws annotation to setPactFileWriteMode() method declaration because implementation throws it --- src/PhpPact/Config/PactConfigInterface.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PhpPact/Config/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php index d952a725..0f97bd11 100644 --- a/src/PhpPact/Config/PactConfigInterface.php +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -2,6 +2,8 @@ namespace PhpPact\Config; +use InvalidArgumentException; + /** * Mock Server configuration interface to allow for simple overrides that are reusable. */ @@ -67,6 +69,8 @@ public function getPactFileWriteMode(): string; /** * @param string $pactFileWriteMode 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten + * + * @throws InvalidArgumentException If mode is incorrect. */ public function setPactFileWriteMode(string $pactFileWriteMode): self; } From 693daad1f5f47ca13c7c6c1a0b7e9ee3cedff3ac Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 8 May 2023 12:41:20 +0700 Subject: [PATCH 037/298] Rename FFI Proxy to Client --- .../Driver/Interaction/AbstractDriver.php | 4 +-- .../Driver/Interaction/InteractionDriver.php | 20 +++++++------- .../Consumer/Driver/Pact/PactDriver.php | 26 +++++++++---------- .../Factory/InteractionRegistryFactory.php | 10 +++---- src/PhpPact/Consumer/Service/MockServer.php | 12 ++++----- src/PhpPact/FFI/{Proxy.php => Client.php} | 2 +- ...ProxyInterface.php => ClientInterface.php} | 2 +- 7 files changed, 38 insertions(+), 38 deletions(-) rename src/PhpPact/FFI/{Proxy.php => Client.php} (94%) rename src/PhpPact/FFI/{ProxyInterface.php => ClientInterface.php} (88%) diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php index 887e9c78..105e413b 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -3,14 +3,14 @@ namespace PhpPact\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; -use PhpPact\FFI\ProxyInterface; +use PhpPact\FFI\ClientInterface; abstract class AbstractDriver implements DriverInterface { protected int $id; public function __construct( - protected ProxyInterface $proxy, + protected ClientInterface $client, protected PactDriverInterface $pactDriver ) { } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index 6b4692de..66279746 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -11,12 +11,12 @@ class InteractionDriver extends AbstractDriver implements InteractionDriverInter public function newInteraction(string $description): void { - $this->id = $this->proxy->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); + $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); } public function uponReceiving(string $description): void { - $this->proxy->call('pactffi_upon_receiving', $this->id, $description); + $this->client->call('pactffi_upon_receiving', $this->id, $description); } public function withBody(bool $isRequest, ?string $contentType = null, ?string $body = null): void @@ -24,7 +24,7 @@ public function withBody(bool $isRequest, ?string $contentType = null, ?string $ if (is_null($body)) { return; } - $success = $this->proxy->call('pactffi_with_body', $this->id, $this->getPart($isRequest), $contentType, $body); + $success = $this->client->call('pactffi_with_body', $this->id, $this->getPart($isRequest), $contentType, $body); if (!$success) { throw new InteractionBodyNotAddedException(); } @@ -33,9 +33,9 @@ public function withBody(bool $isRequest, ?string $contentType = null, ?string $ public function given(array $providerStates): void { foreach ($providerStates as $providerState) { - $this->proxy->call('pactffi_given', $this->id, $providerState->getName()); + $this->client->call('pactffi_given', $this->id, $providerState->getName()); foreach ($providerState->getParams() as $key => $value) { - $this->proxy->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); } } } @@ -44,7 +44,7 @@ public function withHeaders(bool $isRequest, array $headers): void { foreach ($headers as $header => $values) { foreach (array_values($values) as $index => $value) { - $this->proxy->call('pactffi_with_header_v2', $this->id, $this->getPart($isRequest), (string) $header, (int) $index, (string) $value); + $this->client->call('pactffi_with_header_v2', $this->id, $this->getPart($isRequest), (string) $header, (int) $index, (string) $value); } } } @@ -53,23 +53,23 @@ public function withQueryParameters(array $queryParams): void { foreach ($queryParams as $key => $values) { foreach (array_values($values) as $index => $value) { - $this->proxy->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); + $this->client->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); } } } public function withRequest(string $method, string $path): void { - $this->proxy->call('pactffi_with_request', $this->id, $method, $path); + $this->client->call('pactffi_with_request', $this->id, $method, $path); } public function withResponse(int $status): void { - $this->proxy->call('pactffi_response_status', $this->id, $status); + $this->client->call('pactffi_response_status', $this->id, $status); } private function getPart(bool $isRequest): int { - return $this->proxy->get($isRequest ? self::REQUEST : self::RESPONSE); + return $this->client->get($isRequest ? self::REQUEST : self::RESPONSE); } } diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index 02197081..eda28ab6 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -5,14 +5,14 @@ use Composer\Semver\Comparator; use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Exception\PactFileNotWroteException; -use PhpPact\FFI\ProxyInterface; +use PhpPact\FFI\ClientInterface; class PactDriver implements PactDriverInterface { protected int $id; public function __construct( - private ProxyInterface $proxy, + private ClientInterface $client, private PactConfigInterface $config ) { $this->setUp(); @@ -25,13 +25,13 @@ public function getId(): int public function cleanUp(): void { - $this->proxy->call('pactffi_free_pact_handle', $this->id); + $this->client->call('pactffi_free_pact_handle', $this->id); unset($this->id); } public function writePact(): void { - $error = $this->proxy->call( + $error = $this->client->call( 'pactffi_pact_handle_write_file', $this->id, $this->config->getPactDir(), @@ -53,15 +53,15 @@ protected function setUp(): void protected function getSpecification(): int { return match (true) { - $this->versionEqualTo('1.0.0') => $this->proxy->get('PactSpecification_V1'), - $this->versionEqualTo('1.1.0') => $this->proxy->get('PactSpecification_V1_1'), - $this->versionEqualTo('2.0.0') => $this->proxy->get('PactSpecification_V2'), - $this->versionEqualTo('3.0.0') => $this->proxy->get('PactSpecification_V3'), - $this->versionEqualTo('4.0.0') => $this->proxy->get('PactSpecification_V4'), + $this->versionEqualTo('1.0.0') => $this->client->get('PactSpecification_V1'), + $this->versionEqualTo('1.1.0') => $this->client->get('PactSpecification_V1_1'), + $this->versionEqualTo('2.0.0') => $this->client->get('PactSpecification_V2'), + $this->versionEqualTo('3.0.0') => $this->client->get('PactSpecification_V3'), + $this->versionEqualTo('4.0.0') => $this->client->get('PactSpecification_V4'), default => function () { trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); - return $this->proxy->get('PactSpecification_Unknown'); + return $this->client->get('PactSpecification_Unknown'); }, }; } @@ -75,7 +75,7 @@ private function initWithLogLevel(): self { $logLevel = $this->config->getLogLevel(); if ($logLevel) { - $this->proxy->call('pactffi_init_with_log_level', $logLevel); + $this->client->call('pactffi_init_with_log_level', $logLevel); } return $this; @@ -83,14 +83,14 @@ private function initWithLogLevel(): self private function newPact(): self { - $this->id = $this->proxy->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); + $this->id = $this->client->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); return $this; } private function withSpecification(): self { - $this->proxy->call('pactffi_with_specification', $this->id, $this->getSpecification()); + $this->client->call('pactffi_with_specification', $this->id, $this->getSpecification()); return $this; } diff --git a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php index b7a9ccf5..bdf392f1 100644 --- a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php @@ -6,7 +6,7 @@ use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Service\InteractionRegistry; use PhpPact\Consumer\Service\InteractionRegistryInterface; -use PhpPact\FFI\Proxy; +use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; @@ -14,10 +14,10 @@ class InteractionRegistryFactory { public static function create(MockServerConfigInterface $config): InteractionRegistryInterface { - $proxy = new Proxy(); - $pactDriver = new PactDriver($proxy, $config); - $interactionDriver = new InteractionDriver($proxy, $pactDriver); - $mockServer = new MockServer($proxy, $pactDriver, $config); + $client = new Client(); + $pactDriver = new PactDriver($client, $config); + $interactionDriver = new InteractionDriver($client, $pactDriver); + $mockServer = new MockServer($client, $pactDriver, $config); return new InteractionRegistry($interactionDriver, $mockServer); } diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index edbc5158..31378257 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -6,13 +6,13 @@ use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; -use PhpPact\FFI\ProxyInterface; +use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\MockServerConfigInterface; class MockServer implements MockServerInterface { public function __construct( - private ProxyInterface $proxy, + private ClientInterface $client, private PactDriverInterface $pactDriver, private MockServerConfigInterface $config ) { @@ -20,7 +20,7 @@ public function __construct( public function start(): void { - $port = $this->proxy->call( + $port = $this->client->call( 'pactffi_create_mock_server_for_transport', $this->pactDriver->getId(), $this->config->getHost(), @@ -37,12 +37,12 @@ public function start(): void public function isMatched(): bool { - return $this->proxy->call('pactffi_mock_server_matched', $this->config->getPort()); + return $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); } public function writePact(): void { - $error = $this->proxy->call( + $error = $this->client->call( 'pactffi_write_pact_file', $this->config->getPort(), $this->config->getPactDir(), @@ -55,7 +55,7 @@ public function writePact(): void public function cleanUp(): void { - $this->proxy->call('pactffi_cleanup_mock_server', $this->config->getPort()); + $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); $this->pactDriver->cleanUp(); } diff --git a/src/PhpPact/FFI/Proxy.php b/src/PhpPact/FFI/Client.php similarity index 94% rename from src/PhpPact/FFI/Proxy.php rename to src/PhpPact/FFI/Client.php index e36f1ba6..38b54a2b 100644 --- a/src/PhpPact/FFI/Proxy.php +++ b/src/PhpPact/FFI/Client.php @@ -6,7 +6,7 @@ use PhpPact\FFI\Exception\HeaderNotReadException; use PhpPact\Standalone\Installer\Model\Scripts; -class Proxy implements ProxyInterface +class Client implements ClientInterface { private FFI $ffi; diff --git a/src/PhpPact/FFI/ProxyInterface.php b/src/PhpPact/FFI/ClientInterface.php similarity index 88% rename from src/PhpPact/FFI/ProxyInterface.php rename to src/PhpPact/FFI/ClientInterface.php index 9f4a1a29..c0e767ff 100644 --- a/src/PhpPact/FFI/ProxyInterface.php +++ b/src/PhpPact/FFI/ClientInterface.php @@ -2,7 +2,7 @@ namespace PhpPact\FFI; -interface ProxyInterface +interface ClientInterface { /** * @param array $arguments From d9602212afe36d42f1c332a5bb5bc02415729470 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 8 May 2023 14:21:12 +0700 Subject: [PATCH 038/298] Extract interaction part drivers --- .../Driver/Interaction/AbstractDriver.php | 5 ++ .../Driver/Interaction/DriverInterface.php | 2 + .../Driver/Interaction/InteractionDriver.php | 49 ------------------- .../InteractionDriverInterface.php | 16 ------ .../Interaction/Part/AbstractPartDriver.php | 43 ++++++++++++++++ .../Interaction/Part/PartDriverInterface.php | 13 +++++ .../Driver/Interaction/Part/RequestDriver.php | 25 ++++++++++ .../Part/RequestDriverInterface.php | 13 +++++ .../Interaction/Part/ResponseDriver.php | 16 ++++++ .../Part/ResponseDriverInterface.php | 8 +++ .../Factory/InteractionRegistryFactory.php | 6 ++- .../Consumer/Service/InteractionRegistry.php | 26 +++++----- 12 files changed, 145 insertions(+), 77 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php index 105e413b..1a035e7f 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -14,4 +14,9 @@ public function __construct( protected PactDriverInterface $pactDriver ) { } + + public function getId(): int + { + return $this->id; + } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php index d5a87739..4020bf1d 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -4,5 +4,7 @@ interface DriverInterface { + public function getId(): int; + public function newInteraction(string $description): void; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index 66279746..aeb51c27 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -2,13 +2,8 @@ namespace PhpPact\Consumer\Driver\Interaction; -use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; - class InteractionDriver extends AbstractDriver implements InteractionDriverInterface { - private const REQUEST = 'InteractionPart_Request'; - private const RESPONSE = 'InteractionPart_Response'; - public function newInteraction(string $description): void { $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); @@ -19,17 +14,6 @@ public function uponReceiving(string $description): void $this->client->call('pactffi_upon_receiving', $this->id, $description); } - public function withBody(bool $isRequest, ?string $contentType = null, ?string $body = null): void - { - if (is_null($body)) { - return; - } - $success = $this->client->call('pactffi_with_body', $this->id, $this->getPart($isRequest), $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } - public function given(array $providerStates): void { foreach ($providerStates as $providerState) { @@ -39,37 +23,4 @@ public function given(array $providerStates): void } } } - - public function withHeaders(bool $isRequest, array $headers): void - { - foreach ($headers as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->client->call('pactffi_with_header_v2', $this->id, $this->getPart($isRequest), (string) $header, (int) $index, (string) $value); - } - } - } - - public function withQueryParameters(array $queryParams): void - { - foreach ($queryParams as $key => $values) { - foreach (array_values($values) as $index => $value) { - $this->client->call('pactffi_with_query_parameter_v2', $this->id, (string) $key, (int) $index, (string) $value); - } - } - } - - public function withRequest(string $method, string $path): void - { - $this->client->call('pactffi_with_request', $this->id, $method, $path); - } - - public function withResponse(int $status): void - { - $this->client->call('pactffi_response_status', $this->id, $status); - } - - private function getPart(bool $isRequest): int - { - return $this->client->get($isRequest ? self::REQUEST : self::RESPONSE); - } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index d868cc14..72554701 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -8,24 +8,8 @@ interface InteractionDriverInterface extends DriverInterface { public function uponReceiving(string $description): void; - public function withBody(bool $isRequest, ?string $contentType = null, ?string $body = null): void; - /** * @param ProviderState[] $providerStates */ public function given(array $providerStates): void; - - /** - * @param array $headers - */ - public function withHeaders(bool $isRequest, array $headers): void; - - /** - * @param array $queryParams - */ - public function withQueryParameters(array $queryParams): void; - - public function withRequest(string $method, string $path): void; - - public function withResponse(int $status): void; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php new file mode 100644 index 00000000..a9fc2326 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php @@ -0,0 +1,43 @@ +client->call('pactffi_with_body', $this->getInteractionId(), $this->getPart(), $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + public function withHeaders(array $headers): void + { + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_header_v2', $this->getInteractionId(), $this->getPart(), (string) $header, (int) $index, (string) $value); + } + } + } + + protected function getInteractionId(): int + { + return $this->interactionDriver->getId(); + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php new file mode 100644 index 00000000..37d58fc8 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php @@ -0,0 +1,13 @@ + $headers + */ + public function withHeaders(array $headers): void; +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php new file mode 100644 index 00000000..23345a40 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php @@ -0,0 +1,25 @@ + $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_query_parameter_v2', $this->getInteractionId(), (string) $key, (int) $index, (string) $value); + } + } + } + + public function withRequest(string $method, string $path): void + { + $this->client->call('pactffi_with_request', $this->getInteractionId(), $method, $path); + } + + protected function getPart(): int + { + return $this->client->get('InteractionPart_Request'); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php new file mode 100644 index 00000000..de19cbb0 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php @@ -0,0 +1,13 @@ + $queryParams + */ + public function withQueryParameters(array $queryParams): void; + + public function withRequest(string $method, string $path): void; +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php new file mode 100644 index 00000000..481fd00d --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php @@ -0,0 +1,16 @@ +client->call('pactffi_response_status', $this->getInteractionId(), $status); + } + + protected function getPart(): int + { + return $this->client->get('InteractionPart_Response'); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php new file mode 100644 index 00000000..71e5a62c --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php @@ -0,0 +1,8 @@ +driver->newInteraction($interaction->getDescription()); + $this->interactionDriver->newInteraction($interaction->getDescription()); return $this; } private function given(Interaction $interaction): self { - $this->driver->given($interaction->getProviderStates()); + $this->interactionDriver->given($interaction->getProviderStates()); return $this; } private function uponReceiving(Interaction $interaction): self { - $this->driver->uponReceiving($interaction->getDescription()); + $this->interactionDriver->uponReceiving($interaction->getDescription()); return $this; } @@ -75,10 +79,10 @@ private function uponReceiving(Interaction $interaction): self private function with(Interaction $interaction): self { $request = $interaction->getRequest(); - $this->driver->withRequest($request->getMethod(), $request->getPath()); - $this->driver->withHeaders(true, $request->getHeaders()); - $this->driver->withQueryParameters($request->getQuery()); - $this->driver->withBody(true, null, $request->getBody()); + $this->requestDriver->withRequest($request->getMethod(), $request->getPath()); + $this->requestDriver->withHeaders($request->getHeaders()); + $this->requestDriver->withQueryParameters($request->getQuery()); + $this->requestDriver->withBody(null, $request->getBody()); return $this; } @@ -86,9 +90,9 @@ private function with(Interaction $interaction): self private function willRespondWith(Interaction $interaction): self { $response = $interaction->getResponse(); - $this->driver->withResponse($response->getStatus()); - $this->driver->withHeaders(false, $response->getHeaders()); - $this->driver->withBody(false, null, $response->getBody()); + $this->responseDriver->withResponse($response->getStatus()); + $this->responseDriver->withHeaders($response->getHeaders()); + $this->responseDriver->withBody(null, $response->getBody()); return $this; } From 770e6e0c50cc69a846af597c376261f6500f7473 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 9 May 2023 10:33:24 +0700 Subject: [PATCH 039/298] Extract interaction contents drivers --- .../Contents/AbstractBodyDriver.php | 29 +++++++ .../Contents/ContentsDriverInterface.php | 8 ++ .../Contents/RequestBodyDriver.php | 11 +++ .../Contents/ResponseBodyDriver.php | 11 +++ .../Driver/Interaction/DriverInterface.php | 2 +- .../Driver/Interaction/InteractionDriver.php | 56 ++++++++++++- .../InteractionDriverInterface.php | 10 ++- .../Interaction/Part/AbstractPartDriver.php | 21 +++-- .../Interaction/Part/PartDriverInterface.php | 4 +- .../Driver/Interaction/Part/RequestDriver.php | 21 ++++- .../Part/RequestDriverInterface.php | 4 +- .../Interaction/Part/ResponseDriver.php | 17 +++- .../Part/ResponseDriverInterface.php | 2 +- .../Factory/InteractionRegistryFactory.php | 6 +- .../Consumer/Service/InteractionRegistry.php | 80 +++---------------- 15 files changed, 182 insertions(+), 100 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/AbstractBodyDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/AbstractBodyDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/AbstractBodyDriver.php new file mode 100644 index 00000000..44782cff --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Contents/AbstractBodyDriver.php @@ -0,0 +1,29 @@ +client->call('pactffi_with_body', $this->interactionDriver->getId(), $this->getPart(), $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php new file mode 100644 index 00000000..5b87acea --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php @@ -0,0 +1,8 @@ +client->get('InteractionPart_Request'); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php new file mode 100644 index 00000000..78d8c90d --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php @@ -0,0 +1,11 @@ +client->get('InteractionPart_Response'); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php index 4020bf1d..b7a85311 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -6,5 +6,5 @@ interface DriverInterface { public function getId(): int; - public function newInteraction(string $description): void; + public function newInteraction(string $description): static; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index aeb51c27..f4e554d5 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -2,19 +2,46 @@ namespace PhpPact\Consumer\Driver\Interaction; +use PhpPact\Consumer\Driver\Interaction\Part\RequestDriver; +use PhpPact\Consumer\Driver\Interaction\Part\RequestDriverInterface; +use PhpPact\Consumer\Driver\Interaction\Part\ResponseDriver; +use PhpPact\Consumer\Driver\Interaction\Part\ResponseDriverInterface; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; +use PhpPact\Consumer\Model\ConsumerRequest; +use PhpPact\Consumer\Model\ProviderResponse; +use PhpPact\FFI\ClientInterface; + class InteractionDriver extends AbstractDriver implements InteractionDriverInterface { - public function newInteraction(string $description): void + private RequestDriverInterface $requestDriver; + private ResponseDriverInterface $responseDriver; + + public function __construct( + ClientInterface $client, + PactDriverInterface $pactDriver, + ?RequestDriverInterface $requestDriver = null, + ?ResponseDriverInterface $responseDriver = null, + ) { + parent::__construct($client, $pactDriver); + $this->requestDriver = $requestDriver ?? new RequestDriver($client, $this); + $this->responseDriver = $responseDriver ?? new ResponseDriver($client, $this); + } + + public function newInteraction(string $description): static { $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); + + return $this; } - public function uponReceiving(string $description): void + public function uponReceiving(string $description): self { $this->client->call('pactffi_upon_receiving', $this->id, $description); + + return $this; } - public function given(array $providerStates): void + public function given(array $providerStates): self { foreach ($providerStates as $providerState) { $this->client->call('pactffi_given', $this->id, $providerState->getName()); @@ -22,5 +49,28 @@ public function given(array $providerStates): void $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); } } + + return $this; + } + + public function with(ConsumerRequest $request): self + { + $this->requestDriver + ->withRequest($request->getMethod(), $request->getPath()) + ->withQueryParameters($request->getQuery()) + ->withHeaders($request->getHeaders()) + ->withBody(null, $request->getBody()); + + return $this; + } + + public function willRespondWith(ProviderResponse $response): self + { + $this->responseDriver + ->withResponse($response->getStatus()) + ->withHeaders($response->getHeaders()) + ->withBody(null, $response->getBody()); + + return $this; } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index 72554701..762ff99d 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -2,14 +2,20 @@ namespace PhpPact\Consumer\Driver\Interaction; +use PhpPact\Consumer\Model\ConsumerRequest; +use PhpPact\Consumer\Model\ProviderResponse; use PhpPact\Consumer\Model\ProviderState; interface InteractionDriverInterface extends DriverInterface { - public function uponReceiving(string $description): void; + public function uponReceiving(string $description): self; /** * @param ProviderState[] $providerStates */ - public function given(array $providerStates): void; + public function given(array $providerStates): self; + + public function with(ConsumerRequest $request): self; + + public function willRespondWith(ProviderResponse $response): self; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php index a9fc2326..79b27e03 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/AbstractPartDriver.php @@ -2,36 +2,35 @@ namespace PhpPact\Consumer\Driver\Interaction\Part; +use PhpPact\Consumer\Driver\Interaction\Contents\ContentsDriverInterface; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; -use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; use PhpPact\FFI\ClientInterface; abstract class AbstractPartDriver implements PartDriverInterface { public function __construct( protected ClientInterface $client, - protected InteractionDriverInterface $interactionDriver + protected InteractionDriverInterface $interactionDriver, + private ContentsDriverInterface $contentsDriver ) { } - public function withBody(?string $contentType = null, ?string $body = null): void + public function withBody(?string $contentType = null, ?string $body = null): self { - if (is_null($body)) { - return; - } - $success = $this->client->call('pactffi_with_body', $this->getInteractionId(), $this->getPart(), $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } + $this->contentsDriver->withContents($contentType, $body); + + return $this; } - public function withHeaders(array $headers): void + public function withHeaders(array $headers): self { foreach ($headers as $header => $values) { foreach (array_values($values) as $index => $value) { $this->client->call('pactffi_with_header_v2', $this->getInteractionId(), $this->getPart(), (string) $header, (int) $index, (string) $value); } } + + return $this; } protected function getInteractionId(): int diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php index 37d58fc8..417b18cb 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php @@ -4,10 +4,10 @@ interface PartDriverInterface { - public function withBody(?string $contentType = null, ?string $body = null): void; + public function withBody(?string $contentType = null, ?string $body = null): self; /** * @param array $headers */ - public function withHeaders(array $headers): void; + public function withHeaders(array $headers): self; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php index 23345a40..37e790be 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php @@ -2,20 +2,37 @@ namespace PhpPact\Consumer\Driver\Interaction\Part; +use PhpPact\Consumer\Driver\Interaction\Contents\ContentsDriverInterface; +use PhpPact\Consumer\Driver\Interaction\Contents\RequestBodyDriver; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\FFI\ClientInterface; + class RequestDriver extends AbstractPartDriver implements RequestDriverInterface { - public function withQueryParameters(array $queryParams): void + public function __construct( + ClientInterface $client, + InteractionDriverInterface $interactionDriver, + ?ContentsDriverInterface $requestBodyDriver = null + ) { + parent::__construct($client, $interactionDriver, $requestBodyDriver ?? new RequestBodyDriver($client, $interactionDriver)); + } + + public function withQueryParameters(array $queryParams): self { foreach ($queryParams as $key => $values) { foreach (array_values($values) as $index => $value) { $this->client->call('pactffi_with_query_parameter_v2', $this->getInteractionId(), (string) $key, (int) $index, (string) $value); } } + + return $this; } - public function withRequest(string $method, string $path): void + public function withRequest(string $method, string $path): self { $this->client->call('pactffi_with_request', $this->getInteractionId(), $method, $path); + + return $this; } protected function getPart(): int diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php index de19cbb0..febd69aa 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriverInterface.php @@ -7,7 +7,7 @@ interface RequestDriverInterface extends PartDriverInterface /** * @param array $queryParams */ - public function withQueryParameters(array $queryParams): void; + public function withQueryParameters(array $queryParams): self; - public function withRequest(string $method, string $path): void; + public function withRequest(string $method, string $path): self; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php index 481fd00d..9e59702e 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php @@ -2,11 +2,26 @@ namespace PhpPact\Consumer\Driver\Interaction\Part; +use PhpPact\Consumer\Driver\Interaction\Contents\ContentsDriverInterface; +use PhpPact\Consumer\Driver\Interaction\Contents\ResponseBodyDriver; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\FFI\ClientInterface; + class ResponseDriver extends AbstractPartDriver implements ResponseDriverInterface { - public function withResponse(int $status): void + public function __construct( + ClientInterface $client, + InteractionDriverInterface $interactionDriver, + ?ContentsDriverInterface $responseBodyDriver = null + ) { + parent::__construct($client, $interactionDriver, $responseBodyDriver ?? new ResponseBodyDriver($client, $interactionDriver)); + } + + public function withResponse(int $status): self { $this->client->call('pactffi_response_status', $this->getInteractionId(), $status); + + return $this; } protected function getPart(): int diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php index 71e5a62c..c6e80856 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php @@ -4,5 +4,5 @@ interface ResponseDriverInterface extends PartDriverInterface { - public function withResponse(int $status): void; + public function withResponse(int $status): self; } diff --git a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php index c7baf157..bdf392f1 100644 --- a/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php @@ -3,8 +3,6 @@ namespace PhpPact\Consumer\Factory; use PhpPact\Consumer\Driver\Interaction\InteractionDriver; -use PhpPact\Consumer\Driver\Interaction\Part\RequestDriver; -use PhpPact\Consumer\Driver\Interaction\Part\ResponseDriver; use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Service\InteractionRegistry; use PhpPact\Consumer\Service\InteractionRegistryInterface; @@ -19,10 +17,8 @@ public static function create(MockServerConfigInterface $config): InteractionReg $client = new Client(); $pactDriver = new PactDriver($client, $config); $interactionDriver = new InteractionDriver($client, $pactDriver); - $requestDriver = new RequestDriver($client, $interactionDriver); - $responseDriver = new ResponseDriver($client, $interactionDriver); $mockServer = new MockServer($client, $pactDriver, $config); - return new InteractionRegistry($interactionDriver, $requestDriver, $responseDriver, $mockServer); + return new InteractionRegistry($interactionDriver, $mockServer); } } diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index 4e3a0f9f..e46e7f80 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -3,16 +3,12 @@ namespace PhpPact\Consumer\Service; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; -use PhpPact\Consumer\Driver\Interaction\Part\RequestDriverInterface; -use PhpPact\Consumer\Driver\Interaction\Part\ResponseDriverInterface; use PhpPact\Consumer\Model\Interaction; class InteractionRegistry implements InteractionRegistryInterface { public function __construct( private InteractionDriverInterface $interactionDriver, - private RequestDriverInterface $requestDriver, - private ResponseDriverInterface $responseDriver, private MockServerInterface $mockServer ) { } @@ -23,10 +19,10 @@ public function verifyInteractions(): bool try { if ($matched) { - $this->writePact(); + $this->mockServer->writePact(); } } finally { - $this->cleanUp(); + $this->mockServer->cleanUp(); } return $matched; @@ -34,71 +30,15 @@ public function verifyInteractions(): bool public function registerInteraction(Interaction $interaction): bool { - $this - ->newInteraction($interaction) - ->given($interaction) - ->uponReceiving($interaction) - ->with($interaction) - ->willRespondWith($interaction) - ->startMockServer(); + $this->interactionDriver + ->newInteraction($interaction->getDescription()) + ->given($interaction->getProviderStates()) + ->uponReceiving($interaction->getDescription()) + ->with($interaction->getRequest()) + ->willRespondWith($interaction->getResponse()); - return true; - } - - private function cleanUp(): void - { - $this->mockServer->cleanUp(); - } - - private function writePact(): void - { - $this->mockServer->writePact(); - } - - private function newInteraction(Interaction $interaction): self - { - $this->interactionDriver->newInteraction($interaction->getDescription()); - - return $this; - } - - private function given(Interaction $interaction): self - { - $this->interactionDriver->given($interaction->getProviderStates()); - - return $this; - } - - private function uponReceiving(Interaction $interaction): self - { - $this->interactionDriver->uponReceiving($interaction->getDescription()); - - return $this; - } - - private function with(Interaction $interaction): self - { - $request = $interaction->getRequest(); - $this->requestDriver->withRequest($request->getMethod(), $request->getPath()); - $this->requestDriver->withHeaders($request->getHeaders()); - $this->requestDriver->withQueryParameters($request->getQuery()); - $this->requestDriver->withBody(null, $request->getBody()); - - return $this; - } - - private function willRespondWith(Interaction $interaction): self - { - $response = $interaction->getResponse(); - $this->responseDriver->withResponse($response->getStatus()); - $this->responseDriver->withHeaders($response->getHeaders()); - $this->responseDriver->withBody(null, $response->getBody()); - - return $this; - } - - private function startMockServer(): void - { $this->mockServer->start(); + + return true; } } From 29f7b4cea8ffa154f9a03c5b7e5237bd80043009 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 9 May 2023 11:24:59 +0700 Subject: [PATCH 040/298] Extract interaction part traits for reusing --- .../Driver/Interaction/Contents/RequestBodyDriver.php | 7 +++---- .../Interaction/Contents/ResponseBodyDriver.php | 7 +++---- .../Driver/Interaction/Part/RequestDriver.php | 7 ++----- .../Driver/Interaction/Part/RequestPartTrait.php | 11 +++++++++++ .../Driver/Interaction/Part/ResponseDriver.php | 7 ++----- .../Driver/Interaction/Part/ResponsePartTrait.php | 11 +++++++++++ 6 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/RequestPartTrait.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php index aca0f7e7..f4c21803 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php @@ -2,10 +2,9 @@ namespace PhpPact\Consumer\Driver\Interaction\Contents; +use PhpPact\Consumer\Driver\Interaction\Part\RequestPartTrait; + class RequestBodyDriver extends AbstractBodyDriver { - protected function getPart(): int - { - return $this->client->get('InteractionPart_Request'); - } + use RequestPartTrait; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php index 78d8c90d..d2ae9116 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php @@ -2,10 +2,9 @@ namespace PhpPact\Consumer\Driver\Interaction\Contents; +use PhpPact\Consumer\Driver\Interaction\Part\ResponsePartTrait; + class ResponseBodyDriver extends AbstractBodyDriver { - protected function getPart(): int - { - return $this->client->get('InteractionPart_Response'); - } + use ResponsePartTrait; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php index 37e790be..d69df087 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestDriver.php @@ -9,6 +9,8 @@ class RequestDriver extends AbstractPartDriver implements RequestDriverInterface { + use RequestPartTrait; + public function __construct( ClientInterface $client, InteractionDriverInterface $interactionDriver, @@ -34,9 +36,4 @@ public function withRequest(string $method, string $path): self return $this; } - - protected function getPart(): int - { - return $this->client->get('InteractionPart_Request'); - } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/RequestPartTrait.php b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestPartTrait.php new file mode 100644 index 00000000..bec1ee95 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/RequestPartTrait.php @@ -0,0 +1,11 @@ +client->get('InteractionPart_Request'); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php index 9e59702e..ff758e37 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php @@ -9,6 +9,8 @@ class ResponseDriver extends AbstractPartDriver implements ResponseDriverInterface { + use ResponsePartTrait; + public function __construct( ClientInterface $client, InteractionDriverInterface $interactionDriver, @@ -23,9 +25,4 @@ public function withResponse(int $status): self return $this; } - - protected function getPart(): int - { - return $this->client->get('InteractionPart_Response'); - } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php new file mode 100644 index 00000000..515cdf4b --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php @@ -0,0 +1,11 @@ +client->get('InteractionPart_Response'); + } +} From 768b77201903be2d2cbb9d945b5b53d6e4b6cd41 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 9 May 2023 14:18:52 +0700 Subject: [PATCH 041/298] Allow sub-class access properties --- src/PhpPact/Consumer/Driver/Pact/PactDriver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index eda28ab6..3433e662 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -12,8 +12,8 @@ class PactDriver implements PactDriverInterface protected int $id; public function __construct( - private ClientInterface $client, - private PactConfigInterface $config + protected ClientInterface $client, + protected PactConfigInterface $config ) { $this->setUp(); } From a96c56a997e4c387f357453d3643b745783a5227 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 9 May 2023 22:47:36 +0700 Subject: [PATCH 042/298] Move common code back to MockServer for reusing. Remove methods from MockServerInterface --- .../Consumer/Service/InteractionRegistry.php | 12 +----- src/PhpPact/Consumer/Service/MockServer.php | 38 ++++++++++++------- .../Consumer/Service/MockServerInterface.php | 6 +-- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index e46e7f80..ccd6cade 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -15,17 +15,7 @@ public function __construct( public function verifyInteractions(): bool { - $matched = $this->mockServer->isMatched(); - - try { - if ($matched) { - $this->mockServer->writePact(); - } - } finally { - $this->mockServer->cleanUp(); - } - - return $matched; + return $this->mockServer->verify(); } public function registerInteraction(Interaction $interaction): bool diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index 31378257..fa268d6e 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -35,12 +35,32 @@ public function start(): void $this->config->setPort($port); } - public function isMatched(): bool + public function verify(): bool { - return $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); + $matched = $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); + + try { + if ($matched) { + $this->writePact(); + } + } finally { + $this->cleanUp(); + } + + return $matched; + } + + protected function getTransport(): string + { + return $this->config->isSecure() ? 'https' : 'http'; } - public function writePact(): void + protected function getTransportConfig(): ?string + { + return null; + } + + private function writePact(): void { $error = $this->client->call( 'pactffi_write_pact_file', @@ -53,19 +73,9 @@ public function writePact(): void } } - public function cleanUp(): void + private function cleanUp(): void { $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); $this->pactDriver->cleanUp(); } - - protected function getTransport(): string - { - return $this->config->isSecure() ? 'https' : 'http'; - } - - protected function getTransportConfig(): ?string - { - return null; - } } diff --git a/src/PhpPact/Consumer/Service/MockServerInterface.php b/src/PhpPact/Consumer/Service/MockServerInterface.php index c1670d38..5737269c 100644 --- a/src/PhpPact/Consumer/Service/MockServerInterface.php +++ b/src/PhpPact/Consumer/Service/MockServerInterface.php @@ -6,9 +6,5 @@ interface MockServerInterface { public function start(): void; - public function isMatched(): bool; - - public function writePact(): void; - - public function cleanUp(): void; + public function verify(): bool; } From 264018ac736ac7973c5f4d320900209a860ae4b4 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 9 May 2023 23:55:06 +0700 Subject: [PATCH 043/298] Move code from registry to driver --- .../Driver/Interaction/AbstractDriver.php | 2 ++ .../Driver/Interaction/DriverInterface.php | 2 -- .../Driver/Interaction/InteractionDriver.php | 25 +++++++++++++++---- .../InteractionDriverInterface.php | 15 ++--------- .../Consumer/Service/InteractionRegistry.php | 8 +----- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php index 1a035e7f..bfade7a1 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php @@ -19,4 +19,6 @@ public function getId(): int { return $this->id; } + + abstract protected function newInteraction(string $description): self; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php index b7a85311..2d7e3c90 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -5,6 +5,4 @@ interface DriverInterface { public function getId(): int; - - public function newInteraction(string $description): static; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index f4e554d5..dfab000b 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -8,7 +8,9 @@ use PhpPact\Consumer\Driver\Interaction\Part\ResponseDriverInterface; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\ConsumerRequest; +use PhpPact\Consumer\Model\Interaction; use PhpPact\Consumer\Model\ProviderResponse; +use PhpPact\Consumer\Model\ProviderState; use PhpPact\FFI\ClientInterface; class InteractionDriver extends AbstractDriver implements InteractionDriverInterface @@ -27,21 +29,34 @@ public function __construct( $this->responseDriver = $responseDriver ?? new ResponseDriver($client, $this); } - public function newInteraction(string $description): static + public function registerInteraction(Interaction $interaction): void + { + $this + ->newInteraction($interaction->getDescription()) + ->given($interaction->getProviderStates()) + ->uponReceiving($interaction->getDescription()) + ->with($interaction->getRequest()) + ->willRespondWith($interaction->getResponse()); + } + + protected function newInteraction(string $description): self { $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); return $this; } - public function uponReceiving(string $description): self + private function uponReceiving(string $description): self { $this->client->call('pactffi_upon_receiving', $this->id, $description); return $this; } - public function given(array $providerStates): self + /** + * @param ProviderState[] $providerStates + */ + private function given(array $providerStates): self { foreach ($providerStates as $providerState) { $this->client->call('pactffi_given', $this->id, $providerState->getName()); @@ -53,7 +68,7 @@ public function given(array $providerStates): self return $this; } - public function with(ConsumerRequest $request): self + private function with(ConsumerRequest $request): self { $this->requestDriver ->withRequest($request->getMethod(), $request->getPath()) @@ -64,7 +79,7 @@ public function with(ConsumerRequest $request): self return $this; } - public function willRespondWith(ProviderResponse $response): self + private function willRespondWith(ProviderResponse $response): self { $this->responseDriver ->withResponse($response->getStatus()) diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index 762ff99d..f4168ad8 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -2,20 +2,9 @@ namespace PhpPact\Consumer\Driver\Interaction; -use PhpPact\Consumer\Model\ConsumerRequest; -use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Consumer\Model\ProviderState; +use PhpPact\Consumer\Model\Interaction; interface InteractionDriverInterface extends DriverInterface { - public function uponReceiving(string $description): self; - - /** - * @param ProviderState[] $providerStates - */ - public function given(array $providerStates): self; - - public function with(ConsumerRequest $request): self; - - public function willRespondWith(ProviderResponse $response): self; + public function registerInteraction(Interaction $interaction): void; } diff --git a/src/PhpPact/Consumer/Service/InteractionRegistry.php b/src/PhpPact/Consumer/Service/InteractionRegistry.php index ccd6cade..cbc0725e 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Service/InteractionRegistry.php @@ -20,13 +20,7 @@ public function verifyInteractions(): bool public function registerInteraction(Interaction $interaction): bool { - $this->interactionDriver - ->newInteraction($interaction->getDescription()) - ->given($interaction->getProviderStates()) - ->uponReceiving($interaction->getDescription()) - ->with($interaction->getRequest()) - ->willRespondWith($interaction->getResponse()); - + $this->interactionDriver->registerInteraction($interaction); $this->mockServer->start(); return true; From 3a3afda37661a576ac238811d85a2df36dd3d950 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 10 May 2023 07:53:23 +0700 Subject: [PATCH 044/298] Switch term 'Driver' <--> 'Registry' --- .../Contents/RequestBodyDriver.php | 10 -- .../Contents/ResponseBodyDriver.php | 10 -- .../Driver/Interaction/DriverInterface.php | 8 -- .../Driver/Interaction/InteractionDriver.php | 85 +++-------------- .../InteractionDriverInterface.php | 6 +- .../Interaction/Part/ResponseDriver.php | 28 ------ .../Part/ResponseDriverInterface.php | 8 -- .../Consumer/Driver/Pact/PactDriver.php | 37 +++----- .../Driver/Pact/PactDriverInterface.php | 2 +- .../Exception/PactNotRegisteredException.php | 9 ++ .../Factory/InteractionDriverFactory.php | 26 ++++++ .../Factory/InteractionRegistryFactory.php | 24 ----- src/PhpPact/Consumer/InteractionBuilder.php | 14 +-- .../Interaction/AbstractRegistry.php} | 8 +- .../Contents/AbstractBodyRegistry.php} | 10 +- .../Contents/ContentsRegistryInterface.php} | 4 +- .../Contents/RequestBodyRegistry.php | 10 ++ .../Contents/ResponseBodyRegistry.php | 10 ++ .../Interaction/InteractionRegistry.php | 93 +++++++++++++++++++ .../InteractionRegistryInterface.php | 6 +- .../Part/AbstractPartRegistry.php} | 16 ++-- .../Part/PartRegistryInterface.php} | 4 +- .../Interaction/Part/RequestPartTrait.php | 2 +- .../Interaction/Part/RequestRegistry.php} | 16 ++-- .../Part/RequestRegistryInterface.php} | 4 +- .../Interaction/Part/ResponsePartTrait.php | 2 +- .../Interaction/Part/ResponseRegistry.php | 28 ++++++ .../Part/ResponseRegistryInterface.php | 8 ++ .../Interaction/RegistryInterface.php | 8 ++ .../Consumer/Registry/Pact/PactRegistry.php | 50 ++++++++++ .../Registry/Pact/PactRegistryInterface.php | 12 +++ .../Consumer/Service/InteractionRegistry.php | 28 ------ src/PhpPact/Consumer/Service/MockServer.php | 8 +- 33 files changed, 331 insertions(+), 263 deletions(-) delete mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php delete mode 100644 src/PhpPact/Consumer/Driver/Interaction/Contents/ResponseBodyDriver.php delete mode 100644 src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php delete mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php delete mode 100644 src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php create mode 100644 src/PhpPact/Consumer/Exception/PactNotRegisteredException.php create mode 100644 src/PhpPact/Consumer/Factory/InteractionDriverFactory.php delete mode 100644 src/PhpPact/Consumer/Factory/InteractionRegistryFactory.php rename src/PhpPact/Consumer/{Driver/Interaction/AbstractDriver.php => Registry/Interaction/AbstractRegistry.php} (58%) rename src/PhpPact/Consumer/{Driver/Interaction/Contents/AbstractBodyDriver.php => Registry/Interaction/Contents/AbstractBodyRegistry.php} (62%) rename src/PhpPact/Consumer/{Driver/Interaction/Contents/ContentsDriverInterface.php => Registry/Interaction/Contents/ContentsRegistryInterface.php} (53%) create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/ResponseBodyRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php rename src/PhpPact/Consumer/{Service => Registry/Interaction}/InteractionRegistryInterface.php (50%) rename src/PhpPact/Consumer/{Driver/Interaction/Part/AbstractPartDriver.php => Registry/Interaction/Part/AbstractPartRegistry.php} (59%) rename src/PhpPact/Consumer/{Driver/Interaction/Part/PartDriverInterface.php => Registry/Interaction/Part/PartRegistryInterface.php} (71%) rename src/PhpPact/Consumer/{Driver => Registry}/Interaction/Part/RequestPartTrait.php (73%) rename src/PhpPact/Consumer/{Driver/Interaction/Part/RequestDriver.php => Registry/Interaction/Part/RequestRegistry.php} (54%) rename src/PhpPact/Consumer/{Driver/Interaction/Part/RequestDriverInterface.php => Registry/Interaction/Part/RequestRegistryInterface.php} (64%) rename src/PhpPact/Consumer/{Driver => Registry}/Interaction/Part/ResponsePartTrait.php (73%) create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/RegistryInterface.php create mode 100644 src/PhpPact/Consumer/Registry/Pact/PactRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Service/InteractionRegistry.php diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php deleted file mode 100644 index f4c21803..00000000 --- a/src/PhpPact/Consumer/Driver/Interaction/Contents/RequestBodyDriver.php +++ /dev/null @@ -1,10 +0,0 @@ -requestDriver = $requestDriver ?? new RequestDriver($client, $this); - $this->responseDriver = $responseDriver ?? new ResponseDriver($client, $this); } - public function registerInteraction(Interaction $interaction): void + public function verifyInteractions(): bool { - $this - ->newInteraction($interaction->getDescription()) - ->given($interaction->getProviderStates()) - ->uponReceiving($interaction->getDescription()) - ->with($interaction->getRequest()) - ->willRespondWith($interaction->getResponse()); - } - - protected function newInteraction(string $description): self - { - $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getId(), $description); - - return $this; - } - - private function uponReceiving(string $description): self - { - $this->client->call('pactffi_upon_receiving', $this->id, $description); - - return $this; - } - - /** - * @param ProviderState[] $providerStates - */ - private function given(array $providerStates): self - { - foreach ($providerStates as $providerState) { - $this->client->call('pactffi_given', $this->id, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); - } - } - - return $this; - } - - private function with(ConsumerRequest $request): self - { - $this->requestDriver - ->withRequest($request->getMethod(), $request->getPath()) - ->withQueryParameters($request->getQuery()) - ->withHeaders($request->getHeaders()) - ->withBody(null, $request->getBody()); - - return $this; + return $this->mockServer->verify(); } - private function willRespondWith(ProviderResponse $response): self + public function registerInteraction(Interaction $interaction): bool { - $this->responseDriver - ->withResponse($response->getStatus()) - ->withHeaders($response->getHeaders()) - ->withBody(null, $response->getBody()); + $this->pactDriver->setUp(); + $this->interactionRegistry->registerInteraction($interaction); + $this->mockServer->start(); - return $this; + return true; } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index f4168ad8..fe5a3e07 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -4,7 +4,9 @@ use PhpPact\Consumer\Model\Interaction; -interface InteractionDriverInterface extends DriverInterface +interface InteractionDriverInterface { - public function registerInteraction(Interaction $interaction): void; + public function registerInteraction(Interaction $interaction): bool; + + public function verifyInteractions(): bool; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php deleted file mode 100644 index ff758e37..00000000 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriver.php +++ /dev/null @@ -1,28 +0,0 @@ -client->call('pactffi_response_status', $this->getInteractionId(), $status); - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php deleted file mode 100644 index c6e80856..00000000 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponseDriverInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -setUp(); - } - - public function getId(): int - { - return $this->id; } public function cleanUp(): void { - $this->client->call('pactffi_free_pact_handle', $this->id); - unset($this->id); + $this->pactRegistry->deletePact(); } public function writePact(): void { $error = $this->client->call( 'pactffi_pact_handle_write_file', - $this->id, + $this->pactRegistry->getId(), $this->config->getPactDir(), $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); @@ -42,12 +35,11 @@ public function writePact(): void } } - protected function setUp(): void + public function setUp(): void { $this ->initWithLogLevel() - ->newPact() - ->withSpecification(); + ->registerPact(); } protected function getSpecification(): int @@ -81,16 +73,13 @@ private function initWithLogLevel(): self return $this; } - private function newPact(): self - { - $this->id = $this->client->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider()); - - return $this; - } - - private function withSpecification(): self + private function registerPact(): self { - $this->client->call('pactffi_with_specification', $this->id, $this->getSpecification()); + $this->pactRegistry->registerPact( + $this->config->getConsumer(), + $this->config->getProvider(), + $this->getSpecification() + ); return $this; } diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php index 6b06d013..69ccdc42 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php @@ -4,7 +4,7 @@ interface PactDriverInterface { - public function getId(): int; + public function setUp(): void; public function cleanUp(): void; diff --git a/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php b/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php new file mode 100644 index 00000000..8af9a00b --- /dev/null +++ b/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php @@ -0,0 +1,9 @@ +registry = $registry instanceof InteractionRegistryInterface ? $registry : InteractionRegistryFactory::create($registry); + $this->driver = $driver instanceof InteractionDriverInterface ? $driver : InteractionDriverFactory::create($driver); $this->interaction = new Interaction(); } @@ -64,7 +64,7 @@ public function willRespondWith(ProviderResponse $response): bool { $this->interaction->setResponse($response); - return $this->registry->registerInteraction($this->interaction); + return $this->driver->registerInteraction($this->interaction); } /** @@ -72,6 +72,6 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->registry->verifyInteractions(); + return $this->driver->verifyInteractions(); } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php b/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php similarity index 58% rename from src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php rename to src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php index bfade7a1..82e1d76d 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php +++ b/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php @@ -1,17 +1,17 @@ client->call('pactffi_with_body', $this->interactionDriver->getId(), $this->getPart(), $contentType, $body); + $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $contentType, $body); if (!$success) { throw new InteractionBodyNotAddedException(); } diff --git a/src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php similarity index 53% rename from src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php rename to src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php index 5b87acea..3e3394ff 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Contents/ContentsDriverInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php @@ -1,8 +1,8 @@ requestRegistry = $requestRegistry ?? new RequestRegistry($client, $this); + $this->responseRegistry = $responseRegistry ?? new ResponseRegistry($client, $this); + } + + public function registerInteraction(Interaction $interaction): bool + { + $this + ->newInteraction($interaction->getDescription()) + ->given($interaction->getProviderStates()) + ->uponReceiving($interaction->getDescription()) + ->with($interaction->getRequest()) + ->willRespondWith($interaction->getResponse()); + + return true; + } + + protected function newInteraction(string $description): self + { + $this->id = $this->client->call('pactffi_new_interaction', $this->pactRegistry->getId(), $description); + + return $this; + } + + private function uponReceiving(string $description): self + { + $this->client->call('pactffi_upon_receiving', $this->id, $description); + + return $this; + } + + /** + * @param ProviderState[] $providerStates + */ + private function given(array $providerStates): self + { + foreach ($providerStates as $providerState) { + $this->client->call('pactffi_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + + return $this; + } + + private function with(ConsumerRequest $request): self + { + $this->requestRegistry + ->withRequest($request->getMethod(), $request->getPath()) + ->withQueryParameters($request->getQuery()) + ->withHeaders($request->getHeaders()) + ->withBody(null, $request->getBody()); + + return $this; + } + + private function willRespondWith(ProviderResponse $response): self + { + $this->responseRegistry + ->withResponse($response->getStatus()) + ->withHeaders($response->getHeaders()) + ->withBody(null, $response->getBody()); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Service/InteractionRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php similarity index 50% rename from src/PhpPact/Consumer/Service/InteractionRegistryInterface.php rename to src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php index 54159b90..c3f1c7fc 100644 --- a/src/PhpPact/Consumer/Service/InteractionRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php @@ -1,12 +1,10 @@ contentsDriver->withContents($contentType, $body); + $this->contentsRegistry->withContents($contentType, $body); return $this; } @@ -35,7 +35,7 @@ public function withHeaders(array $headers): self protected function getInteractionId(): int { - return $this->interactionDriver->getId(); + return $this->interactionRegistry->getId(); } abstract protected function getPart(): int; diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php similarity index 71% rename from src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php rename to src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php index 417b18cb..90a6516b 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/PartDriverInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php @@ -1,8 +1,8 @@ $queryParams diff --git a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php similarity index 73% rename from src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php rename to src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php index 515cdf4b..389fe949 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/Part/ResponsePartTrait.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php @@ -1,6 +1,6 @@ client->call('pactffi_response_status', $this->getInteractionId(), $status); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php new file mode 100644 index 00000000..05f56e52 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php @@ -0,0 +1,8 @@ +id)) { + throw new PactNotRegisteredException('New pact must be registered.'); + } + return $this->id; + } + + public function deletePact(): void + { + $this->client->call('pactffi_free_pact_handle', $this->id); + unset($this->id); + } + + public function registerPact(string $consumer, string $provider, int $specification): void + { + $this + ->newPact($consumer, $provider) + ->withSpecification($specification); + } + + private function newPact(string $consumer, string $provider): self + { + $this->id = $this->client->call('pactffi_new_pact', $consumer, $provider); + + return $this; + } + + private function withSpecification(int $specification): self + { + $this->client->call('pactffi_with_specification', $this->id, $specification); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php b/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php new file mode 100644 index 00000000..c9ffb883 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php @@ -0,0 +1,12 @@ +mockServer->verify(); - } - - public function registerInteraction(Interaction $interaction): bool - { - $this->interactionDriver->registerInteraction($interaction); - $this->mockServer->start(); - - return true; - } -} diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index fa268d6e..44aa9179 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -3,9 +3,9 @@ namespace PhpPact\Consumer\Service; use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; +use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\MockServerConfigInterface; @@ -13,7 +13,7 @@ class MockServer implements MockServerInterface { public function __construct( private ClientInterface $client, - private PactDriverInterface $pactDriver, + private PactRegistryInterface $pactRegistry, private MockServerConfigInterface $config ) { } @@ -22,7 +22,7 @@ public function start(): void { $port = $this->client->call( 'pactffi_create_mock_server_for_transport', - $this->pactDriver->getId(), + $this->pactRegistry->getId(), $this->config->getHost(), $this->config->getPort(), $this->getTransport(), @@ -76,6 +76,6 @@ private function writePact(): void private function cleanUp(): void { $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); - $this->pactDriver->cleanUp(); + $this->pactRegistry->deletePact(); } } From 1d6d39eb36b824e6e960c10c1bf2000932979675 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 12 May 2023 07:27:51 +0700 Subject: [PATCH 045/298] Inject driver factory into builder --- .../{ => Builder}/InteractionDriverFactory.php | 6 +++--- .../Builder/InteractionDriverFactoryInterface.php | 11 +++++++++++ src/PhpPact/Consumer/InteractionBuilder.php | 7 ++++--- 3 files changed, 18 insertions(+), 6 deletions(-) rename src/PhpPact/Consumer/Factory/{ => Builder}/InteractionDriverFactory.php (79%) create mode 100644 src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactoryInterface.php diff --git a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php b/src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php similarity index 79% rename from src/PhpPact/Consumer/Factory/InteractionDriverFactory.php rename to src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php index acfd1a31..6a1a2efe 100644 --- a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php +++ b/src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php @@ -1,6 +1,6 @@ driver = $driver instanceof InteractionDriverInterface ? $driver : InteractionDriverFactory::create($driver); + $this->driver = ($driverFactory ?? new InteractionDriverFactory())->create($config); $this->interaction = new Interaction(); } From f844be0f301f313d926d075aeeb379e7851273ae Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 12 May 2023 13:06:26 +0700 Subject: [PATCH 046/298] Revert namespace change --- .../Factory/{Builder => }/InteractionDriverFactory.php | 2 +- .../{Builder => }/InteractionDriverFactoryInterface.php | 2 +- src/PhpPact/Consumer/InteractionBuilder.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/PhpPact/Consumer/Factory/{Builder => }/InteractionDriverFactory.php (95%) rename src/PhpPact/Consumer/Factory/{Builder => }/InteractionDriverFactoryInterface.php (86%) diff --git a/src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php similarity index 95% rename from src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php rename to src/PhpPact/Consumer/Factory/InteractionDriverFactory.php index 6a1a2efe..f911c5fc 100644 --- a/src/PhpPact/Consumer/Factory/Builder/InteractionDriverFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php @@ -1,6 +1,6 @@ Date: Wed, 28 Dec 2022 09:37:14 +0700 Subject: [PATCH 047/298] Use Rust FFI: Message Consumer --- README.md | 4 +- .../ExampleMessageConsumer.php | 4 +- .../ExampleMessageConsumerTest.php | 32 +------ .../Consumer/AbstractMessageBuilder.php | 59 ++++++++++++ .../Driver/Interaction/MessageDriver.php | 37 ++++++++ .../Interaction/MessageDriverInterface.php | 14 +++ .../Consumer/Factory/MessageDriverFactory.php | 24 +++++ .../Factory/MessageDriverFactoryInterface.php | 11 +++ src/PhpPact/Consumer/MessageBuilder.php | 88 +++--------------- src/PhpPact/Consumer/Model/Message.php | 12 ++- .../Contents/MessageContentsRegistry.php | 25 +++++ .../Registry/Interaction/MessageRegistry.php | 91 +++++++++++++++++++ .../Interaction/MessageRegistryInterface.php | 10 ++ src/PhpPact/FFI/Model/StringData.php | 35 +++++++ .../Standalone/Installer/Model/Scripts.php | 5 - .../Standalone/PactMessage/PactMessage.php | 47 ---------- tests/PhpPact/Consumer/Model/MessageTest.php | 34 +++++++ 17 files changed, 374 insertions(+), 158 deletions(-) create mode 100644 src/PhpPact/Consumer/AbstractMessageBuilder.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php create mode 100644 src/PhpPact/Consumer/Factory/MessageDriverFactory.php create mode 100644 src/PhpPact/Consumer/Factory/MessageDriverFactoryInterface.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/MessageContentsRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php create mode 100644 src/PhpPact/FFI/Model/StringData.php delete mode 100644 src/PhpPact/Standalone/PactMessage/PactMessage.php create mode 100644 tests/PhpPact/Consumer/Model/MessageTest.php diff --git a/README.md b/README.md index e208e86d..5a0c9573 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,9 @@ $consumerMessage = new ExampleMessageConsumer(); $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); -$builder->verify(); +$verifyResult = $builder->verify(); + +$this->assertTrue($verifyResult); ``` diff --git a/example/src/MessageConsumer/ExampleMessageConsumer.php b/example/src/MessageConsumer/ExampleMessageConsumer.php index 0a821944..77851aa6 100644 --- a/example/src/MessageConsumer/ExampleMessageConsumer.php +++ b/example/src/MessageConsumer/ExampleMessageConsumer.php @@ -4,7 +4,7 @@ class ExampleMessageConsumer { - public function ProcessText($message) + public function ProcessText(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->text, true) . "\n"; @@ -12,7 +12,7 @@ public function ProcessText($message) return $obj; } - public function ProcessSong($message) + public function ProcessSong(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->song, true) . "\n"; diff --git a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php index e9bda5d0..0056d35d 100644 --- a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php +++ b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php @@ -2,8 +2,6 @@ namespace MessageConsumer; -require_once __DIR__ . '/../../src/MessageConsumer/ExampleMessageConsumer.php'; - use Exception; use PhpPact\Consumer\MessageBuilder; use PhpPact\Config\PactConfigInterface; @@ -28,18 +26,6 @@ public static function setUpBeforeClass(): void ->setPactDir(__DIR__ . '/../../output/'); } - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - - // build out brokerHttpService as your example - /* - $brokerHttpService = new BrokerHttpClient(new GuzzleClient(), new Uri($pactBrokerUri)); - $brokerHttpService->publishJson($json, $consumerVersion); - $brokerHttpService->tag($this->mockServerConfig->getConsumer(), $consumerVersion, $tag); - */ - } - /** * @throws Exception */ @@ -53,7 +39,7 @@ public function testProcessText() $metadata = ['queue'=>'wind cries', 'routing_key'=>'wind cries']; $builder - ->given('a message', ['foo']) + ->given('a message', ['foo' => 'bar']) ->expectsToReceive('an alligator named Mary exists') ->withMetadata($metadata) ->withContent($contents); @@ -63,11 +49,9 @@ public function testProcessText() $callback = [$consumerMessage, 'ProcessText']; $builder->setCallback($callback); - $hasException = false; - - $builder->verify(); + $verifyResult = $builder->verify(); - $this->assertTrue(true, 'Expects to reach this true statement by running verify()'); + $this->assertTrue($verifyResult); } /** @@ -93,14 +77,8 @@ public function testProcessSong() $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); - $hasException = false; - - try { - $builder->verify(); - } catch (Exception $e) { - $hasException = true; - } + $verifyResult = $builder->verify(); - $this->assertFalse($hasException, 'Expects verification to pass without exceptions being thrown'); + $this->assertTrue($verifyResult); } } diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php new file mode 100644 index 00000000..71a23275 --- /dev/null +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -0,0 +1,59 @@ +message = new Message(); + } + + /** + * @param string $name what is given to the request + * @param array $params for that request + * @param bool $overwrite clear pass states completely and start this array + */ + public function given(string $name, array $params = [], bool $overwrite = false): self + { + $this->message->setProviderState($name, $params, $overwrite); + + return $this; + } + + /** + * @param string $description what is received when the request is made + */ + public function expectsToReceive(string $description): self + { + $this->message->setDescription($description); + + return $this; + } + + /** + * @param array $metadata what is the additional metadata of the message + */ + public function withMetadata(array $metadata): self + { + $this->message->setMetadata($metadata); + + return $this; + } + + /** + * Make the http request to the Mock Service to register the message. Content is required. + * + * @param mixed $contents required to be in the message + */ + public function withContent(mixed $contents): self + { + $this->message->setContents($contents); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php new file mode 100644 index 00000000..6ba771ff --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php @@ -0,0 +1,37 @@ +client->call('pactffi_message_reify', $this->messageRegistry->getId()); + } + + public function writePactAndCleanUp(): bool + { + $this->pactDriver->writePact(); + $this->pactDriver->cleanUp(); + + return true; + } + + public function registerMessage(Message $message): void + { + $this->pactDriver->setUp(); + $this->messageRegistry->registerMessage($message); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php new file mode 100644 index 00000000..be9a3089 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php @@ -0,0 +1,14 @@ + */ protected array $callback; - private Message $message; - - public function __construct(PactConfigInterface $config) + public function __construct(PactConfigInterface $config, ?MessageDriverFactoryInterface $driverFactory = null) { - $this->config = $config; - $this->message = new Message(); - $this->pactMessage = new PactMessage(); + parent::__construct(); + $this->driver = ($driverFactory ?? new MessageDriverFactory())->create($config); } /** @@ -45,56 +41,14 @@ public function setCallback(callable $callback, ?string $description = null): se return $this; } - /** - * @param string $name what is given to the request - * @param array $params for that request - * @param bool $overwrite clear pass states completely and start this array - */ - public function given(string $name, array $params = [], bool $overwrite = false): self - { - $this->message->setProviderState($name, $params, $overwrite); - - return $this; - } - - /** - * @param string $description what is received when the request is made - */ - public function expectsToReceive(string $description): self - { - $this->message->setDescription($description); - - return $this; - } - - /** - * @param array $metadata what is the additional metadata of the message - */ - public function withMetadata(array $metadata): self - { - $this->message->setMetadata($metadata); - - return $this; - } - - /** - * Make the http request to the Mock Service to register the message. Content is required. - * - * @param mixed $contents required to be in the message - */ - public function withContent($contents): self - { - $this->message->setContents($contents); - - return $this; - } - /** * Run reify to create an example pact from the message (i.e. create messages from matchers) */ public function reify(): string { - return $this->pactMessage->reify($this->message); + $this->driver->registerMessage($this->message); + + return $this->driver->reify(); } /** @@ -107,18 +61,16 @@ public function verifyMessage(callable $callback, ?string $description = null): { $this->setCallback($callback, $description); - return $this->verify($description); + return $this->verify(); } /** * Verify the use of the pact by calling the callback * It also calls finalize to write the pact * - * @param null|string $description description of the pact and thus callback - * * @throws \Exception if callback is not set */ - public function verify(?string $description = null): bool + public function verify(): bool { if (\count($this->callback) < 1) { throw new \Exception('Callbacks need to exist to run verify.'); @@ -133,21 +85,9 @@ public function verify(?string $description = null): bool \call_user_func($callback, $pactJson); } - return $this->writePact(); + return $this->driver->writePactAndCleanUp(); } catch (\Exception $e) { return false; } } - - /** - * Write the Pact without deleting the interactions. - * @throws \JsonException - */ - public function writePact(): bool - { - // you do not want to save the reified json - $pactJson = \json_encode($this->message, JSON_THROW_ON_ERROR); - - return $this->pactMessage->update($pactJson, $this->config->getConsumer(), $this->config->getProvider(), $this->config->getPactDir()); - } } diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index af2c047d..1b8b3a4e 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -14,7 +14,7 @@ class Message /** * @var array */ - private array $metadata; + private array $metadata = []; private mixed $contents; @@ -43,11 +43,19 @@ public function getMetadata(): array */ public function setMetadata(array $metadata): self { - $this->metadata = $metadata; + $this->metadata = []; + foreach ($metadata as $key => $value) { + $this->setMetadataValue($key, $value); + } return $this; } + private function setMetadataValue(string $key, string $value): void + { + $this->metadata[$key] = $value; + } + public function getContents(): mixed { return $this->contents; diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/MessageContentsRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/MessageContentsRegistry.php new file mode 100644 index 00000000..e3f8c355 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Contents/MessageContentsRegistry.php @@ -0,0 +1,25 @@ +client->call('pactffi_message_with_contents', $this->messageRegistry->getId(), $contentType, $data->getValue(), $data->getSize()); + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php new file mode 100644 index 00000000..11f70f91 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -0,0 +1,91 @@ +messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); + } + + + public function registerMessage(Message $message): void + { + if (\is_string($message->getContents())) { + $contents = $message->getContents(); + $contentType = 'text/plain'; + } else { + $contents = \json_encode($message->getContents(), JSON_THROW_ON_ERROR); + $contentType = 'application/json'; + } + + $this + ->newInteraction($message->getDescription()) + ->given($message->getProviderStates()) + ->expectsToReceive($message->getDescription()) + ->withMetadata($message->getMetadata()) + ->withContents($contentType, $contents); + } + + protected function newInteraction(string $description): self + { + $this->id = $this->client->call('pactffi_new_message_interaction', $this->pactRegistry->getId(), $description); + + return $this; + } + + private function withContents(?string $contentType = null, ?string $contents = null): self + { + $this->messageContentsRegistry->withContents($contentType, $contents); + + return $this; + } + + private function expectsToReceive(string $description): self + { + $this->client->call('pactffi_message_expects_to_receive', $this->id, $description); + + return $this; + } + + /** + * @param ProviderState[] $providerStates + */ + private function given(array $providerStates): self + { + foreach ($providerStates as $providerState) { + $this->client->call('pactffi_message_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_message_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + + return $this; + } + + /** + * @param array $metadata + */ + private function withMetadata(array $metadata): self + { + foreach ($metadata as $key => $value) { + $this->client->call('pactffi_message_with_metadata', $this->id, (string) $key, (string) $value); + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php new file mode 100644 index 00000000..ca3af5c2 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php @@ -0,0 +1,10 @@ +value; + } + + public function getSize(): int + { + return $this->size; + } + + public static function createFrom(string $value): ?self + { + $length = \strlen($value); + $size = $length + 1; + $cData = FFI::new("uint8_t[{$size}]"); + FFI::memcpy($cData, $value, $length); + + return new self($cData, $size); + } +} diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 243bf45a..02932804 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -41,11 +41,6 @@ public static function getBroker(): string return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker' . self::getSuffix(); } - public static function getPactMessage(): string - { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-message' . self::getSuffix(); - } - private static function getSuffix(): string { return (PHP_OS_FAMILY === 'Windows' ? '.bat' : ''); diff --git a/src/PhpPact/Standalone/PactMessage/PactMessage.php b/src/PhpPact/Standalone/PactMessage/PactMessage.php deleted file mode 100644 index b96b29d3..00000000 --- a/src/PhpPact/Standalone/PactMessage/PactMessage.php +++ /dev/null @@ -1,47 +0,0 @@ -runBlocking(); - - $output = $process->getOutput(); - \preg_replace("/\r|\n/", '', $output); - - return $output; - } - - /** - * Update a pact with the given message, or create the pact if it does not exist. The MESSAGE_JSON may be in the legacy Ruby JSON format or the v2+ format. - */ - public function update(string $pactJson, string $consumer, string $provider, string $pactDir): bool - { - $arguments = []; - $arguments[] = 'update'; - $arguments[] = "--consumer={$consumer}"; - $arguments[] = "--provider={$provider}"; - $arguments[] = "--pact-dir={$pactDir}"; - $arguments[] = "'" . $pactJson . "'"; - - $process = new ProcessRunner(Scripts::getPactMessage(), $arguments); - $process->runBlocking(); - - \sleep(1); - - return true; - } -} diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php new file mode 100644 index 00000000..165ed8a8 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -0,0 +1,34 @@ + 'bar']; + $metadata = ['queue' => 'foo', 'routing_key' => 'bar']; + $contents = 'test'; + + $subject = (new Message()) + ->setDescription($description) + ->addProviderState($providerStateName, $providerStateParams) + ->setMetadata($metadata) + ->setContents($contents); + + static::assertSame($description, $subject->getDescription()); + $providerStates = $subject->getProviderStates(); + static::assertCount(1, $providerStates); + static::assertContainsOnlyInstancesOf(ProviderState::class, $providerStates); + static::assertEquals($providerStateName, $providerStates[0]->getName()); + static::assertEquals($providerStateParams, $providerStates[0]->getParams()); + static::assertSame($metadata, $subject->getMetadata()); + static::assertSame($contents, $subject->getContents()); + } +} From 6aad4567b834b831babcf9e0af0ea2b10f5a96c5 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 2 Jan 2023 21:05:49 +0700 Subject: [PATCH 048/298] Use Rust FFI: Provider --- .github/workflows/build.yml | 3 + README.md | 156 +++---- UPGRADE-9.0.md | 44 ++ composer.json | 12 +- example/README.md | 4 - example/pacts/someconsumer-someprovider.json | 2 +- .../pacts/test_consumer-test_provider.json | 2 +- example/phpunit.all.xml | 3 - example/phpunit.message.provider.xml | 8 - example/src/Consumer/publish_json_example.php | 16 - .../ExampleMessageProvider.php | 34 +- example/src/Provider/ExampleProvider.php | 66 +++ example/src/Provider/public/index.php | 33 +- .../ExampleMessageProviderTest.php | 61 --- example/tests/Provider/PactVerifyTest.php | 35 +- .../Broker/Service/BrokerHttpClient.php | 109 ----- .../Service/BrokerHttpClientInterface.php | 49 -- .../Exception/CDataNotCreatedException.php | 9 + src/PhpPact/FFI/Model/ArrayData.php | 61 +++ src/PhpPact/Provider/MessageVerifier.php | 208 --------- .../Standalone/Installer/Model/Scripts.php | 5 - .../Model/Config/CallingApp.php | 33 ++ .../Model/Config/CallingAppInterface.php | 14 + .../Model/Config/ConsumerFilters.php | 33 ++ .../Model/Config/ConsumerFiltersInterface.php | 18 + .../Model/Config/FilterInfo.php | 46 ++ .../Model/Config/FilterInfoInterface.php | 18 + .../Model/Config/PluginDirTrait.php | 20 + .../Model/Config/ProviderInfo.php | 72 +++ .../Model/Config/ProviderInfoInterface.php | 26 ++ .../Model/Config/ProviderState.php | 48 ++ .../Model/Config/ProviderStateInterface.php | 20 + .../Model/Config/ProviderTransport.php | 59 +++ .../Config/ProviderTransportInterface.php | 28 ++ .../Model/Config/PublishOptions.php | 74 +++ .../Model/Config/PublishOptionsInterface.php | 32 ++ .../Model/Config/VerificationOptions.php | 33 ++ .../Config/VerificationOptionsInterface.php | 14 + .../Model/ConsumerVersionSelectors.php | 16 +- .../ProviderVerifier/Model/Source/Broker.php | 118 +++++ .../Model/Source/BrokerInterface.php | 52 +++ .../ProviderVerifier/Model/Source/Url.php | 61 +++ .../Model/Source/UrlInterface.php | 24 + .../ProviderVerifier/Model/VerifierConfig.php | 439 +++--------------- .../Model/VerifierConfigInterface.php | 221 ++------- .../ProviderVerifier/ProcessRunnerFactory.php | 30 -- .../Standalone/ProviderVerifier/Verifier.php | 367 +++++++-------- .../ProviderVerifier/VerifierProcess.php | 57 --- .../Broker/Service/BrokerHttpClientTest.php | 53 --- tests/PhpPact/FFI/Model/ArrayDataTest.php | 21 + .../Model/Source/BrokerTest.php | 37 ++ .../ProviderVerifier/Model/Source/UrlTest.php | 29 ++ .../Model/VerifierConfigTest.php | 88 ++++ .../ProviderVerifier/VerifierProcessTest.php | 77 --- .../ProviderVerifier/VerifierTest.php | 232 ++------- .../Standalone/ProviderVerifier/verifier.bat | 8 - .../Standalone/ProviderVerifier/verifier.sh | 10 - .../Service/StubServerHttpServiceTest.php | 2 +- tests/_public/index.php | 10 + 59 files changed, 1638 insertions(+), 1822 deletions(-) delete mode 100644 example/phpunit.message.provider.xml delete mode 100644 example/src/Consumer/publish_json_example.php create mode 100644 example/src/Provider/ExampleProvider.php delete mode 100644 example/tests/MessageProvider/ExampleMessageProviderTest.php delete mode 100644 src/PhpPact/Broker/Service/BrokerHttpClient.php delete mode 100644 src/PhpPact/Broker/Service/BrokerHttpClientInterface.php create mode 100644 src/PhpPact/FFI/Exception/CDataNotCreatedException.php create mode 100644 src/PhpPact/FFI/Model/ArrayData.php delete mode 100644 src/PhpPact/Provider/MessageVerifier.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFilters.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/PluginDirTrait.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderState.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransport.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptions.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php delete mode 100644 tests/PhpPact/Broker/Service/BrokerHttpClientTest.php create mode 100644 tests/PhpPact/FFI/Model/ArrayDataTest.php create mode 100644 tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php create mode 100644 tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php create mode 100644 tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php delete mode 100644 tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php delete mode 100755 tests/PhpPact/Standalone/ProviderVerifier/verifier.bat delete mode 100755 tests/PhpPact/Standalone/ProviderVerifier/verifier.sh create mode 100644 tests/_public/index.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff996ef3..427531c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + coverage: none - uses: ramsey/composer-install@v2 with: @@ -48,6 +49,7 @@ jobs: operating-system: [ ubuntu-latest, macos-latest, windows-latest ] php: [ '8.0', '8.1', '8.2' ] dependencies: [ 'lowest', 'locked' ] + timeout-minutes: 5 name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies @@ -60,6 +62,7 @@ jobs: with: extensions: openssl, sockets, curl, zip, ffi php-version: ${{ matrix.php }} + coverage: none - name: Composer install uses: ramsey/composer-install@v2 diff --git a/README.md b/README.md index 5a0c9573..29e72c00 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Table of contents - [Start API](#start-api) - [Provider Verification](#provider-verification) - [Verify From Pact Broker](#verify-from-pact-broker) - - [Verify All from Pact Broker](#verify-all-from-pact-broker) + - [Verify Files in Directory](#verify-files-in-directory) - [Verify Files by Path](#verify-files-by-path) - [Tips](#tips) - [Starting API Asynchronously](#starting-api-asynchronously) @@ -191,7 +191,7 @@ Verify that all interactions took place that were registered. This typically should be in each test, that way the test that failed to verify is marked correctly. ```php -$verifyResult = $builder->verify(); +$verifyResult = $verifier->verify(); $this->assertTrue($verifyResult); ``` @@ -230,51 +230,62 @@ $config = new VerifierConfig(); $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. - ->setProcessTimeout(60) // Set process timeout (optional) - default 60 - ->setProcessIdleTimeout(10) // Set process idle timeout (optional) - default 10 - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ); -// Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. + ->setProviderTags('prod' ,'dev') + ->setProviderBranch('main') + ->setScheme('http') + ->setHost('localhost') + ->setPort(58000) + ->setBasePath('/') + ->setStateChangeUrl(new Uri('http://localhost:58000/change-state')) + ->setBuildUrl(new Uri('http://build.domain.com')) + ->setFilterConsumerNames('someConsumer', 'otherConsumer') + ->setFilterDescription('Send POST to create') + ->setFilterNoState(true) + ->setFilterState('state') + ->setPublishResults(true) + ->setDisableSslVerification(true) + ->setStateChangeAsBody(false) + ->setStateChangeTeardown(true) + ->setRequestTimeout(500); + $verifier = new Verifier($config); -$verifier->verify('someConsumer', 'master'); // The tag is option. If no tag is set it will just grab the latest. -// This will not be reached if the PACT verifier throws an error, otherwise it was successful. -$this->assertTrue(true, 'Pact Verification has failed.'); +$selectors = (new ConsumerVersionSelectors()) + ->addSelector('{"tag":"foo","latest":true}') + ->addSelector('{"tag":"bar","latest":true}'); + +$broker = new Broker(); +$broker + ->setUrl(new Uri('http://localhost')) + ->setUsername('user') + ->setPassword('pass') + ->setToken('token') + ->setEnablePending(true) + ->setIncludeWipPactSince('2020-01-30') + ->setProviderTags(['prod']) + ->setProviderBranch('main') + ->setConsumerVersionSelectors($selectors) + ->setConsumerVersionTags(['dev']); + +$verifier->addBroker($broker); + +$verifyResult = $verifier->verify(); + +$this->assertTrue($verifyResult); ``` -##### Verify All from Pact Broker +##### Verify Files in Directory -This will grab every Pact file associated with the given provider. +This allows local Pact file testing. ```php -public function testPactVerifyAll() +public function testPactVerifyFilesInDirectory() { - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - - // Verify that all consumers of 'someProvider' are valid. - $verifier = new Verifier($config); - $verifier->verifyAll(); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifier->addDirectory('C:\SomePath'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } ``` @@ -283,25 +294,13 @@ public function testPactVerifyAll() This allows local Pact file testing. ```php -public function testPactVerifyAll() +public function testPactVerifyFiles() { - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true); // Flag the verifier service to publish the results to the Pact Broker. - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - - // Verify that the files in the array are valid. - $verifier = new Verifier($config); - $verifier->verifyFiles(['C:\SomePath\consumer-provider.json']); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifier->addFile('C:\SomePath\consumer-provider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } ``` @@ -333,7 +332,6 @@ There is a separate repository with an end to end example for both the 2.X and 3 - [2.2.1 tag](https://github.com/mattermack/pact-php-example/tree/2.2.1) for 2.X examples ## Message support -This feature is preliminary as the Pact community as a whole is flushing this out. The goal is not to test the transmission of an object over a bus but instead vet the contents of the message. While examples included focus on a Rabbit MQ, the exact message queue is irrelevant. Initial comparisons require a certain object type to be created by the Publisher/Producer and the Consumer of the message. This includes a metadata set where you @@ -380,42 +378,16 @@ $this->assertTrue($verifyResult); ### Provider Side Message Validation -This may evolve as we work through this implementation. The provider relies heavily on callbacks. -Some of the complexity lies in a consumer and provider having many messages and states between the each other in a single pact. - -For each message, one needs to provide a single provider state. The name of this provider state must be the key to run -a particular message callback on the provider side. See example\tests\MessageProvider - -1. Create your callbacks and states wrapped in a callable object - 1. The array key is a provider state / given() on the consumer side - 1. It is helpful to wrap the whole thing in a lambda if you need to customize paramaters to be passed in -1. Choose your verification method -1. If nothing explodes, #winning - -```php - - $callbacks = array(); +Handle these requests on your provider: - // a hello message is a provider state / given() on the consumer side - $callbacks["a hello message"] = function() { - $content = new \stdClass(); - $content->text ="Hello Mary"; +1. POST /pact-change-state + 1. Set up your database to meet the expectations of the request + 2. Reset the database to its original state. +2. POST /pact-messages + 1. Return message's content in body + 2. Return message's metadata in header `PACT-MESSAGE-METADATA` - $metadata = array(); - $metadata['queue'] = "myKey"; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../output/test_consumer-test_provider.json']); - -``` +[Click here](/example/src/Provider/public/index.php) to see the full sample file. ## Usage for the optional `pact-stub-service` diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md index 0285dbe7..aca9d300 100644 --- a/UPGRADE-9.0.md +++ b/UPGRADE-9.0.md @@ -6,3 +6,47 @@ UPGRADE FROM 8.x to 9.0 * PACT_CORS * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT * PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC + +* Verifier + * Different pacts sources can be configured via `addXxx` methods + + Example Usage: + ```php + $config = new VerifierConfig(); + $config + ->setPort(8000) + ->setProviderName('someProvider') + ->setProviderVersion('1.0.0'); + + $url = new Url(); + $url + ->setUrl(new Uri('http://localhost')) + ->setProviderName('someProvider') + ->setUsername('user') + ->setPassword('pass') + ->setToken('token'); + + $selectors = (new ConsumerVersionSelectors()) + ->addSelector('{"tag":"foo","latest":true}') + ->addSelector('{"tag":"bar","latest":true}'); + + $broker = new Broker(); + $broker + ->setUrl(new Uri('http://localhost')) + ->setProviderName('someProvider') + ->setUsername('user') + ->setPassword('pass') + ->setToken('token') + ->setConsumerVersionSelectors($selectors); + + $verifier = new Verifier($config); + $verifier + ->addFile('C:\SomePath\consumer-provider.json'); + ->addDirectory('C:\OtherPath'); + ->addUrl($url); + ->addBroker($broker); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + ``` diff --git a/composer.json b/composer.json index 3deedf5e..93b18c8b 100644 --- a/composer.json +++ b/composer.json @@ -24,23 +24,14 @@ "composer/semver": "^1.4.0|^3.2.0", "amphp/amp": "^2.5.1", "amphp/byte-stream": "^1.8", - "amphp/dns": "^1.2.3", - "amphp/hpack": "^3.1.0", - "amphp/http-server": "^2.1", "amphp/log": "^1.1", "amphp/process": "^1.1.1", - "amphp/serialization": "^1.0", - "amphp/socket": "^1.1.3", - "amphp/sync": "^1.4.0", - "amphp/cache": "^1.4.0", - "amphp/windows-registry": "v0.3.3", "guzzlehttp/guzzle": "^6.5.8|^7.4.5", "phpunit/phpunit": ">=8.5.23 <10", "tienvx/composer-downloads-plugin": "^1.1.0" }, "require-dev": { "roave/security-advisories": "dev-latest", - "mockery/mockery": "^1.4.2", "slim/slim": "^4.6", "slim/psr7": "^1.2.0", "friendsofphp/php-cs-fixer": "^3.0", @@ -66,6 +57,9 @@ "MessageProvider\\": [ "example/src/MessageProvider", "example/tests/MessageProvider" + ], + "Provider\\": [ + "example/src/Provider" ] } }, diff --git a/example/README.md b/example/README.md index bb79b88e..ab66a43b 100644 --- a/example/README.md +++ b/example/README.md @@ -16,10 +16,6 @@ All examples could be run within tests. ## Consumer Tests for Message Processing vendor/bin/phpunit -c example/phpunit.message.consumer.xml - -## Provider Verification Tests for Message Processing - - vendor/bin/phpunit -c example/phpunit.message.provider.xml ## All tests together diff --git a/example/pacts/someconsumer-someprovider.json b/example/pacts/someconsumer-someprovider.json index c502e5ba..f9a78bce 100644 --- a/example/pacts/someconsumer-someprovider.json +++ b/example/pacts/someconsumer-someprovider.json @@ -47,7 +47,7 @@ "matchingRules": { "$.body.message": { "match": "regex", - "regex": "(Hello, )[A-Za-z]" + "regex": "(Hello, )[A-Za-z]+" } } }, diff --git a/example/pacts/test_consumer-test_provider.json b/example/pacts/test_consumer-test_provider.json index a015e9ae..ae5e2dab 100644 --- a/example/pacts/test_consumer-test_provider.json +++ b/example/pacts/test_consumer-test_provider.json @@ -47,7 +47,7 @@ ], "metadata": { "pactSpecification": { - "version": "2.0.0" + "version": "3.0.0" } } } diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 02a047d5..3cc67cbf 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -13,9 +13,6 @@ ./tests/MessageConsumer - - ./tests/MessageProvider - diff --git a/example/phpunit.message.provider.xml b/example/phpunit.message.provider.xml deleted file mode 100644 index 2844c97d..00000000 --- a/example/phpunit.message.provider.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ./tests/MessageProvider - - - diff --git a/example/src/Consumer/publish_json_example.php b/example/src/Consumer/publish_json_example.php deleted file mode 100644 index 9e91327b..00000000 --- a/example/src/Consumer/publish_json_example.php +++ /dev/null @@ -1,16 +0,0 @@ - 'someConsumer', - 'provider' => 'someProvider' -]); - -$httpService->publishJson('1.0.0', $json); diff --git a/example/src/MessageProvider/ExampleMessageProvider.php b/example/src/MessageProvider/ExampleMessageProvider.php index d038e8f2..a8f8c60e 100644 --- a/example/src/MessageProvider/ExampleMessageProvider.php +++ b/example/src/MessageProvider/ExampleMessageProvider.php @@ -4,32 +4,20 @@ class ExampleMessageProvider { - /** @var array */ - private $metadata; + private array $metadata; - /** - * @var mixed - */ - private $contents; + private mixed $contents; - public function __construct($metadata = []) + public function __construct(array $metadata = []) { $this->metadata = $metadata; } - /** - * @return array - */ public function getMetadata(): array { return $this->metadata; } - /** - * @param array $metadata - * - * @return ExampleMessageProvider - */ public function setMetadata(array $metadata): self { $this->metadata = $metadata; @@ -37,20 +25,12 @@ public function setMetadata(array $metadata): self return $this; } - /** - * @return mixed - */ - public function getContents() + public function getContents(): mixed { return $this->contents; } - /** - * @param mixed $contents - * - * @return ExampleMessageProvider - */ - public function setContents($contents) + public function setContents(mixed $contents): self { $this->contents = $contents; @@ -59,10 +39,8 @@ public function setContents($contents) /** * Build metadata and content for message - * - * @return string */ - public function Build() + public function Build(): string { $obj = new \stdClass(); $obj->metadata = $this->metadata; diff --git a/example/src/Provider/ExampleProvider.php b/example/src/Provider/ExampleProvider.php new file mode 100644 index 00000000..c7fddb72 --- /dev/null +++ b/example/src/Provider/ExampleProvider.php @@ -0,0 +1,66 @@ +messages = [ + 'an alligator named Mary exists' => [ + 'metadata' => [ + 'queue' => 'wind cries', + 'routing_key' => 'wind cries', + ], + 'contents' => [ + 'text' => 'Hello Mary', + ] + ], + 'footprints dressed in red' => [ + 'metadata' => [ + 'queue' => 'And the clowns have all gone to bed', + 'routing_key' => 'And the clowns have all gone to bed', + ], + 'contents' => [ + 'song' => 'And the wind whispers Mary', + ] + ], + ]; + } + + public function sayHello(string $name): string + { + return "Hello, {$name}"; + } + + public function sayGoodbye(string $name): string + { + return "Goodbye, {$name}"; + } + + public function dispatchMessage(string $description, array $providerStates): ?ExampleMessageProvider + { + if (!isset($this->messages[$description])) { + return null; + } + + return (new ExampleMessageProvider()) + ->setMetadata($this->messages[$description]['metadata']) + ->setContents($this->messages[$description]['contents']); + } + + public function changeSate(string $action, string $state, array $params): void + { + $this->currentState = [ + 'action' => $action, + 'state' => $state, + 'params' => $params, + ]; + } +} diff --git a/example/src/Provider/public/index.php b/example/src/Provider/public/index.php index 2a5406ed..48e8a188 100644 --- a/example/src/Provider/public/index.php +++ b/example/src/Provider/public/index.php @@ -1,5 +1,6 @@ addBodyParsingMiddleware(); -$app->get('/hello/{name}', function (Request $request, Response $response) { +$provider = new ExampleProvider(); + +$app->get('/hello/{name}', function (Request $request, Response $response) use ($provider) { $name = $request->getAttribute('name'); - $response->getBody()->write(\json_encode(['message' => "Hello, {$name}"])); + $response->getBody()->write(\json_encode(['message' => $provider->sayHello($name)])); return $response->withHeader('Content-Type', 'application/json'); }); -$app->get('/goodbye/{name}', function (Request $request, Response $response) { +$app->get('/goodbye/{name}', function (Request $request, Response $response) use ($provider) { $name = $request->getAttribute('name'); - $response->getBody()->write(\json_encode(['message' => "Goodbye, {$name}"])); + $response->getBody()->write(\json_encode(['message' => $provider->sayGoodbye($name)])); return $response->withHeader('Content-Type', 'application/json'); }); +$app->post('/pact-messages', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $message = $provider->dispatchMessage($body['description'], $body['providerStates']); + if ($message) { + $response->getBody()->write(\json_encode($message->getContents())); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('Pact-Message-Metadata', \base64_encode(\json_encode($message->getMetadata()))); + } + + return $response; +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $provider->changeSate($body['action'], $body['state'], $body['params']); + + return $response; +}); + $app->run(); diff --git a/example/tests/MessageProvider/ExampleMessageProviderTest.php b/example/tests/MessageProvider/ExampleMessageProviderTest.php deleted file mode 100644 index 21906049..00000000 --- a/example/tests/MessageProvider/ExampleMessageProviderTest.php +++ /dev/null @@ -1,61 +0,0 @@ -text ='Hello Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $callbacks['footprints dressed in red'] = function () { - $content = new \stdClass(); - $content->song ='And the wind whispers Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setPublishResults(false); // Flag the verifier service to publish the results to the Pact Broker. - - // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../pacts/test_consumer-test_provider.json']); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Expects to reach true by running verification'); - } -} diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index f1927caa..e09a09d7 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -3,6 +3,7 @@ namespace Provider; use GuzzleHttp\Psr7\Uri; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; use PhpPact\Standalone\Runner\ProcessRunner; @@ -15,7 +16,7 @@ class PactVerifyTest extends TestCase { /** @var ProcessRunner */ - private $processRunner; + private ProcessRunner $processRunner; /** * Run the PHP build-in web server. @@ -27,6 +28,7 @@ protected function setUp(): void $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); $this->processRunner->run(); + \usleep(300000); // wait for server to start } /** @@ -43,18 +45,31 @@ protected function tearDown(): void public function testPactVerifyConsumer() { $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch - ->setProviderBaseUrl(new Uri('http://localhost:7202')) // URL of the Provider. - ; // Flag the verifier service to publish the results to the Pact Broker. + $config->getProviderInfo() + ->setName('someProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + $config->addProviderTransport( + (new ProviderTransport()) + ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) + ->setPort(7202) + ->setPath('/pact-messages') + ->setScheme('http') + ); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. $verifier = new Verifier($config); - $verifier->verifyFiles([__DIR__ . '/../../pacts/someconsumer-someprovider.json']); + $verifier->addFile(__DIR__ . '/../../pacts/someconsumer-someprovider.json'); + $verifier->addFile(__DIR__ . '/../../pacts/test_consumer-test_provider.json'); - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } } diff --git a/src/PhpPact/Broker/Service/BrokerHttpClient.php b/src/PhpPact/Broker/Service/BrokerHttpClient.php deleted file mode 100644 index 53be68e7..00000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClient.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ - private array $headers; - - /** - * {@inheritdoc} - */ - public function __construct(ClientInterface $httpClient, UriInterface $baseUri, array $headers = []) - { - $this->httpClient = $httpClient; - $this->baseUri = $baseUri; - $this->headers = $headers; - - if (!\array_key_exists('Content-Type', $headers)) { - $this->headers['Content-Type'] = 'application/json'; - } - } - - /** - * {@inheritdoc} - */ - public function publishJson(string $version, string $json): void - { - $array = \json_decode($json, true, 512, JSON_THROW_ON_ERROR); - $consumer = $array['consumer']['name']; - $provider = $array['provider']['name']; - - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/consumer/{$consumer}/version/{$version}"); - - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - 'body' => $json, - ]); - } - - /** - * {@inheritdoc} - */ - public function tag(string $consumer, string $version, string $tag): void - { - $uri = $this->baseUri->withPath("/pacticipants/{$consumer}/versions/{$version}/tags/{$tag}"); - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - ]); - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrls(string $provider, string $version = 'latest'): array - { - if ($version !== 'latest') { - @\trigger_error(\sprintf('The second argument "version" in "%s()" method makes no sense and will be removed in any upcoming major version', __METHOD__), E_USER_DEPRECATED); - } - - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrlsForTag(string $provider, string $tag): array - { - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest/{$tag}"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } -} diff --git a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php b/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php deleted file mode 100644 index e4baa16a..00000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - $headers additional headers - */ - public function __construct(ClientInterface $client, UriInterface $baseUri, array $headers); - - /** - * Publish JSON. - * - * @param string $version Consumer version - * @param string $json PACT File JSON - */ - public function publishJson(string $version, string $json): void; - - /** - * Tag a consumer version with a tag. - */ - public function tag(string $consumer, string $version, string $tag): void; - - /** - * Get all Pact urls for the consumer. - * - * @param string $provider provider name - * @param string $version version of the provider - * - * @return array - */ - public function getAllConsumerUrls(string $provider, string $version = 'latest'): array; - - /** - * Get all Pact URLs for a specific tag. - * - * @return array - */ - public function getAllConsumerUrlsForTag(string $provider, string $tag): array; -} diff --git a/src/PhpPact/FFI/Exception/CDataNotCreatedException.php b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php new file mode 100644 index 00000000..2e17f429 --- /dev/null +++ b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php @@ -0,0 +1,9 @@ +items; + } + + public function getSize(): int + { + return $this->size; + } + + /** + * @param array $values + */ + public static function createFrom(array $values): ?self + { + $size = count($values); + if ($size === 0) { + return null; + } + + $items = FFI::new("char*[{$size}]"); + if ($items === null) { + return throw new CDataNotCreatedException(); + } + foreach ($values as $index => $value) { + $length = \strlen($value); + $itemSize = $length + 1; + $item = FFI::new("char[{$itemSize}]", false); + if ($item === null) { + return throw new CDataNotCreatedException(); + } + FFI::memcpy($item, $value, $length); + $items[$index] = $item; // @phpstan-ignore-line + } + + return new self($items, $size); + } + + public function __destruct() + { + for ($i=0; $i < $this->size; $i++) { + FFI::free($this->items[$i]); // @phpstan-ignore-line + } + } +} diff --git a/src/PhpPact/Provider/MessageVerifier.php b/src/PhpPact/Provider/MessageVerifier.php deleted file mode 100644 index 7c5a7113..00000000 --- a/src/PhpPact/Provider/MessageVerifier.php +++ /dev/null @@ -1,208 +0,0 @@ - */ - protected array $callbacks = []; - - /** - * Default host name for the proxy server - */ - protected string $defaultProxyHost = 'localhost'; - - /** - * Default port for the proxy server to listen on - */ - protected int $defaultProxyPort = 7201; - - /** - * floor(provider-verification timeout / this value) = default verificationDelaySec - */ - protected int $defaultDelayFactor = 3; - - /** - * Set the number of seconds to delay the verification test to allow the proxy server to be stood up - * - * By default, it is a third of the provider-verification timeout - */ - protected float $verificationDelaySec; - - private ?LoggerInterface $logger = null; - - public function __construct(VerifierConfigInterface $config) - { - parent::__construct($config); - - $this->callbacks = []; - - $baseUrl = $this->config->getProviderBaseUrl(); - if ($baseUrl === null) { - $config->setProviderBaseUrl(new Uri("http://{$this->defaultProxyHost}:{$this->defaultProxyPort}")); - } - - // default verification delay - $this->setVerificationDelaySec(\floor($config->getProcessIdleTimeout() / $this->defaultDelayFactor)); - } - - /** - * @param array $callbacks - */ - public function setCallbacks(array $callbacks): self - { - $this->callbacks = $callbacks; - - return $this; - } - - /** - * Add an individual call back - * - * @throws \Exception - */ - public function addCallback(string $key, callable $callback): self - { - if (isset($this->callbacks[$key])) { - throw new \Exception("Callback with key ($key) already exists"); - } - - $this->callbacks[$key] = $callback; - - return $this; - } - - public function setVerificationDelaySec(float $verificationDelaySec): self - { - $this->verificationDelaySec = $verificationDelaySec; - - return $this; - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * @throws \Exception - */ - protected function verifyAction(array $arguments): void - { - if (\count($this->callbacks) < 1) { - throw new \Exception('Callback needs to bet set when using message pacts'); - } - - $callbacks = $this->callbacks; - $uri = $this->config->getProviderBaseUrl(); - - $arguments = \array_merge([Scripts::getProviderVerifier()], $arguments); - - /** - * @throws \Amp\Socket\SocketException - * @throws \Error - * @throws \TypeError - * - * @return \Generator - */ - $lambdaLoop = function () use ($callbacks, $arguments, $uri) { - // spin up a server - $url = "{$uri->getHost()}:{$uri->getPort()}"; - $servers = [ - Socket\Server::listen($url) - ]; - - $logger = $this->getLogger(); - - $server = new Server($servers, new CallableRequestHandler(function (Request $request) use ($callbacks) { - if (\count($callbacks) === 1) { - $callback = \array_pop($callbacks); - } else { - $payload = new Payload($request->getBody()); - $requestBody = yield $payload->buffer(); - $requestBody = \json_decode($requestBody); - $description = $requestBody->description; - - $callback = false; - - if (isset($this->callbacks[$description])) { - $callback = $this->callbacks[$description]; - } - - if ($callback === false) { - throw new \Exception("Pacts with multiple states need to have callbacks key'ed by the description"); - } - } - - //@todo pass $providerStates to the call back - $out = \call_user_func($callback); - - // return response should only happen if the \call_user_fun() - return new Response(Status::OK, [ - 'content-type' => 'application/json;', - ], $out); - }), $logger); - - yield $server->start(); - - // delay long enough for the server to be stood up - $delay = (int) ($this->verificationDelaySec * 1000); - - // call the provider-verification cmd - Loop::delay($delay, function () use ($arguments) { - $cmd = \implode(' ', $arguments); - $process = new Process($cmd); - yield $process->start(); - - $payload = new Payload($process->getStdout()); - print yield $payload->buffer(); - - $code = yield $process->join(); - - // if the provider verification cmd returns a non-zero number, the test failed - if ($code !== 0) { - $this->getLogger()->warning(yield $process->getStderr()->read()); - - throw new \Exception("Pact failed to validate. Exit code: {$code}"); - } - - Loop::stop(); - }); - }; - - Loop::run($lambdaLoop); - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('server'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 02932804..80084fe4 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -31,11 +31,6 @@ public static function getStubService(): string return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-stub-service' . self::getSuffix(); } - public static function getProviderVerifier(): string - { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-provider-verifier' . self::getSuffix(); - } - public static function getBroker(): string { return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker' . self::getSuffix(); diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php new file mode 100644 index 00000000..2b7630fa --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php @@ -0,0 +1,33 @@ +name; + } + + public function setName(?string $name): CallingAppInterface + { + $this->name = $name; + + return $this; + } + + public function getVersion(): ?string + { + return $this->version; + } + + public function setVersion(?string $version): CallingAppInterface + { + $this->version = $version; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php new file mode 100644 index 00000000..bae0ec05 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php @@ -0,0 +1,14 @@ + + */ + private array $filterConsumerNames = []; + + public function setFilterConsumerNames(array $filterConsumerNames): self + { + $this->filterConsumerNames = []; + foreach ($filterConsumerNames as $filterConsumerName) { + $this->addFilterConsumerName($filterConsumerName); + } + + return $this; + } + + public function addFilterConsumerName(string $filterConsumerName): self + { + $this->filterConsumerNames[] = $filterConsumerName; + + return $this; + } + + public function getFilterConsumerNames(): array + { + return $this->filterConsumerNames; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php new file mode 100644 index 00000000..91402e7b --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php @@ -0,0 +1,18 @@ + $filterConsumerNames + */ + public function setFilterConsumerNames(array $filterConsumerNames): self; + + public function addFilterConsumerName(string $filterConsumerName): self; + + /** + * @return array + */ + public function getFilterConsumerNames(): array; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php new file mode 100644 index 00000000..1f5cdce6 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php @@ -0,0 +1,46 @@ +filterDescription; + } + + public function setFilterDescription(?string $filterDescription): self + { + $this->filterDescription = $filterDescription; + + return $this; + } + + public function getFilterNoState(): bool + { + return $this->filterNoState; + } + + public function setFilterNoState(bool $filterNoState): self + { + $this->filterNoState = $filterNoState; + + return $this; + } + + public function getFilterState(): ?string + { + return $this->filterState; + } + + public function setFilterState(?string $filterState): self + { + $this->filterState = $filterState; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php new file mode 100644 index 00000000..51420ae8 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php @@ -0,0 +1,18 @@ +pluginDir; + } + + public function setPluginDir(?string $pluginDir): self + { + $this->pluginDir = $pluginDir; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php new file mode 100644 index 00000000..375af81c --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php @@ -0,0 +1,72 @@ +name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getHost(): ?string + { + return $this->host; + } + + public function setHost(string $host): self + { + $this->host = $host; + + return $this; + } + + public function getScheme(): ?string + { + return $this->scheme; + } + + public function setScheme(?string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function setPort(?int $port): self + { + $this->port = $port; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): self + { + $this->path = $path; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php new file mode 100644 index 00000000..d76d297a --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php @@ -0,0 +1,26 @@ +stateChangeUrl; + } + + public function setStateChangeUrl(?UriInterface $stateChangeUrl): self + { + $this->stateChangeUrl = $stateChangeUrl; + + return $this; + } + + public function setStateChangeAsBody(bool $stateChangeAsBody): self + { + $this->stateChangeAsBody = $stateChangeAsBody; + + return $this; + } + + public function isStateChangeAsBody(): bool + { + return $this->stateChangeAsBody; + } + + public function setStateChangeTeardown(bool $stateChangeTeardown): self + { + $this->stateChangeTeardown = $stateChangeTeardown; + + return $this; + } + + public function isStateChangeTeardown(): bool + { + return $this->stateChangeTeardown; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php new file mode 100644 index 00000000..a51f8619 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php @@ -0,0 +1,20 @@ +protocol; + } + + public function setProtocol(?string $protocol): self + { + $this->protocol = $protocol; + + return $this; + } + + public function getScheme(): ?string + { + return $this->scheme; + } + + public function setScheme(?string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function setPort(?int $port): self + { + $this->port = $port; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): self + { + $this->path = $path; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php new file mode 100644 index 00000000..ad269b16 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php @@ -0,0 +1,28 @@ + + */ + private array $providerTags = []; + private string $providerVersion; + private ?UriInterface $buildUrl = null; + private ?string $providerBranch = null; + + public function getProviderTags(): array + { + return $this->providerTags; + } + + public function setProviderTags(array $providerTags): self + { + $this->providerTags = []; + foreach ($providerTags as $providerTag) { + $this->addProviderTag($providerTag); + } + + return $this; + } + + public function addProviderTag(string $providerTag): self + { + $this->providerTags[] = $providerTag; + + return $this; + } + + public function getProviderVersion(): string + { + return $this->providerVersion; + } + + public function setProviderVersion(string $providerVersion): self + { + $this->providerVersion = $providerVersion; + + return $this; + } + + public function getBuildUrl(): ?UriInterface + { + return $this->buildUrl; + } + + public function setBuildUrl(?UriInterface $buildUrl): self + { + $this->buildUrl = $buildUrl; + + return $this; + } + + public function getProviderBranch(): ?string + { + return $this->providerBranch; + } + + public function setProviderBranch(?string $providerBranch): self + { + $this->providerBranch = $providerBranch; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php new file mode 100644 index 00000000..00429e89 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php @@ -0,0 +1,32 @@ + + */ + public function getProviderTags(): array; + + /** + * @param array $providerTags + */ + public function setProviderTags(array $providerTags): self; + + public function addProviderTag(string $providerTag): self; + + public function getProviderVersion(): string; + + public function setProviderVersion(string $providerVersion): self; + + public function getBuildUrl(): ?UriInterface; + + public function setBuildUrl(UriInterface $buildUrl): self; + + public function getProviderBranch(): ?string; + + public function setProviderBranch(?string $providerBranch): self; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php new file mode 100644 index 00000000..5e6c25a8 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php @@ -0,0 +1,33 @@ +disableSslVerification; + } + + public function setDisableSslVerification(bool $disableSslVerification): self + { + $this->disableSslVerification = $disableSslVerification; + + return $this; + } + + public function setRequestTimeout(int $requestTimeout): self + { + $this->requestTimeout = $requestTimeout; + + return $this; + } + + public function getRequestTimeout(): int + { + return $this->requestTimeout; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php new file mode 100644 index 00000000..093fbbf7 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php @@ -0,0 +1,14 @@ +> */ - private array $selectors; + /** @var array */ + private array $selectors = []; /** * @param array $selectors @@ -30,37 +30,31 @@ public function addSelector(string $selector): self return $this; } - #[\ReturnTypeWillChange] - public function current() + public function current(): string { return $this->selectors[$this->position]; } - #[\ReturnTypeWillChange] - public function next() + public function next(): void { ++$this->position; } - #[\ReturnTypeWillChange] public function key(): int { return $this->position; } - #[\ReturnTypeWillChange] public function valid(): bool { return isset($this->selectors[$this->position]); } - #[\ReturnTypeWillChange] - public function rewind() + public function rewind(): void { $this->position = 0; } - #[\ReturnTypeWillChange] public function count(): int { return \count($this->selectors); diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php new file mode 100644 index 00000000..8b1cc29d --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php @@ -0,0 +1,118 @@ + + */ + private array $providerTags = []; + protected ?string $providerBranch = null; + protected ConsumerVersionSelectors $consumerVersionSelectors; + /** + * @var array + */ + private array $consumerVersionTags = []; + + public function __construct() + { + $this->consumerVersionSelectors = new ConsumerVersionSelectors(); + } + + public function isEnablePending(): bool + { + return $this->enablePending; + } + + public function setEnablePending(bool $enablePending): self + { + $this->enablePending = $enablePending; + + return $this; + } + + public function setIncludeWipPactSince(?string $date): self + { + $this->wipPactSince = $date; + + return $this; + } + + public function getIncludeWipPactSince(): ?string + { + return $this->wipPactSince; + } + + public function getProviderTags(): array + { + return $this->providerTags; + } + + public function setProviderTags(array $providerTags): self + { + $this->providerTags = []; + foreach ($providerTags as $providerTag) { + $this->addProviderTag($providerTag); + } + + return $this; + } + + public function addProviderTag(string $providerTag): self + { + $this->providerTags[] = $providerTag; + + return $this; + } + + public function getProviderBranch(): ?string + { + return $this->providerBranch; + } + + public function setProviderBranch(?string $providerBranch): self + { + $this->providerBranch = $providerBranch; + + return $this; + } + + public function getConsumerVersionSelectors(): ConsumerVersionSelectors + { + return $this->consumerVersionSelectors; + } + + public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self + { + $this->consumerVersionSelectors = $selectors; + + return $this; + } + + public function getConsumerVersionTags(): array + { + return $this->consumerVersionTags; + } + + public function setConsumerVersionTags(array $consumerVersionTags): self + { + $this->consumerVersionTags = []; + foreach ($consumerVersionTags as $consumerVersionTag) { + $this->addConsumerVersionTag($consumerVersionTag); + } + + return $this; + } + + public function addConsumerVersionTag(string $consumerVersionTag): self + { + $this->consumerVersionTags[] = $consumerVersionTag; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php new file mode 100644 index 00000000..45419b23 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php @@ -0,0 +1,52 @@ + + */ + public function getProviderTags(): array; + + /** + * @param array $providerTags + */ + public function setProviderTags(array $providerTags): self; + + public function addProviderTag(string $providerTag): self; + + public function getProviderBranch(): ?string; + + public function setProviderBranch(?string $providerBranch): self; + + public function getConsumerVersionSelectors(): ConsumerVersionSelectors; + + public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self; + + /** + * @return array + */ + public function getConsumerVersionTags(): array; + + /** + * @param array $consumerVersionTags + */ + public function setConsumerVersionTags(array $consumerVersionTags): self; + + public function addConsumerVersionTag(string $consumerVersionTag): self; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php new file mode 100644 index 00000000..eb3dd1fb --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php @@ -0,0 +1,61 @@ +url; + } + + public function setUrl(UriInterface $url): self + { + $this->url = $url; + + return $this; + } + + public function getToken(): ?string + { + return $this->token; + } + + public function setToken(?string $token): self + { + $this->token = $token; + + return $this; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php new file mode 100644 index 00000000..cfc8effa --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php @@ -0,0 +1,24 @@ + - */ - private array $providerVersionTag = []; - - private bool $publishResults = false; - - private ?UriInterface $brokerUri = null; - - private ?string $brokerToken = null; - - private ?string $brokerUsername = null; - - private ?string $brokerPassword = null; + private CallingAppInterface $callingApp; + private ProviderInfoInterface $providerInfo; /** - * @var array + * @var array */ - private array $customProviderHeaders = []; - - private bool $verbose = false; - - private ?string $logDirectory = null; - - private ?string $format = null; - - private int $processTimeout = 60; - - private int $processIdleTimeout = 10; + private array $providerTransports = []; - private bool $enablePending = false; - - private ?string $wipPactSince = null; - - /** - * @var array - */ - private array $consumerVersionTag = []; - - private ConsumerVersionSelectors $consumerVersionSelectors; - - /** @var null|callable */ - private $requestFilter = null; + private FilterInfoInterface $filterInfo; + private ProviderStateInterface $providerState; + private VerificationOptionsInterface $verificationOptions; + private ?PublishOptionsInterface $publishOptions = null; + private ConsumerFiltersInterface $consumerFilters; public function __construct() { - $this->consumerVersionSelectors = new ConsumerVersionSelectors(); - } - - /** - * {@inheritdoc} - */ - public function getProviderBaseUrl(): ?UriInterface - { - return $this->providerBaseUrl; - } - - /** - * {@inheritdoc} - */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): VerifierConfigInterface - { - $this->providerBaseUrl = $providerBaseUrl; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderStatesSetupUrl(): ?string - { - return $this->providerStatesSetupUrl; - } - - /** - * {@inheritdoc} - */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): VerifierConfigInterface - { - $this->providerStatesSetupUrl = $providerStatesSetupUrl; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderName(): ?string - { - return $this->providerName; - } - - /** - * {@inheritdoc} - */ - public function setProviderName(string $providerName): VerifierConfigInterface - { - $this->providerName = $providerName; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderVersion(): ?string - { - return $this->providerVersion; - } - - /** - * {@inheritdoc} - */ - public function setProviderVersion(string $providerVersion): VerifierConfigInterface - { - $this->providerVersion = $providerVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderVersionTag(): array - { - return $this->providerVersionTag; - } - - /** - * {@inheritdoc} - */ - public function setProviderVersionTag(string $providerVersionTag): VerifierConfigInterface - { - return $this->addProviderVersionTag($providerVersionTag); - } - - /** - * {@inheritdoc} - */ - public function getConsumerVersionTag(): array - { - return $this->consumerVersionTag; - } - - /** - * {@inheritdoc} - */ - public function addConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface - { - $this->consumerVersionTag[] = $consumerVersionTag; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function addProviderVersionTag(string $providerVersionTag): VerifierConfigInterface - { - $this->providerVersionTag[] = $providerVersionTag; - - return $this; - } - - public function setConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface - { - return $this->addConsumerVersionTag($consumerVersionTag); - } - - public function getConsumerVersionSelectors(): ConsumerVersionSelectors - { - return $this->consumerVersionSelectors; - } - - public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): VerifierConfigInterface - { - $this->consumerVersionSelectors = $selectors; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function isPublishResults(): bool - { - return $this->publishResults; + $this->callingApp = new CallingApp(); + $this->providerInfo = new ProviderInfo(); + $this->filterInfo = new FilterInfo(); + $this->providerState = new ProviderState(); + $this->verificationOptions = new VerificationOptions(); + $this->consumerFilters = new ConsumerFilters(); } - /** - * {@inheritdoc} - */ - public function setPublishResults(bool $publishResults): VerifierConfigInterface + public function setCallingApp(CallingAppInterface $callingApp): self { - $this->publishResults = $publishResults; + $this->callingApp = $callingApp; return $this; } - /** - * {@inheritdoc} - */ - public function getBrokerUri(): ?UriInterface + public function getCallingApp(): CallingAppInterface { - return $this->brokerUri; + return $this->callingApp; } - /** - * {@inheritdoc} - */ - public function setBrokerUri(UriInterface $brokerUri): VerifierConfigInterface + public function setProviderInfo(ProviderInfoInterface $providerInfo): self { - $this->brokerUri = $brokerUri; + $this->providerInfo = $providerInfo; return $this; } - /** - * {@inheritdoc}} - */ - public function getBrokerToken(): ?string + public function getProviderInfo(): ProviderInfoInterface { - return $this->brokerToken; - } - - /** - * {@inheritdoc } - */ - public function setBrokerToken(?string $brokerToken): VerifierConfigInterface - { - $this->brokerToken = $brokerToken; - - return $this; + return $this->providerInfo; } /** * {@inheritdoc} */ - public function getBrokerUsername(): ?string + public function setProviderTransports(array $providerTransports): self { - return $this->brokerUsername; - } - - /** - * {@inheritdoc} - */ - public function setBrokerUsername(string $brokerUsername): VerifierConfigInterface - { - $this->brokerUsername = $brokerUsername; + $this->providerTransports = []; + foreach ($providerTransports as $providerTransport) { + $this->addProviderTransport($providerTransport); + } return $this; } - /** - * {@inheritdoc} - */ - public function getBrokerPassword(): ?string + public function addProviderTransport(ProviderTransportInterface $providerTransport): self { - return $this->brokerPassword; - } - - /** - * {@inheritdoc} - */ - public function setBrokerPassword(string $brokerPassword): self - { - $this->brokerPassword = $brokerPassword; + $this->providerTransports[] = $providerTransport; return $this; } @@ -294,169 +95,73 @@ public function setBrokerPassword(string $brokerPassword): self /** * {@inheritdoc} */ - public function getCustomProviderHeaders(): array - { - return $this->customProviderHeaders; - } - - /** - * {@inheritdoc} - */ - public function setCustomProviderHeaders(array $customProviderHeaders): VerifierConfigInterface + public function getProviderTransports(): array { - $this->customProviderHeaders = $customProviderHeaders; - - return $this; + return $this->providerTransports; } - public function addCustomProviderHeader(string $name, string $value): VerifierConfigInterface + public function setFilterInfo(FilterInfoInterface $filterInfo): self { - $this->customProviderHeaders[] = "$name: $value"; + $this->filterInfo = $filterInfo; return $this; } - /** - * {@inheritdoc} - */ - public function isVerbose(): bool + public function getFilterInfo(): FilterInfoInterface { - return $this->verbose; + return $this->filterInfo; } - /** - * {@inheritdoc} - */ - public function setVerbose(bool $verbose): VerifierConfigInterface + public function setProviderState(ProviderStateInterface $providerState): self { - $this->verbose = $verbose; + $this->providerState = $providerState; return $this; } - /** - * {@inheritdoc} - */ - public function getLogDirectory(): ?string + public function getProviderState(): ProviderStateInterface { - return $this->logDirectory; + return $this->providerState; } - /** - * {@inheritdoc} - */ - public function setLogDirectory(string $log): VerifierConfigInterface - { - $this->logDirectory = $log; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getFormat(): ?string - { - return $this->format; - } - - /** - * {@inheritdoc} - */ - public function setFormat(string $format): VerifierConfigInterface + public function setPublishOptions(?PublishOptionsInterface $publishOptions): self { - $this->format = $format; + $this->publishOptions = $publishOptions; return $this; } - public function setProcessTimeout(int $timeout): VerifierConfigInterface - { - $this->processTimeout = $timeout; - - return $this; - } - - public function setProcessIdleTimeout(int $timeout): VerifierConfigInterface - { - $this->processIdleTimeout = $timeout; - - return $this; - } - - public function getProcessTimeout(): int - { - return $this->processTimeout; - } - - public function getProcessIdleTimeout(): int - { - return $this->processIdleTimeout; - } - - /** - * {@inheritdoc} - */ - public function isEnablePending(): bool + public function getPublishOptions(): ?PublishOptionsInterface { - return $this->enablePending; + return $this->publishOptions; } - /** - * {@inheritdoc} - */ - public function setEnablePending(bool $pending): VerifierConfigInterface + public function isPublishResults(): bool { - $this->enablePending = $pending; - - return $this; + return $this->publishOptions !== null; } - /** - * {@inheritdoc} - */ - public function setIncludeWipPactSince(string $date): VerifierConfigInterface + public function setConsumerFilters(ConsumerFiltersInterface $consumerFilters): self { - $this->wipPactSince = $date; + $this->consumerFilters = $consumerFilters; return $this; } - /** - * {@inheritdoc} - */ - public function getIncludeWipPactSince(): ?string - { - return $this->wipPactSince; - } - - public function getRequestFilter(): ?callable + public function getConsumerFilters(): ConsumerFiltersInterface { - return $this->requestFilter; + return $this->consumerFilters; } - public function setRequestFilter(callable $requestFilter): VerifierConfigInterface + public function setVerificationOptions(VerificationOptionsInterface $verificationOptions): self { - $this->requestFilter = $requestFilter; + $this->verificationOptions = $verificationOptions; return $this; } - /** - * {@inheritdoc} - */ - public function setProviderBranch(string $providerBranch): VerifierConfigInterface - { - $this->providerBranch = $providerBranch; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderBranch(): ?string + public function getVerificationOptions(): VerificationOptionsInterface { - return $this->providerBranch; + return $this->verificationOptions; } } diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php index 9b748f9e..6f13cb83 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php @@ -2,223 +2,64 @@ namespace PhpPact\Standalone\ProviderVerifier\Model; -use Psr\Http\Message\UriInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\CallingAppInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ConsumerFiltersInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\FilterInfoInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderInfoInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderStateInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransportInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\PublishOptionsInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\VerificationOptionsInterface; -/** - * Configuration to use with the verifier server. - */ interface VerifierConfigInterface { - /** - * @return null|UriInterface providers base url - */ - public function getProviderBaseUrl(): ?UriInterface; + public function setCallingApp(CallingAppInterface $callingApp): self; - /** - * @param UriInterface $providerBaseUrl providers base url - */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): self; + public function getCallingApp(): CallingAppInterface; - /** - * @return null|string Base URL to setup the provider states at - */ - public function getProviderStatesSetupUrl(): ?string; + public function setProviderInfo(ProviderInfoInterface $providerInfo): self; - /** - * @param string $providerStatesSetupUrl Base URL to setup the provider states at - */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): self; + public function getProviderInfo(): ProviderInfoInterface; /** - * @return null|string name of the provider + * @param array $providerTransports */ - public function getProviderName(): ?string; + public function setProviderTransports(array $providerTransports): self; - /** - * @param string $providerName Name of the provider - */ - public function setProviderName(string $providerName): self; + public function addProviderTransport(ProviderTransportInterface $providerTransport): self; /** - * @return null|string providers version + * @return array */ - public function getProviderVersion(): ?string; + public function getProviderTransports(): array; - /** - * @param string $providerVersion providers version - */ - public function setProviderVersion(string $providerVersion): self; + public function setFilterInfo(FilterInfoInterface $filterInfo): self; - /** - * @param string $providerBranch providers branch name - */ - public function setProviderBranch(string $providerBranch): self; - - /** - * @return array providers version tag - */ - public function getProviderVersionTag(): array; - - /** - * @return null|string providers branch name - */ - public function getProviderBranch(): ?string; - - /** - * @param string $providerVersionTag providers version tag - */ - public function setProviderVersionTag(string $providerVersionTag): self; - - /** - * @return array consumers version tag - */ - public function getConsumerVersionTag(): array; + public function getFilterInfo(): FilterInfoInterface; - /** - * @param string $consumerVersionTag consumers version tag - */ - public function addConsumerVersionTag(string $consumerVersionTag): self; + public function setProviderState(ProviderStateInterface $providerState): self; - /** - * @param string $providerVersionTag provider version tag - */ - public function addProviderVersionTag(string $providerVersionTag): self; + public function getProviderState(): ProviderStateInterface; - public function getConsumerVersionSelectors(): ConsumerVersionSelectors; + public function setPublishOptions(?PublishOptionsInterface $publishOptions): self; - /** - * @param ConsumerVersionSelectors $selectors Consumer version selectors - */ - public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self; + public function getPublishOptions(): ?PublishOptionsInterface; - /** - * @return bool are results going to be published - */ public function isPublishResults(): bool; - /** - * @param bool $publishResults flag to publish results - */ - public function setPublishResults(bool $publishResults): self; - - /** - * @return null|UriInterface url to the pact broker - */ - public function getBrokerUri(): ?UriInterface; - - /** - * @param UriInterface $brokerUri uri to the pact broker - */ - public function setBrokerUri(UriInterface $brokerUri): self; - - /** - * @return null|string token for the pact broker - */ - public function getBrokerToken(): ?string; - - /** - * @param null|string $brokerToken token for the pact broker - */ - public function setBrokerToken(?string $brokerToken): self; - - /** - * @return null|string username for the pact broker if secured - */ - public function getBrokerUsername(): ?string; - - /** - * @param string $brokerUsername username for the pact broker if secured - */ - public function setBrokerUsername(string $brokerUsername): self; - - /** - * @return null|string password for the pact broker if secured - */ - public function getBrokerPassword(): ?string; - - /** - * @param string $brokerPassword password for the pact broker if secured - */ - public function setBrokerPassword(string $brokerPassword): self; - - /** - * @return array custom headers for the request to the provider such as authorization - */ - public function getCustomProviderHeaders(): array; - - /** - * @param array $customProviderHeaders custom headers for the requests to the provider such as authorization - */ - public function setCustomProviderHeaders(array $customProviderHeaders): self; - - public function addCustomProviderHeader(string $name, string $value): self; - - /** - * @return bool is verbosity level increased - */ - public function isVerbose(): bool; - - /** - * @param bool $verbose increase verbosity level - */ - public function setVerbose(bool $verbose): self; - - /** - * @return null|string set the directory for the pact.log file - */ - public function getLogDirectory(): ?string; - - /** - * @param string $log set the directory for the pact.log file - */ - public function setLogDirectory(string $log): self; + public function setConsumerFilters(ConsumerFiltersInterface $consumerFilters): self; - /** - * @return null|string RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used - */ - public function getFormat(): ?string; + public function getConsumerFilters(): ConsumerFiltersInterface; - /** - * @param string $format RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used - */ - public function setFormat(string $format): self; - - public function setProcessTimeout(int $timeout): self; + public function setVerificationOptions(VerificationOptionsInterface $verificationOptions): self; - public function setProcessIdleTimeout(int $timeout): self; + public function getVerificationOptions(): VerificationOptionsInterface; - public function getProcessTimeout(): int; + public function getLogLevel(): ?string; - public function getProcessIdleTimeout(): int; + public function setLogLevel(string $logLevel): self; - /** - * @param bool $pending allow pacts which are in pending state to be verified without causing the overall task to fail - */ - public function setEnablePending(bool $pending): self; + public function getPluginDir(): ?string; - /** - * @return bool is enabled pending pacts - */ - public function isEnablePending(): bool; - - /** - * @param string $date Includes pact marked as WIP since this date. - * Accepted formats: Y-m-d (2020-01-30) or c (ISO 8601 date 2004-02-12T15:19:21+00:00) - */ - public function setIncludeWipPactSince(string $date): self; - - /** - * @return null|string get start date of included WIP Pacts - */ - public function getIncludeWipPactSince(); - - /** - * @return null|callable - */ - public function getRequestFilter(): ?callable; - - /** - * @param callable $requestFilter - */ - public function setRequestFilter(callable $requestFilter): self; + public function setPluginDir(?string $pluginDir): self; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php b/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php deleted file mode 100644 index 257e44a6..00000000 --- a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php +++ /dev/null @@ -1,30 +0,0 @@ -providerVerifier = $providerVerifier ?: Scripts::getProviderVerifier(); - } - - /** - * @param array $arguments - */ - public function createRunner(array $arguments, LoggerInterface $logger = null): ProcessRunner - { - $processRunner = new ProcessRunner($this->providerVerifier, $arguments); - if ($logger) { - $processRunner->setLogger($logger); - } - - return $processRunner; - } -} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index 2bc678b3..d5b6d900 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -2,264 +2,223 @@ namespace PhpPact\Standalone\ProviderVerifier; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Http\GuzzleClient; +use FFI\CData; +use PhpPact\FFI\Client; +use PhpPact\FFI\ClientInterface; +use PhpPact\FFI\Model\ArrayData; +use PhpPact\Standalone\ProviderVerifier\Model\Source\BrokerInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Source\UrlInterface; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; -/** - * Wrapper for the Ruby Standalone Verifier service. - */ class Verifier { - protected int $processTimeout = 60; + protected ClientInterface $client; + protected CData $handle; - protected int $processIdleTimeout = 10; - - protected VerifierConfigInterface $config; - - protected ?BrokerHttpClientInterface $brokerHttpClient = null; - - protected ?VerifierProcess $verifierProcess = null; - - public function __construct( - VerifierConfigInterface $config, - VerifierProcess $verifierProcess = null, - BrokerHttpClient $brokerHttpClient = null - ) { - $this->config = $config; - $this->verifierProcess = $verifierProcess ?: new VerifierProcess(); - $this->processTimeout = $config->getProcessTimeout(); - $this->processIdleTimeout = $config->getProcessIdleTimeout(); - - if ($brokerHttpClient) { - $this->brokerHttpClient = $brokerHttpClient; - } + public function __construct(VerifierConfigInterface $config) + { + $this->client = new Client(); + $this + ->newHandle($config) + ->setProviderInfo($config) + ->setProviderTransports($config) + ->setFilterInfo($config) + ->setProviderState($config) + ->setVerificationOptions($config) + ->setPublishOptions($config) + ->setConsumerFilters($config) + ->setLogLevel($config) + ->setPluginDir($config); } - /** - * @throws \Exception - * - * @return array parameters to be passed into the process - */ - public function getArguments(): array + private function newHandle(VerifierConfigInterface $config): self { - $parameters = []; - - if ($this->config->getProviderName() !== null) { - $parameters[] = "--provider='{$this->config->getProviderName()}'"; - } - - if ($this->config->getProviderBaseUrl() !== null) { - $parameters[] = "--provider-base-url={$this->config->getProviderBaseUrl()}"; - } - - if ($this->config->getProviderVersion() !== null) { - $parameters[] = "--provider-app-version={$this->config->getProviderVersion()}"; - } - - if ($this->config->getProviderBranch() !== null) { - $parameters[] = "--provider-version-branch={$this->config->getProviderBranch()}"; - } - - if (\count($this->config->getConsumerVersionTag()) > 0) { - foreach ($this->config->getConsumerVersionTag() as $tag) { - $parameters[] = "--consumer-version-tag={$tag}"; - } - } + $this->handle = $this->client->call( + 'pactffi_verifier_new_for_application', + $config->getCallingApp()->getName(), + $config->getCallingApp()->getVersion() + ); - if (\count($this->config->getConsumerVersionSelectors()) > 0) { - foreach ($this->config->getConsumerVersionSelectors() as $selector) { - $parameters[] = "--consumer-version-selector='{$selector}'"; - } - } + return $this; + } - if (\count($this->config->getProviderVersionTag()) > 0) { - foreach ($this->config->getProviderVersionTag() as $tag) { - $parameters[] = "--provider-version-tag={$tag}"; - } - } + private function setProviderInfo(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_provider_info', + $this->handle, + $config->getProviderInfo()->getName(), + $config->getProviderInfo()->getScheme(), + $config->getProviderInfo()->getHost(), + $config->getProviderInfo()->getPort(), + $config->getProviderInfo()->getPath() + ); - if ($this->config->getProviderStatesSetupUrl() !== null) { - $parameters[] = "--provider-states-setup-url={$this->config->getProviderStatesSetupUrl()}"; - } + return $this; + } - if ($this->config->isPublishResults() === true) { - $parameters[] = '--publish-verification-results'; + private function setProviderTransports(VerifierConfigInterface $config): self + { + foreach ($config->getProviderTransports() as $transport) { + $this->client->call( + 'pactffi_verifier_add_provider_transport', + $this->handle, + $transport->getProtocol(), + $transport->getPort(), + $transport->getPath(), + $transport->getScheme() + ); } - if ($this->config->getBrokerToken() !== null) { - $parameters[] = "--broker-token={$this->config->getBrokerToken()}"; - } + return $this; + } - if ($this->config->getBrokerUsername() !== null) { - $parameters[] = "--broker-username={$this->config->getBrokerUsername()}"; - } + private function setFilterInfo(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_provider_state', + $this->handle, + $config->getProviderState()->getStateChangeUrl() ? (string) $config->getProviderState()->getStateChangeUrl() : null, + $config->getProviderState()->isStateChangeTeardown(), + $config->getProviderState()->isStateChangeAsBody() + ); - if ($this->config->getBrokerPassword() !== null) { - $parameters[] = "--broker-password={$this->config->getBrokerPassword()}"; - } + return $this; + } - if (count($this->config->getCustomProviderHeaders()) > 0) { - foreach ($this->config->getCustomProviderHeaders() as $customProviderHeader) { - $parameters[] = "--custom-provider-header=\"{$customProviderHeader}\""; - } - } + private function setProviderState(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_filter_info', + $this->handle, + $config->getFilterInfo()->getFilterDescription(), + $config->getFilterInfo()->getFilterState(), + $config->getFilterInfo()->getFilterNoState() + ); - if ($this->config->isVerbose() === true) { - $parameters[] = '--verbose=VERBOSE'; - } + return $this; + } - if ($this->config->getLogDirectory() !== null) { - $parameters[] = "--log-dir={$this->config->getLogDirectory()}"; - } + private function setVerificationOptions(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_verification_options', + $this->handle, + $config->getVerificationOptions()->isDisableSslVerification(), + $config->getVerificationOptions()->getRequestTimeout() + ); - if ($this->config->getFormat() !== null) { - $parameters[] = "--format={$this->config->getFormat()}"; - } + return $this; + } - if ($this->config->isEnablePending() === true) { - $parameters[] = '--enable-pending'; + private function setPublishOptions(VerifierConfigInterface $config): self + { + if ($config->isPublishResults()) { + $providerTags = ArrayData::createFrom($config->getPublishOptions()->getProviderTags()); + $this->client->call( + 'pactffi_verifier_set_publish_options', + $this->handle, + $config->getPublishOptions()->getProviderVersion(), + $config->getPublishOptions()->getBuildUrl(), + $providerTags?->getItems(), + $providerTags?->getSize(), + $config->getPublishOptions()->getProviderBranch() + ); } - if ($this->config->getIncludeWipPactSince() !== null) { - $parameters[] = "--include-wip-pacts-since={$this->config->getIncludeWipPactSince()}"; - } + return $this; + } - if ($this->config->getBrokerUri() !== null) { - $parameters[] = "--pact-broker-base-url={$this->config->getBrokerUri()->__toString()}"; - } + private function setConsumerFilters(VerifierConfigInterface $config): self + { + $filterConsumerNames = ArrayData::createFrom($config->getConsumerFilters()->getFilterConsumerNames()); + $this->client->call( + 'pactffi_verifier_set_consumer_filters', + $this->handle, + $filterConsumerNames?->getItems(), + $filterConsumerNames?->getSize() + ); - return $parameters; + return $this; } - /** - * Make the request to the PACT Verifier Service to run a Pact file tests from the Pact Broker. - * - * @param string $consumerName name of the consumer to be compared against - * @param null|string $tag optional tag of the consumer such as a branch name - * @param null|string $consumerVersion optional specific version of the consumer; this is overridden by tag - * @throws \Exception - */ - public function verify(string $consumerName, string $tag = null, string $consumerVersion = null): self + private function setLogLevel(VerifierConfigInterface $config): self { - $path = "/pacts/provider/{$this->config->getProviderName()}/consumer/{$consumerName}/"; - - if ($tag) { - $path .= "latest/{$tag}/"; - } elseif ($consumerVersion) { - $path .= "version/{$consumerVersion}/"; - } else { - $path .= 'latest/'; + if ($logLevel = $config->getLogLevel()) { + $this->client->call('pactffi_init_with_log_level', $logLevel); } - $uri = $this->config->getBrokerUri()->withPath($path); - - $arguments = \array_merge([$uri->__toString()], $this->getArguments()); - - $this->verifyAction($arguments); - return $this; } - /** - * Provides a way to validate local Pact JSON files. - * - * @param array $files paths to pact json files - * @throws \Exception - */ - public function verifyFiles(array $files): self + private function setPluginDir(VerifierConfigInterface $config): self { - $arguments = \array_merge($files, $this->getArguments()); - - $this->verifyAction($arguments); + if ($pluginDir = $config->getPluginDir()) { + \putenv("PACT_PLUGIN_DIR={$pluginDir}"); + } return $this; } - /** - * Verify all Pacts from the Pact Broker are valid for the Provider. - * @throws \Exception - */ - public function verifyAll(): void + public function addFile(string $file): self { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrls($this->config->getProviderName()); + $this->client->call('pactffi_verifier_add_file_source', $this->handle, $file); - $arguments = \array_merge($arguments, $this->getArguments()); - - $this->verifyAction($arguments); + return $this; } - /** - * Verify all PACTs for a given tag. - * @throws \Exception - */ - public function verifyAllForTag(string $tag): void + public function addDirectory(string $directory): self { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrlsForTag($this->config->getProviderName(), $tag); + $this->client->call('pactffi_verifier_add_directory_source', $this->handle, $directory); - $arguments = \array_merge($arguments, $this->getArguments()); - - $this->verifyAction($arguments); + return $this; } - /** - * Verify all PACTs that match the VerifierConfig - * @throws \Exception - */ - public function verifyFromConfig(): void + public function addUrl(UrlInterface $url): self { - $this->verifyAction($this->getArguments()); - } + $this->client->call( + 'pactffi_verifier_url_source', + $this->handle, + (string) $url->getUrl(), + $url->getUsername(), + $url->getPassword(), + $url->getToken() + ); - /** - * @return array - */ - public function getTimeoutValues(): array - { - return ['process_timeout' => $this->processTimeout, 'process_idle_timeout' => $this->processIdleTimeout]; + return $this; } - /** - * Trigger execution of the Pact Verifier Service. - * - * @param array $arguments - * @throws \Exception - */ - protected function verifyAction(array $arguments): void + public function addBroker(BrokerInterface $broker): self { - $this->verifierProcess->run($arguments, $this->processTimeout, $this->processIdleTimeout); + $providerTags = ArrayData::createFrom($broker->getProviderTags()); + $consumerVersionSelectors = ArrayData::createFrom(iterator_to_array($broker->getConsumerVersionSelectors())); + $consumerVersionTags = ArrayData::createFrom($broker->getConsumerVersionTags()); + $this->client->call( + 'pactffi_verifier_broker_source_with_selectors', + $this->handle, + (string) $broker->getUrl(), + $broker->getUsername(), + $broker->getPassword(), + $broker->getToken(), + $broker->isEnablePending(), + $broker->getIncludeWipPactSince(), + $providerTags?->getItems(), + $providerTags?->getSize(), + $broker->getProviderBranch(), + $consumerVersionSelectors?->getItems(), + $consumerVersionSelectors?->getSize(), + $consumerVersionTags?->getItems(), + $consumerVersionTags?->getSize() + ); + + return $this; } - protected function getBrokerHttpClient(): BrokerHttpClientInterface + public function verify(): bool { - if (!$this->brokerHttpClient) { - $user = $this->config->getBrokerUsername(); - $password = $this->config->getBrokerPassword(); - $token = $this->config->getBrokerToken(); - $reqFilter = $this->config->getRequestFilter(); - - $config = []; - if (\strlen($token) > 0) { - $config = ['headers' => ['Authorization' => 'Bearer ' . $token]]; - } elseif ($user && $password) { - $config = ['auth' => [$user, $password]]; - } - if (\is_callable($reqFilter)) { - $stack = HandlerStack::create(); - $stack->push(Middleware::mapRequest($reqFilter), 'requestFilter'); - $config['handler'] = $stack; - } - if (($sslVerify = \getenv('PACT_BROKER_SSL_VERIFY'))) { - $client['verify'] = $sslVerify !== 'no'; - } - $client = new GuzzleClient($config); - - $this->brokerHttpClient = new BrokerHttpClient($client, $this->config->getBrokerUri()); - } + $error = $this->client->call('pactffi_verifier_execute', $this->handle); + $this->client->call('pactffi_verifier_shutdown', $this->handle); - return $this->brokerHttpClient; + return !$error; } } diff --git a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php b/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php deleted file mode 100644 index 6201c53f..00000000 --- a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php +++ /dev/null @@ -1,57 +0,0 @@ -processRunnerFactory = $processRunnerFactory ?: new ProcessRunnerFactory(); - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * @param array $arguments - * @throws \Exception - */ - public function run(array $arguments, ?int $processTimeout = null, ?int $processIdleTimeout = null): void - { - $logger = $this->getLogger(); - $processRunner = $this->processRunnerFactory->createRunner( - $arguments, - $logger - ); - - $logger->info("Verifying PACT with script:\n{$processRunner->getCommand()}\n\n"); - - $processRunner->runBlocking(); - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('console'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php b/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php deleted file mode 100644 index 7c510e79..00000000 --- a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php +++ /dev/null @@ -1,53 +0,0 @@ - [ - 'pacts' => [ - ['href' => 'pact-url-1'], - ['href' => 'pact-url-2'], - ], - ], - ] - ); - - $streamMock = $this->createMock(StreamInterface::class); - $streamMock->expects($this->once()) - ->method('getContents') - ->will($this->returnValue($expectedContents)); - - $responseMock = $this->createMock(ResponseInterface::class); - $responseMock->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($streamMock)); - - $httpClientMock = $this->createMock(ClientInterface::class); - $httpClientMock->expects($this->once()) - ->method('get') - ->will($this->returnValue($responseMock)); - - $uriMock = $this->createMock(UriInterface::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($this->equalTo($expectedPath)) - ->will($this->returnValue($uriMock)); - - $broker = new BrokerHttpClient($httpClientMock, $uriMock); - $broker->getAllConsumerUrls($provider); - } -} diff --git a/tests/PhpPact/FFI/Model/ArrayDataTest.php b/tests/PhpPact/FFI/Model/ArrayDataTest.php new file mode 100644 index 00000000..b55a5f65 --- /dev/null +++ b/tests/PhpPact/FFI/Model/ArrayDataTest.php @@ -0,0 +1,21 @@ +assertSame(count($branches), $arrayData->getSize()); + foreach ($branches as $index => $branch) { + $this->assertSame($branch, FFI::string($arrayData->getItems()[$index])); + } + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php new file mode 100644 index 00000000..23106df1 --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php @@ -0,0 +1,37 @@ +addSelector('{"tag":"foo","latest":true}') + ->addSelector('{"tag":"bar","latest":true}'); + $consumerVersionTags = ['dev']; + + $subject = (new Broker()) + ->setEnablePending($enablePending) + ->setIncludeWipPactSince($wipPactSince) + ->setProviderTags($providerTags) + ->setProviderBranch($providerBranch) + ->setConsumerVersionSelectors($consumerVersionSelectors) + ->setConsumerVersionTags($consumerVersionTags); + + static::assertSame($enablePending, $subject->isEnablePending()); + static::assertSame($wipPactSince, $subject->getIncludeWipPactSince()); + static::assertSame($providerTags, $subject->getProviderTags()); + static::assertSame($providerBranch, $subject->getProviderBranch()); + static::assertSame($consumerVersionSelectors, $subject->getConsumerVersionSelectors()); + static::assertSame($consumerVersionTags, $subject->getConsumerVersionTags()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php new file mode 100644 index 00000000..20716b8a --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php @@ -0,0 +1,29 @@ +setUrl($url) + ->setToken($token) + ->setUsername($username) + ->setPassword($password); + + static::assertSame($url, $subject->getUrl()); + static::assertSame($token, $subject->getToken()); + static::assertSame($username, $subject->getUsername()); + static::assertSame($password, $subject->getPassword()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php new file mode 100644 index 00000000..48446b0a --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php @@ -0,0 +1,88 @@ +getProviderInfo() + ->setName($providerName) + ->setScheme($scheme) + ->setHost($host) + ->setPort($port) + ->setPath($basePath); + $subject->getFilterInfo() + ->setFilterDescription($filterDescription) + ->setFilterNoState($filterNoState) + ->setFilterState($filterState); + $subject->getProviderState() + ->setStateChangeUrl($stateChangeUrl) + ->setStateChangeAsBody($stateChangeAsBody) + ->setStateChangeTeardown($stateChangeTeardown); + $subject->getVerificationOptions() + ->setRequestTimeout($requestTimeout) + ->setDisableSslVerification($disableSslVerification); + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderTags($providerTags) + ->setProviderVersion($providerVersion) + ->setBuildUrl($buildUrl) + ->setProviderBranch($providerBranch); + $subject->setPublishOptions($publishOptions); + $subject->getConsumerFilters() + ->setFilterConsumerNames($filterConsumerNames); + + $providerInfo = $subject->getProviderInfo(); + static::assertSame($providerName, $providerInfo->getName()); + static::assertSame($scheme, $providerInfo->getScheme()); + static::assertSame($host, $providerInfo->getHost()); + static::assertSame($port, $providerInfo->getPort()); + static::assertSame($basePath, $providerInfo->getPath()); + $filterInfo = $subject->getFilterInfo(); + static::assertSame($filterDescription, $filterInfo->getFilterDescription()); + static::assertSame($filterNoState, $filterInfo->getFilterNoState()); + static::assertSame($filterState, $filterInfo->getFilterState()); + $providerState = $subject->getProviderState(); + static::assertSame($stateChangeUrl, $providerState->getStateChangeUrl()); + static::assertSame($stateChangeAsBody, $providerState->isStateChangeAsBody()); + static::assertSame($stateChangeTeardown, $providerState->isStateChangeTeardown()); + $verificationOptions = $subject->getVerificationOptions(); + static::assertSame($requestTimeout, $verificationOptions->getRequestTimeout()); + static::assertSame($disableSslVerification, $verificationOptions->isDisableSslVerification()); + static::assertSame($publishResults, $subject->isPublishResults()); + $publishOptions = $subject->getPublishOptions(); + static::assertSame($providerTags, $publishOptions->getProviderTags()); + static::assertSame($providerVersion, $publishOptions->getProviderVersion()); + static::assertSame($buildUrl, $publishOptions->getBuildUrl()); + static::assertSame($providerBranch, $publishOptions->getProviderBranch()); + $consumerFilters = $subject->getConsumerFilters(); + static::assertSame($filterConsumerNames, $consumerFilters->getFilterConsumerNames()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php deleted file mode 100644 index 72449939..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php +++ /dev/null @@ -1,77 +0,0 @@ - 'bar']; - - $logger = $this->createMock(LoggerInterface::class); - - $processRunner = $this->createMock(ProcessRunner::class); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments), $this->equalTo($logger)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->setLogger($logger); - $process->run($arguments, 42, 23); - } - - public function testRunWithDefaultLogger() - { - $arguments = ['foo' => 'bar']; - - $processRunner = $this->createMock(ProcessRunner::class); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->run($arguments, 42, 23); - } - - public function testRunForwardsException() - { - $this->expectExceptionMessage('foo'); - $this->expectException(\RuntimeException::class); - - $arguments = ['foo' => 'bar']; - - $expectedException = new \RuntimeException('foo'); - - $processRunner = $this->createMock(ProcessRunner::class); - $processRunner->expects($this->once()) - ->method('runBlocking') - ->will( - $this->returnCallback( - function () use ($expectedException) { - throw $expectedException; - } - ) - ); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->run($arguments, 42, 23); - } -} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 8a5d896a..c6cf11f2 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -2,235 +2,55 @@ namespace PhpPactTest\Standalone\ProviderVerifier; -use GuzzleHttp\Psr7\Uri; -use Monolog\Handler\TestHandler; -use Monolog\Logger; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Standalone\ProviderVerifier\Model\ConsumerVersionSelectors; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; -use PhpPact\Standalone\ProviderVerifier\ProcessRunnerFactory; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\ProviderVerifier\VerifierProcess; +use PhpPact\Standalone\Runner\ProcessRunner; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; class VerifierTest extends TestCase { - public function testGetArguments() - { - $consumerVersionSelectors = (new ConsumerVersionSelectors()) - ->addSelector('{"tag":"foo","latest":true}') - ->addSelector('{"tag":"bar","latest":true}'); - - $config = new VerifierConfig(); - $config - ->setProviderName('some provider with whitespace') - ->setProviderVersion('1.0.0') - ->setProviderBranch('main') - ->addProviderVersionTag('prod') - ->addProviderVersionTag('dev') - ->addConsumerVersionTag('dev') - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setPublishResults(true) - ->setBrokerToken('someToken') - ->setBrokerUsername('someusername') - ->setBrokerPassword('somepassword') - ->setBrokerUri(new Uri('https://example.broker/')) - ->addCustomProviderHeader('key1', 'value1') - ->addCustomProviderHeader('key2', 'value2') - ->setVerbose(true) - ->setLogDirectory('my/log/directory') - ->setFormat('someformat') - ->setProcessTimeout(30) - ->setProcessIdleTimeout(5) - ->setEnablePending(true) - ->setIncludeWipPactSince('2020-01-30') - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ) - ->setConsumerVersionSelectors($consumerVersionSelectors); - - /** @var BrokerHttpClientInterface $brokerHttpService */ - $server = new Verifier($config); - $arguments = $server->getArguments(); - - $this->assertContains('--provider-base-url=http://myprovider:1234', $arguments); - $this->assertContains('--provider-states-setup-url=http://someurl:1234', $arguments); - $this->assertContains('--publish-verification-results', $arguments); - $this->assertContains('--broker-token=someToken', $arguments); - $this->assertContains('--broker-username=someusername', $arguments); - $this->assertContains('--broker-password=somepassword', $arguments); - $this->assertContains('--custom-provider-header="key1: value1"', $arguments); - $this->assertContains('--custom-provider-header="key2: value2"', $arguments); - $this->assertContains('--verbose=VERBOSE', $arguments); - $this->assertContains('--log-dir=my/log/directory', $arguments); - $this->assertContains('--format=someformat', $arguments); - $this->assertContains('--provider-version-tag=prod', $arguments); - $this->assertContains('--provider-version-tag=dev', $arguments); - $this->assertContains('--provider-version-branch=main', $arguments); - $this->assertContains('--consumer-version-tag=dev', $arguments); - $this->assertSame(['process_timeout' => 30, 'process_idle_timeout' => 5], $server->getTimeoutValues()); - $this->assertContains('--enable-pending', $arguments); - $this->assertContains('--include-wip-pacts-since=2020-01-30', $arguments); - $this->assertContains('--consumer-version-selector=\'{"tag":"foo","latest":true}\'', $this->stripSpaces($arguments)); - $this->assertContains('--consumer-version-selector=\'{"tag":"bar","latest":true}\'', $this->stripSpaces($arguments)); - $this->assertContains('--provider=\'some provider with whitespace\'', $arguments); - $this->assertContains('--pact-broker-base-url=https://example.broker/', $arguments); - } + /** @var ProcessRunner */ + private ProcessRunner $processRunner; /** - * Strip spaces for Windows CMD + * Run the PHP build-in web server. */ - private function stripSpaces($arr) - { - $newArr = []; - foreach ($arr as $str) { - $newArr[] = str_ireplace(' ', '', $str); - } - return $newArr; - } - - public function testGetArgumentsEmptyConfig() + protected function setUp(): void { - $this->assertEmpty((new Verifier(new VerifierConfig()))->getArguments()); - } - - /** - * @dataProvider dataProviderForBrokerPathTest - * - * @param string $consumerName - * @param string $providerName - * @param null|string $tag - * @param null|string $version - * @param string $path - */ - public function testBuildValidPathToPactBroker($consumerName, $providerName, $tag, $version, $path) - { - $expectedUrltoBroker = 'http://mock/' . $path; - - /** @var Uri $uriMock */ - $uriMock = $this->createMock(Uri::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($path) - ->willReturn($uriMock); - - $uriMock->expects($this->any()) - ->method('__toString') - ->willReturn($expectedUrltoBroker); - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrltoBroker) { - return \in_array($expectedUrltoBroker, $args); - }) - ); - - $config = new VerifierConfig(); - $config->setProviderName($providerName) - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setBrokerUri($uriMock) - ->setVerbose(true); - - $verifier = new Verifier($config, $verifierProcessMock); + $publicPath = __DIR__ . '/../../../_public/'; - $verifier->verify($consumerName, $tag, $version); - } - - public function dataProviderForBrokerPathTest() - { - $consumerName = 'someProviderName'; - $providerName = 'someProviderName'; - $tag = '1.0.0'; - $version = '11111'; + $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); - return [ - [$consumerName, $providerName, null, $version, "/pacts/provider/$providerName/consumer/$consumerName/version/$version/"], - [$consumerName, $providerName, $tag, null, "/pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, $tag, $version, "/pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, null, null, "/pacts/provider/$providerName/consumer/$consumerName/latest/"], - ]; + $this->processRunner->run(); + \usleep(300000); // wait for server to start } /** - * @dataProvider provideDataForVerifyAll - * - * @param string $providerName - * @param string $providerVersion - * @param bool $forceLatest - * @param mixed $expectedProviderVersion + * Stop the web server process once complete. */ - public function testIfDataForVerifyAllIsConvertedCorrectly($providerName, $providerVersion) + protected function tearDown(): void { - $expectedUrl1 = 'expectedUrl1'; - $expectedUrl2 = 'expectedUrl2'; - $expectedPactUrls = [$expectedUrl1, $expectedUrl2]; - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrl1, $expectedUrl2) { - return \in_array($expectedUrl1, $args) && \in_array($expectedUrl2, $args); - }) - ); - - $brokerHttpClient = $this->createMock(BrokerHttpClient::class); - - $brokerHttpClient->expects($this->once()) - ->method('getAllConsumerUrls') - ->with($this->equalTo($providerName)) - ->willReturn($expectedPactUrls); - - $config = new VerifierConfig(); - $config->setProviderName($providerName); - $config->setProviderVersion($providerVersion); - - $verifier = new Verifier($config, $verifierProcessMock, $brokerHttpClient); - $verifier->verifyAll(); + $this->processRunner->stop(); } - public function provideDataForVerifyAll() + public function testVerify(): void { - return [ - ['someProvider', '1.0.0'], - ['someProvider', '1.2.3'], - ]; - } - - public function testRunShouldLogOutputIfCmdFails() - { - if ('\\' !== \DIRECTORY_SEPARATOR) { - $cmd = __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.sh'; - } else { - $cmd = 'cmd /c' . __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.bat'; - } - - $process = new VerifierProcess(new ProcessRunnerFactory($cmd)); - - $logger = new Logger('console', [$handler = new TestHandler()]); - $process->setLogger($logger); - - try { - $exception = null; - $process->run([], 60, 10); - } catch (\Exception $e) { - $exception = $e; + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('someProvider') + ->setHost('localhost') + ->setPort(7202) + ->setScheme('http') + ->setPath('/'); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); } - $logMessages = $handler->getRecords(); + $verifier = new Verifier($config); + $verifier->addDirectory(__DIR__ . '/../../../_resources'); - $this->assertGreaterThan(2, \count($logMessages)); - $this->assertStringContainsString('first line', $logMessages[\count($logMessages) - 2]['message']); - $this->assertStringContainsString('second line', $logMessages[\count($logMessages) - 1]['message']); + $verifyResult = $verifier->verify(); - $this->assertNotNull($exception); + $this->assertTrue($verifyResult); } } diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat b/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat deleted file mode 100755 index 4cdede33..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat +++ /dev/null @@ -1,8 +0,0 @@ -@ECHO OFF - -REM this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -ECHO "first line" -ECHO "second line" 1>&2 - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh b/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh deleted file mode 100755 index c196300a..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -echoerr() { echo "$@" 1>&2; } - -echo "first line" -echoerr "second line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php index 2990bd58..8aa3a00b 100644 --- a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php +++ b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php @@ -40,7 +40,7 @@ protected function setUp(): void ->setEndpoint($endpoint); $this->stubServer = new StubServer($this->config); - $this->stubServer->start(10); + $this->stubServer->start(); $this->service = new StubServerHttpService(new GuzzleClient(), $this->config); } diff --git a/tests/_public/index.php b/tests/_public/index.php new file mode 100644 index 00000000..cc71ddcc --- /dev/null +++ b/tests/_public/index.php @@ -0,0 +1,10 @@ + [ + [ + 'name' => 'g', + ], + ], +]); From e96453fd7947926799ebf418a00c98d691682c5b Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 3 Jan 2023 22:38:12 +0700 Subject: [PATCH 049/298] Use Rust FFI: Stub Server --- README.md | 10 +- composer.json | 21 +- .../Standalone/Installer/Model/Scripts.php | 9 +- .../Service/StubServerHttpService.php | 25 -- .../StubServerHttpServiceInterface.php | 9 - .../Standalone/StubService/StubServer.php | 64 ++++- .../StubService/StubServerConfig.php | 252 ++++++++++++++---- .../StubService/StubServerConfigInterface.php | 131 +++++++-- .../Service/StubServerHttpServiceTest.php | 22 +- .../StubServer/StubServerConfigTest.php | 60 ++++- .../Standalone/StubServer/StubServerTest.php | 12 +- 11 files changed, 450 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index 29e72c00..f4c998c6 100644 --- a/README.md +++ b/README.md @@ -394,14 +394,12 @@ Handle these requests on your provider: If you would like to test with fixtures, you can use the `pact-stub-service` like this: ```php -$pactLocation = __DIR__ . '/someconsumer-someprovider.json'; -$host = 'localhost'; -$port = 7201; -$endpoint = 'test'; +$files = [__DIR__ . '/someconsumer-someprovider.json']; +$port = 7201; +$endpoint = 'test'; $config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) + ->setFiles($files) ->setPort($port) ->setEndpoint($endpoint); diff --git a/composer.json b/composer.json index 93b18c8b..68129809 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "amphp/process": "^1.1.1", "guzzlehttp/guzzle": "^6.5.8|^7.4.5", "phpunit/phpunit": ">=8.5.23 <10", - "tienvx/composer-downloads-plugin": "^1.1.0" + "tienvx/composer-downloads-plugin": "^1.2.0" }, "require-dev": { "roave/security-advisories": "dev-latest", @@ -77,10 +77,15 @@ "variables": { "{$os}": "PHP_OS_FAMILY === 'Windows' ? 'win32' : (PHP_OS === 'Darwin' ? 'osx' : 'linux')", "{$architecture}": "PHP_OS === 'Linux' ? '-x86_64' : ''", - "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'zip' : 'tar.gz'" + "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'zip' : 'tar.gz'", + "{$keep}": "PHP_OS_FAMILY === 'Windows' ? 'pact-broker.bat' : 'pact-broker'" }, "url": "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{$version}/pact-{$version}-{$os}{$architecture}.{$extension}", - "path": "bin/pact-ruby-standalone" + "path": "bin/pact-ruby-standalone", + "ignore": [ + "bin/*", + "!bin/{$keep}" + ] }, "pact-ffi-headers": { "version": "0.4.4", @@ -97,6 +102,16 @@ }, "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/{$prefix}-{$os}-{$architecture}.{$extension}.gz", "path": "bin/pact-ffi-lib/pact.{$extension}" + }, + "pact-stub-server": { + "version": "0.5.3", + "variables": { + "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", + "{$extension}": "PHP_OS_FAMILY === 'Windows' ? '.exe' : ''" + }, + "url": "https://github.com/pact-foundation/pact-stub-server/releases/download/v{$version}/pact-stub-server-{$os}-x86_64{$extension}.gz", + "path": "bin/pact-stub-server/pact-stub-server{$extension}", + "executable": true } } }, diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 80084fe4..67e96568 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -28,16 +28,11 @@ public static function getLibrary(): string public static function getStubService(): string { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-stub-service' . self::getSuffix(); + return self::$destinationDir . '/bin/pact-stub-server/pact-stub-server' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : ''); } public static function getBroker(): string { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker' . self::getSuffix(); - } - - private static function getSuffix(): string - { - return (PHP_OS_FAMILY === 'Windows' ? '.bat' : ''); + return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker' . (PHP_OS_FAMILY === 'Windows' ? '.bat' : ''); } } diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php index 8aa9a849..81f5f726 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php @@ -2,7 +2,6 @@ namespace PhpPact\Standalone\StubService\Service; -use PhpPact\Exception\ConnectionException; use PhpPact\Http\ClientInterface; use PhpPact\Standalone\StubService\StubServerConfigInterface; @@ -23,30 +22,6 @@ public function __construct(ClientInterface $client, StubServerConfigInterface $ $this->config = $config; } - /** - * {@inheritdoc} - */ - public function healthCheck(): bool - { - $uri = $this->config->getBaseUri()->withPath('/'); - - $response = $this->client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - $body = $response->getBody()->getContents(); - - if ($response->getStatusCode() !== 200 - || $body !== "Mock service running\n") { - throw new ConnectionException('Failed to receive a successful response from the Stub Server.'); - } - - return true; - } - /** * {@inheritdoc} * @throws \JsonException diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php index 307a9448..e1b84aa0 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php @@ -2,17 +2,8 @@ namespace PhpPact\Standalone\StubService\Service; -use PhpPact\Exception\ConnectionException; - interface StubServerHttpServiceInterface { - /** - * Verify that the Ruby PhpPact Stub Server is running. - * - * @throws ConnectionException - */ - public function healthCheck(): bool; - /** * Get the current state of the PACT JSON file and write it to disk. */ diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index f19b4c67..4805770c 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -43,6 +43,8 @@ public function start(int $wait = 1): int /** * Stop the Stub Server process. * + * @throws ProcessException + * * @return bool Was stopping successful? * @throws ProcessException */ @@ -60,12 +62,64 @@ private function getArguments(): array { $results = []; - $results[] = $this->config->getPactLocation(); - $results[] = "--host={$this->config->getHost()}"; - $results[] = "--port={$this->config->getPort()}"; + if ($this->config->getBrokerUrl() !== null) { + $results[] = "--broker-url={$this->config->getBrokerUrl()}"; + } + + foreach ($this->config->getDirs() as $dir) { + $results[] = "--dir={$dir}"; + } + + if ($this->config->getExtension() !== null) { + $results[] = "--extension={$this->config->getExtension()}"; + } + + foreach ($this->config->getFiles() as $file) { + $results[] = "--file={$file}"; + } + + if ($this->config->getLogLevel() !== null) { + $results[] = "--loglevel={$this->config->getLogLevel()}"; + } + + if ($this->config->getPort() !== null) { + $results[] = "--port={$this->config->getPort()}"; + } + + if ($this->config->getProviderState() !== null) { + $results[] = "--provider-state={$this->config->getProviderState()}"; + } + + if ($this->config->getProviderStateHeaderName() !== null) { + $results[] = "--provider-state-header-name={$this->config->getProviderStateHeaderName()}"; + } + + if ($this->config->getToken() !== null) { + $results[] = "--token={$this->config->getToken()}"; + } + + foreach ($this->config->getUrls() as $url) { + $results[] = "--url={$url}"; + } + + if ($this->config->getUser() !== null) { + $results[] = "--user={$this->config->getUser()}"; + } + + if ($this->config->isCors()) { + $results[] = '--cors'; + } + + if ($this->config->isCorsReferer()) { + $results[] = '--cors-referer'; + } + + if ($this->config->isEmptyProviderState()) { + $results[] = '--empty-provider-state'; + } - if ($this->config->getLog() !== null) { - $results[] = "--log={$this->config->getLog()}"; + if ($this->config->isInsecureTls()) { + $results[] = '--insecure-tls'; } return $results; diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index e12779a6..beb55ee4 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -10,55 +10,109 @@ */ class StubServerConfig implements StubServerConfigInterface { + private ?UriInterface $brokerUrl = null; + private ?int $port = null; + + private ?string $extension = null; + private ?string $logLevel = null; + private ?string $providerState = null; + private ?string $providerStateHeaderName = null; + private ?string $token = null; + private ?string $user = null; + /** - * Host on which to bind the service. + * @var array */ - private string $host = 'localhost'; - + private array $dirs = []; /** - * Port on which to run the service. + * @var array */ - private int $port = 7201; - - private bool $secure = false; - + private array $files = []; + /** + * @var array + */ + private array $urls = []; + /** + * @var array + */ + private array $consumerNames = []; /** - * File to which to log output. + * @var array */ - private ?string $log = null; + private array $providerNames = []; + + private bool $cors = false; + private bool $corsReferer = false; + private bool $emptyProviderState = false; + private bool $insecureTls = false; - private string $pactLocation; private string $endpoint; - /** - * {@inheritdoc} - */ - public function getHost(): string + public function getBrokerUrl(): ?UriInterface { - return $this->host; + return $this->brokerUrl; } - /** - * {@inheritdoc} - */ - public function setHost(string $host): StubServerConfigInterface + public function setBrokerUrl(UriInterface $brokerUrl): StubServerConfigInterface { - $this->host = $host; + $this->brokerUrl = $brokerUrl; return $this; } - /** - * {@inheritdoc} - */ - public function getPort(): int + public function setDirs(array $dirs): StubServerConfigInterface + { + $this->dirs = array_map(fn (string $dir) => $dir, $dirs); + + return $this; + } + + public function getDirs(): array + { + return $this->dirs; + } + + public function getExtension(): ?string + { + return $this->extension; + } + + public function setExtension(string $extension): StubServerConfigInterface + { + $this->extension = $extension; + + return $this; + } + + public function setFiles(array $files): StubServerConfigInterface + { + $this->files = array_map(fn (string $file) => $file, $files); + + return $this; + } + + public function getFiles(): array + { + return $this->files; + } + + public function setLogLevel(string $logLevel): StubServerConfigInterface + { + $this->logLevel = $logLevel; + + return $this; + } + + public function getLogLevel(): ?string + { + return $this->logLevel; + } + + public function getPort(): ?int { return $this->port; } - /** - * {@inheritdoc} - */ public function setPort(int $port): StubServerConfigInterface { $this->port = $port; @@ -66,70 +120,150 @@ public function setPort(int $port): StubServerConfigInterface return $this; } - /** - * {@inheritdoc} - */ - public function isSecure(): bool + public function getProviderState(): ?string { - return $this->secure; + return $this->providerState; } - /** - * {@inheritdoc} - */ - public function setSecure(bool $secure): StubServerConfigInterface + public function setProviderState(string $providerState): StubServerConfigInterface { - $this->secure = $secure; + $this->providerState = $providerState; return $this; } - /** - * {@inheritdoc} - */ - public function getBaseUri(): UriInterface + public function getProviderStateHeaderName(): ?string + { + return $this->providerStateHeaderName; + } + + public function setProviderStateHeaderName(string $providerStateHeaderName): StubServerConfigInterface { - $protocol = $this->secure ? 'https' : 'http'; + $this->providerStateHeaderName = $providerStateHeaderName; - return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); + return $this; } - /** - * {@inheritdoc} - */ - public function getLog(): ?string + public function getToken(): ?string { - return $this->log; + return $this->token; } - /** - * {@inheritdoc} - */ - public function setLog(string $log): StubServerConfigInterface + public function setToken(?string $token): StubServerConfigInterface + { + $this->token = $token; + + return $this; + } + + public function setUrls(array $urls): StubServerConfigInterface + { + $this->urls = array_map(fn (string $url) => $url, $urls); + + return $this; + } + + public function getUrls(): array + { + return $this->urls; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function setUser(string $user): StubServerConfigInterface + { + $this->user = $user; + + return $this; + } + + public function isCors(): bool + { + return $this->cors; + } + + public function setCors(bool $cors): StubServerConfigInterface { - $this->log = $log; + $this->cors = $cors; return $this; } - public function getPactLocation(): string + public function isCorsReferer(): bool { - return $this->pactLocation; + return $this->corsReferer; } - public function setPactLocation(string $location): self + public function setCorsReferer(bool $corsReferer): StubServerConfigInterface { - $this->pactLocation = $location; + $this->corsReferer = $corsReferer; return $this; } + public function isEmptyProviderState(): bool + { + return $this->emptyProviderState; + } + + public function setEmptyProviderState(bool $emptyProviderState): StubServerConfigInterface + { + $this->emptyProviderState = $emptyProviderState; + + return $this; + } + + public function isInsecureTls(): bool + { + return $this->insecureTls; + } + + public function setInsecureTls(bool $insecureTls): StubServerConfigInterface + { + $this->insecureTls = $insecureTls; + + return $this; + } + + public function setConsumerNames(array $consumerNames): StubServerConfigInterface + { + $this->consumerNames = array_map(fn (string $consumerName) => $consumerName, $consumerNames); + + return $this; + } + + public function getConsumerNames(): array + { + return $this->consumerNames; + } + + public function setProviderNames(array $providerNames): StubServerConfigInterface + { + $this->providerNames = array_map(fn (string $providerName) => $providerName, $providerNames); + + return $this; + } + + public function getProviderNames(): array + { + return $this->providerNames; + } + + public function getBaseUri(): UriInterface + { + return new Uri("http://localhost:{$this->getPort()}"); + } + + public function getEndpoint(): string { return $this->endpoint; } - public function setEndpoint(string $endpoint): self + public function setEndpoint(string $endpoint): StubServerConfigInterface { $this->endpoint = $endpoint; diff --git a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php index 7f232923..ecfe26c0 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php @@ -10,53 +10,146 @@ interface StubServerConfigInterface { /** - * @return string the host of the stub service + * @return null|UriInterface url to the pact broker */ - public function getHost(): string; + public function getBrokerUrl(): ?UriInterface; /** - * @param string $host The host of the stub service + * @param UriInterface $brokerUrl URL of the pact broker to fetch pacts from */ - public function setHost(string $host): self; + public function setBrokerUrl(UriInterface $brokerUrl): self; /** - * @return int the port of the stub service + * @param array $dirs Directory of pact files to load */ - public function getPort(): int; + public function setDirs(array $dirs): self; /** - * @param int $port the port of the stub service + * @return array + */ + public function getDirs(): array; + + public function getExtension(): ?string; + + /** + * @param string $extension File extension to use when loading from a directory (default is json) + */ + public function setExtension(string $extension): self; + + /** + * @param array $files Pact file to load + */ + public function setFiles(array $files): self; + + /** + * @return array + */ + public function getFiles(): array; + + public function getLogLevel(): ?string; + + /** + * @param string $logLevel Log level (defaults to info) [possible values: error, warn, info, debug, trace, none] + */ + public function setLogLevel(string $logLevel): self; + + /** + * @return null|int the port of the stub service + */ + public function getPort(): ?int; + + /** + * @param int $port Port to run on (defaults to random port assigned by the OS) */ public function setPort(int $port): self; /** - * @return bool true if https + * @return null|string state of the provider */ - public function isSecure(): bool; + public function getProviderState(): ?string; /** - * @param bool $secure set to true for https + * @param string $providerState Provider state regular expression to filter the responses by */ - public function setSecure(bool $secure): self; + public function setProviderState(string $providerState): self; /** - * @return UriInterface + * @return null|string name of the header */ - public function getBaseUri(): UriInterface; + public function getProviderStateHeaderName(): ?string; + + /** + * @param string $providerStateHeaderName Name of the header parameter containing the provider state to be used in case multiple matching interactions are found + */ + public function setProviderStateHeaderName(string $providerStateHeaderName): self; + + /** + * @return null|string token for the pact broker + */ + public function getToken(): ?string; + + /** + * @param null|string $token Bearer token to use when fetching pacts from URLS or Pact Broker + */ + public function setToken(?string $token): self; + + /** + * @param array $urls URL of pact file to fetch + */ + public function setUrls(array $urls): self; + + /** + * @return array + */ + public function getUrls(): array; + + /** + * @return null|string user and password + */ + public function getUser(): ?string; + + /** + * @param string $user User and password to use when fetching pacts from URLS or Pact Broker in user:password form + */ + public function setUser(string $user): self; + + public function isCors(): bool; + + public function setCors(bool $cors): self; + + public function isCorsReferer(): bool; + + public function setCorsReferer(bool $corsReferer): self; + + public function isEmptyProviderState(): bool; + + public function setEmptyProviderState(bool $emptyProviderState): self; + + public function isInsecureTls(): bool; + + public function setInsecureTls(bool $insecureTls): self; /** - * @return ?string directory for log output + * @param array $consumerNames Consumer name to use to filter the Pacts fetched from the Pact broker */ - public function getLog(): ?string; + public function setConsumerNames(array $consumerNames): self; /** - * @param string $log directory for log output + * @return array */ - public function setLog(string $log): self; + public function getConsumerNames(): array; - public function getPactLocation(): string; + /** + * @param array $providerNames Provider name to use to filter the Pacts fetched from the Pact broker + */ + public function setProviderNames(array $providerNames): self; - public function setPactLocation(string $location): self; + /** + * @return array + */ + public function getProviderNames(): array; + + public function getBaseUri(): UriInterface; public function getEndpoint(): string; diff --git a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php index 8aa3a00b..9a9769b0 100644 --- a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php +++ b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php @@ -14,13 +14,13 @@ class StubServerHttpServiceTest extends TestCase { /** @var StubServerHttpServiceInterface */ - private $service; + private StubServerHttpServiceInterface $service; /** @var StubServer */ - private $stubServer; + private StubServer $stubServer; /** @var StubServerConfigInterface */ - private $config; + private StubServerConfigInterface $config; /** * @throws MissingEnvVariableException @@ -28,14 +28,12 @@ class StubServerHttpServiceTest extends TestCase */ protected function setUp(): void { - $pactLocation = __DIR__ . '/../../../../_resources/someconsumer-someprovider.json'; - $host = 'localhost'; - $port = 7201; - $endpoint = 'test'; + $files = [__DIR__ . '/../../../../_resources/someconsumer-someprovider.json']; + $port = 7201; + $endpoint = 'test'; $this->config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) + ->setFiles($files) ->setPort($port) ->setEndpoint($endpoint); @@ -49,12 +47,6 @@ protected function tearDown(): void $this->stubServer->stop(); } - public function testHealthCheck() - { - $result = $this->service->healthCheck(); - $this->assertTrue($result); - } - public function testGetJson() { $result = $this->service->getJson(); diff --git a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php index db9aed12..6ae7c400 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Standalone\StubServer; +use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\StubService\StubServerConfig; use PHPUnit\Framework\TestCase; @@ -9,20 +10,59 @@ class StubServerConfigTest extends TestCase { public function testSetters() { - $pactLocation = __DIR__ . '/../../../_resources/someconsumer-someprovider.json'; - $host = 'test-host'; - $port = 1234; - $log = 'test-log-dir/'; + $brokerUrl = new Uri('http://localhost'); + $port = 1234; + $extension = 'json'; + $logLevel = 'debug'; + $providerState = 'state'; + $providerStateHeaderName = 'header'; + $token = 'token'; + $user = 'user:password'; + $dirs = [__DIR__ . '/../../../_resources']; + $files = ['/path/to/pact.json']; + $urls = ['http://example.com/path/to/file.json']; + $consumerNames = ['consumer-1', 'consumer-2']; + $providerNames = ['provider-1', 'provider-2']; + $cors = true; + $corsReferer = true; + $emptyProviderState = true; + $insecureTls = true; $subject = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) + ->setBrokerUrl($brokerUrl) ->setPort($port) - ->setLog($log); + ->setExtension($extension) + ->setLogLevel($logLevel) + ->setProviderState($providerState) + ->setProviderStateHeaderName($providerStateHeaderName) + ->setToken($token) + ->setUser($user) + ->setDirs($dirs) + ->setFiles($files) + ->setUrls($urls) + ->setConsumerNames($consumerNames) + ->setProviderNames($providerNames) + ->setCors($cors) + ->setCorsReferer($corsReferer) + ->setEmptyProviderState($emptyProviderState) + ->setInsecureTls($insecureTls); - static::assertSame($pactLocation, $subject->getPactLocation()); - static::assertSame($host, $subject->getHost()); + static::assertSame($brokerUrl, $subject->getBrokerUrl()); static::assertSame($port, $subject->getPort()); - static::assertSame($log, $subject->getLog()); + static::assertSame($extension, $subject->getExtension()); + static::assertSame($logLevel, $subject->getLogLevel()); + static::assertSame($providerState, $subject->getProviderState()); + static::assertSame($providerStateHeaderName, $subject->getProviderStateHeaderName()); + static::assertSame($token, $subject->getToken()); + static::assertSame($user, $subject->getUser()); + static::assertSame($dirs, $subject->getDirs()); + static::assertSame($files, $subject->getFiles()); + static::assertSame($urls, $subject->getUrls()); + static::assertSame($consumerNames, $subject->getConsumerNames()); + static::assertSame($providerNames, $subject->getProviderNames()); + static::assertSame($cors, $subject->isCors()); + static::assertSame($corsReferer, $subject->isCorsReferer()); + static::assertSame($emptyProviderState, $subject->isEmptyProviderState()); + static::assertSame($insecureTls, $subject->isInsecureTls()); } } diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubServer/StubServerTest.php index 963fabf8..687e5800 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerTest.php @@ -14,16 +14,14 @@ class StubServerTest extends TestCase public function testStartAndStop() { try { - $pactLocation = __DIR__ . '/../../../_resources/someconsumer-someprovider.json'; - $host = 'localhost'; - $port = 7201; - $endpoint = 'test'; + $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; + $port = 7201; + $endpoint= 'test'; $subject = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) + ->setFiles($files) ->setPort($port) - ->setEndpoint($endpoint); + ->setEndpoint($endpoint); $stubServer = new StubServer($subject); $pid = $stubServer->start(); From 6d19fa86d5871a14bdf0ceb4ed98030132384d29 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 3 Jan 2023 23:32:48 +0700 Subject: [PATCH 050/298] Use Rust FFI: Offload process --- .github/workflows/build.yml | 2 +- composer.json | 6 +- example/tests/Provider/PactVerifyTest.php | 14 +- phpstan.neon | 6 + src/PhpPact/Standalone/Broker/Broker.php | 271 +++++++----------- .../Standalone/Installer/Model/Scripts.php | 4 +- .../Standalone/Runner/ProcessRunner.php | 193 ------------- .../Standalone/StubService/StubServer.php | 30 +- .../PhpPact/Standalone/Broker/BrokerTest.php | 11 +- .../ProviderVerifier/VerifierTest.php | 14 +- .../Standalone/Runner/ProcessRunnerTest.php | 74 ----- tests/PhpPact/Standalone/Runner/verifier.bat | 11 - tests/PhpPact/Standalone/Runner/verifier.sh | 13 - 13 files changed, 143 insertions(+), 506 deletions(-) delete mode 100644 src/PhpPact/Standalone/Runner/ProcessRunner.php delete mode 100644 tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php delete mode 100755 tests/PhpPact/Standalone/Runner/verifier.bat delete mode 100755 tests/PhpPact/Standalone/Runner/verifier.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 427531c8..b75b69f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: openssl, sockets, curl, zip, ffi + extensions: sockets, curl, zip, ffi php-version: ${{ matrix.php }} coverage: none diff --git a/composer.json b/composer.json index 68129809..b1ce6078 100644 --- a/composer.json +++ b/composer.json @@ -19,13 +19,9 @@ ], "require": { "php": "^8.0", - "ext-openssl": "*", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", - "amphp/amp": "^2.5.1", - "amphp/byte-stream": "^1.8", - "amphp/log": "^1.1", - "amphp/process": "^1.1.1", + "symfony/process": "^4.4|^5.4|^6.0", "guzzlehttp/guzzle": "^6.5.8|^7.4.5", "phpunit/phpunit": ">=8.5.23 <10", "tienvx/composer-downloads-plugin": "^1.2.0" diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index e09a09d7..4f61e93b 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -6,8 +6,8 @@ use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\Runner\ProcessRunner; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; /** * This is an example on how you could use the included amphp/process wrapper to start your API to run PACT verification against a Provider. @@ -15,8 +15,8 @@ */ class PactVerifyTest extends TestCase { - /** @var ProcessRunner */ - private ProcessRunner $processRunner; + /** @var Process */ + private Process $process; /** * Run the PHP build-in web server. @@ -25,10 +25,10 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../src/Provider/public/'; - $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - $this->processRunner->run(); - \usleep(300000); // wait for server to start + $this->process->start(); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); } /** @@ -36,7 +36,7 @@ protected function setUp(): void */ protected function tearDown(): void { - $this->processRunner->stop(); + $this->process->stop(); } /** diff --git a/phpstan.neon b/phpstan.neon index 1d5823a9..02aff4b2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,9 @@ parameters: level: 7 paths: - src + ignoreErrors: + - + messages: + - '#Method PhpPact\\Standalone\\Broker\\Broker::[a-zA-Z0-9\\_]+\(\) return type has no value type specified in iterable type array\.#' + - '#Method PhpPact\\Standalone\\Broker\\Broker::[a-zA-Z0-9\\_]+\(\) has parameter \$options with no value type specified in iterable type array\.#' + path: src/PhpPact/Standalone/Broker/Broker.php diff --git a/src/PhpPact/Standalone/Broker/Broker.php b/src/PhpPact/Standalone/Broker/Broker.php index f7caa83e..529bf80d 100644 --- a/src/PhpPact/Standalone/Broker/Broker.php +++ b/src/PhpPact/Standalone/Broker/Broker.php @@ -2,8 +2,9 @@ namespace PhpPact\Standalone\Broker; +use Exception; use PhpPact\Standalone\Installer\Model\Scripts; -use PhpPact\Standalone\Runner\ProcessRunner; +use Symfony\Component\Process\Process; class Broker { @@ -17,25 +18,13 @@ public function __construct(BrokerConfig $config) $this->command = Scripts::getBroker(); } - /** - * @throws \Exception - */ - public function canIDeploy(): mixed + public function canIDeploy(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'can-i-deploy', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--version=' . $this->config->getVersion() - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'can-i-deploy', + '--pacticipant', $this->config->getPacticipant(), + '--version', $this->config->getVersion(), + ]); } /** @@ -46,161 +35,93 @@ public function getArguments(): array $parameters = []; if ($this->config->getBrokerUri() !== null) { - $parameters[] = "--broker-base-url={$this->config->getBrokerUri()}"; + $parameters[] = '--broker-base-url'; + $parameters[] = $this->config->getBrokerUri(); } if ($this->config->getBrokerToken() !== null) { - $parameters[] = "--broker-token={$this->config->getBrokerToken()}"; + $parameters[] = '--broker-token'; + $parameters[] = $this->config->getBrokerToken(); } if ($this->config->getBrokerUsername() !== null) { - $parameters[] = "--broker-username={$this->config->getBrokerUsername()}"; + $parameters[] = '--broker-username'; + $parameters[] = $this->config->getBrokerUsername(); } if ($this->config->getBrokerPassword() !== null) { - $parameters[] = "--broker-password={$this->config->getBrokerPassword()}"; + $parameters[] = '--broker-password'; + $parameters[] = $this->config->getBrokerPassword(); } return $parameters; } - /** - * @throws \Exception - */ - public function createOrUpdatePacticipant(): mixed + public function createOrUpdatePacticipant(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-or-update-pacticipant', - '--name=' . $this->config->getName(), - '--repository-url=' . $this->config->getRepositoryUrl(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'create-or-update-pacticipant', + '--name', $this->config->getName(), + '--repository-url', $this->config->getRepositoryUrl(), + ]); } - /** - * @throws \Exception - */ - public function createOrUpdateWebhook(): mixed + public function createOrUpdateWebhook(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-or-update-webhook', - $this->config->getUrl(), - '--request=' . $this->config->getRequest(), - '--header=' . $this->config->getHeader(), - '--data=' . $this->config->getData(), - '--user=' . $this->config->getUser(), - '--consumer=' . $this->config->getConsumer(), - '--provider=' . $this->config->getProvider(), - '--description=' . $this->config->getDescription(), - '--uuid=' . $this->config->getUuid(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'create-or-update-webhook', + $this->config->getUrl(), + '--request', $this->config->getRequest(), + '--header', $this->config->getHeader(), + '--data', $this->config->getData(), + '--user', $this->config->getUser(), + '--consumer', $this->config->getConsumer(), + '--provider', $this->config->getProvider(), + '--description', $this->config->getDescription(), + '--uuid', $this->config->getUuid(), + ]); } - /** - * @throws \Exception - */ - public function createVersionTag(): mixed + public function createVersionTag(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-version-tag', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--version=' . $this->config->getVersion(), - '--tag=' . $this->config->getTag(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'create-version-tag', + '--pacticipant', $this->config->getPacticipant(), + '--version', $this->config->getVersion(), + '--tag', $this->config->getTag(), + ]); } - /** - * @throws \Exception - */ - public function createWebhook(): mixed + public function createWebhook(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-webhook', - $this->config->getUrl(), - '--request=' . $this->config->getRequest(), - '--header=' . $this->config->getHeader(), - '--data=' . $this->config->getData(), - '--user=' . $this->config->getUser(), - '--consumer=' . $this->config->getConsumer(), - '--provider=' . $this->config->getProvider(), - '--description=' . $this->config->getDescription(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'create-webhook', + $this->config->getUrl(), + '--request', $this->config->getRequest(), + '--header', $this->config->getHeader(), + '--data', $this->config->getData(), + '--user', $this->config->getUser(), + '--consumer', $this->config->getConsumer(), + '--provider', $this->config->getProvider(), + '--description', $this->config->getDescription(), + ]); } - /** - * @throws \Exception - */ - public function describeVersion(): mixed + public function describeVersion(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'describe-version', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--output=json', - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'describe-version', + '--pacticipant', $this->config->getPacticipant(), + '--output', 'json', + ]); } - /** - * @throws \Exception - */ - public function listLatestPactVersions(): mixed + public function listLatestPactVersions(): array { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'list-latest-pact-versions', - '--output=json', - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $this->runThenDecodeJson([ + 'list-latest-pact-versions', + '--output', 'json', + ]); } public function publish(): void @@ -208,53 +129,53 @@ public function publish(): void $options = [ 'publish', $this->config->getPactLocations(), - '--consumer-app-version=' . $this->config->getConsumerVersion(), + '--consumer-app-version', $this->config->getConsumerVersion(), ]; if (null !== $this->config->getBranch()) { - $options[] = '--branch=' . $this->config->getBranch(); + $options[] = '--branch'; + $options[] = $this->config->getBranch(); } if (null !== $this->config->getTag()) { - $options[] = '--tag=' . $this->config->getTag(); + $options[] = '--tag'; + $options[] = $this->config->getTag(); } - $runner = new ProcessRunner( - $this->command, - \array_merge( - $options, - $this->getArguments() - ) - ); + $this->run($options); + } + + public function testWebhook(): array + { + return $this->runThenDecodeJson([ + 'test-webhook', + '--uuid', $this->config->getUuid(), + ]); + } - $runner->runBlocking(); + public function generateUuid(): string + { + return \rtrim($this->run(['generate-uuid'])); } - /** - * @throws \Exception - */ - public function testWebhook(): mixed + private function run(array $options): string { - $runner = new ProcessRunner( + $process = new Process([ $this->command, - \array_merge( - [ - 'test-webhook', - '--uuid=' . $this->config->getUuid(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); + ...$options, + ...$this->getArguments(), + ]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new Exception("PactPHP Process returned non-zero exit code: {$process->getExitCode()}", $process->getExitCode()); + } - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); + return $process->getOutput(); } - public function generateUuid(): string + private function runThenDecodeJson(array $options): array { - $runner = new ProcessRunner($this->command, ['generate-uuid']); - $runner->runBlocking(); - - return \rtrim($runner->getOutput()); + return \json_decode($this->run($options), true, 512, JSON_THROW_ON_ERROR); } } diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 67e96568..3c7747c1 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -28,11 +28,11 @@ public static function getLibrary(): string public static function getStubService(): string { - return self::$destinationDir . '/bin/pact-stub-server/pact-stub-server' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : ''); + return self::$destinationDir . '/bin/pact-stub-server/pact-stub-server'; } public static function getBroker(): string { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker' . (PHP_OS_FAMILY === 'Windows' ? '.bat' : ''); + return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker'; } } diff --git a/src/PhpPact/Standalone/Runner/ProcessRunner.php b/src/PhpPact/Standalone/Runner/ProcessRunner.php deleted file mode 100644 index 2da9eb15..00000000 --- a/src/PhpPact/Standalone/Runner/ProcessRunner.php +++ /dev/null @@ -1,193 +0,0 @@ - $arguments - */ - public function __construct(string $command, array $arguments) - { - $this->exitCode = -1; - $this->process = new Process($command . ' ' . \implode(' ', $arguments)); - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - public function getOutput(): string - { - return $this->output; - } - - public function setOutput(string $output): void - { - $this->output = $output; - } - - public function getExitCode(): int - { - return $this->exitCode; - } - - public function setExitCode(int $exitCode): void - { - $this->exitCode = $exitCode; - } - - public function getCommand(): string - { - return $this->process->getCommand(); - } - - public function getStderr(): string - { - return $this->stderr; - } - - public function setStderr(string $stderr): void - { - $this->stderr = $stderr; - } - - /** - * Run a blocking, synchronous process - */ - public function runBlocking(): int - { - $logger = $this->getLogger(); - $pid = null; - $lambdaLoop = function () use ($logger, &$pid) { - $logger->debug("Process command: {$this->process->getCommand()}"); - - $pid = yield $this->process->start(); - - $this->output .= yield ByteStream\buffer($this->process->getStdout()); - $this->stderr .= yield ByteStream\buffer($this->process->getStderr()); - - $exitCode = yield $this->process->join(); - $this->setExitCode($exitCode); - $logger->debug("Exit code: {$this->getExitCode()}"); - - if ($this->getExitCode() !== 0) { - $this->logger->info('out > ' . $this->getOutput()); - $this->logger->error('err > ' . $this->getStderr()); - throw new \Exception("PactPHP Process returned non-zero exit code: {$this->getExitCode()}", $this->getExitCode()); - } - - Loop::stop(); - }; - - Loop::run($lambdaLoop); - - return $pid; - } - - /** - * Run a blocking, synchronous process - */ - public function runNonBlocking(): int - { - $logger = $this->getLogger(); - - $pid = null; - - $lambdaLoop = function () use ($logger, &$pid) { - $logger->debug("start background command: {$this->process->getCommand()}"); - - $pid = yield $this->process->start(); - - $this->process->getStdout()->read()->onResolve(function (\Throwable $reason = null, $value) { - $this->output .= $value; - }); - $this->process->getStderr()->read()->onResolve(function (\Throwable $reason = null, $value) { - $this->output .= $value; - }); - - Loop::stop(); - }; - - Loop::run($lambdaLoop); - - $logger->debug("started process pid=$pid"); - - return $pid; - } - - /** - * Run the process and set output - * - * @return int Process Id - */ - public function run(bool $blocking = false): int - { - return $blocking - ? $this->runBlocking() - : $this->runNonBlocking(); - } - - /** - * Stop the running process - * - * @throws ProcessException - */ - public function stop(): bool - { - $pid = $this->process->getPid(); - - print "\nStopping Process Id: {$pid}\n"; - - if ('\\' === \DIRECTORY_SEPARATOR) { - \exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); - } - - $this->process->kill(); - - if ($this->process->isRunning()) { - throw new ProcessException(\sprintf('Error while killing process "%s".', $pid)); - } - - return true; - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('server'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index 4805770c..551e884a 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -2,10 +2,9 @@ namespace PhpPact\Standalone\StubService; -use Amp\Process\ProcessException; use Exception; use PhpPact\Standalone\Installer\Model\Scripts; -use PhpPact\Standalone\Runner\ProcessRunner; +use Symfony\Component\Process\Process; /** * Ruby Standalone Stub Server Wrapper @@ -14,7 +13,7 @@ class StubServer { private StubServerConfigInterface $config; - private ProcessRunner $processRunner; + private Process $process; public function __construct(StubServerConfigInterface $config) { @@ -24,33 +23,34 @@ public function __construct(StubServerConfigInterface $config) /** * Start the Stub Server. Verify that it is running. * - * @param int $wait seconds to delay for the server to come up - * * @throws Exception * - * @return int process ID of the started Stub Server + * @return int|null process ID of the started Stub Server if running, null otherwise */ - public function start(int $wait = 1): int + public function start(): ?int { - $this->processRunner = new ProcessRunner(Scripts::getStubService(), $this->getArguments()); + $this->process = new Process([Scripts::getStubService(), ...$this->getArguments()]); - $processId = $this->processRunner->run(); - \sleep($wait); // wait for server to start + $this->process->start(function (string $type, string $buffer) { + echo $buffer; + }); + $this->process->waitUntil(function (string $type, string $output) { + return false !== \strpos($output, 'Server started on port'); + }); - return $processId; + return $this->process->getPid(); } /** * Stop the Stub Server process. * - * @throws ProcessException - * * @return bool Was stopping successful? - * @throws ProcessException */ public function stop(): bool { - return $this->processRunner->stop(); + $this->process->stop(); + + return true; } /** diff --git a/tests/PhpPact/Standalone/Broker/BrokerTest.php b/tests/PhpPact/Standalone/Broker/BrokerTest.php index 13b67b38..bdbd7908 100644 --- a/tests/PhpPact/Standalone/Broker/BrokerTest.php +++ b/tests/PhpPact/Standalone/Broker/BrokerTest.php @@ -21,9 +21,14 @@ public function getArguments(): void ->setBrokerPassword('somepassword') ))->getArguments(); - $this->assertContains('--broker-token=someToken', $arguments); - $this->assertContains('--broker-username=someusername', $arguments); - $this->assertContains('--broker-password=somepassword', $arguments); + $this->assertSame([ + '--broker-token', + 'someToken', + '--broker-username', + 'someusername', + '--broker-password', + 'somepassword', + ], $arguments); } /** diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index c6cf11f2..ba3fb34c 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -4,13 +4,13 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\Runner\ProcessRunner; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; class VerifierTest extends TestCase { - /** @var ProcessRunner */ - private ProcessRunner $processRunner; + /** @var Process */ + private Process $process; /** * Run the PHP build-in web server. @@ -19,10 +19,10 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../../_public/'; - $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - $this->processRunner->run(); - \usleep(300000); // wait for server to start + $this->process->start(); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); } /** @@ -30,7 +30,7 @@ protected function setUp(): void */ protected function tearDown(): void { - $this->processRunner->stop(); + $this->process->stop(); } public function testVerify(): void diff --git a/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php b/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php deleted file mode 100644 index ff1b45f4..00000000 --- a/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php +++ /dev/null @@ -1,74 +0,0 @@ -runBlocking(); - $exitCode = $p->getExitCode(); - - $this->assertEquals($exitCode, 0, 'Expect the exit code to be 0'); - $this->assertStringContainsString($expectedOutput, $p->getOutput(), "Expect '{$expectedOutput}' to be in the output"); - $this->assertEquals(null, $p->getStderr(), 'Expect a null stderr'); - - // try an app that does not exists - if ('\\' !== \DIRECTORY_SEPARATOR) { - $p = new ProcessRunner('failedApp', []); - $expectedErr = 'failedApp'; - } else { - $p = new ProcessRunner('cmd /c echo myError 1>&2 && exit 42', []); - $expectedErr = 'myError'; - } - - try { - $p->runBlocking(); - } catch (\Exception $e) { - $exitCode = $p->getExitCode(); - $this->assertEquals($exitCode, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: $exitCode", $e->getMessage()); - $this->assertNotEquals($exitCode, 0, 'Expect the exit code to be non-zero: ' . $exitCode); - $this->assertStringContainsString($expectedErr, $p->getStderr(), "Expect '{$expectedErr}' to be in the stderr"); - $this->assertEquals(null, $p->getOutput(), 'Expect a null stdout'); - } - } - - /** - * @throws \Exception - */ - public function testProcessRunnerShouldReturnCompleteOutput() - { - if ('\\' !== \DIRECTORY_SEPARATOR) { - $cmd = __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.sh'; - } else { - $cmd = 'cmd /c' . __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.bat'; - } - - $p = new ProcessRunner($cmd, []); - $expectedOutput = 'third line'; - $expectedErr = 'fourth line'; - try { - $p->runBlocking(); - } catch (\Exception $e) { - $this->assertEquals(42, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: 42", $e->getMessage()); - } - $this->assertTrue((\stripos($p->getOutput(), $expectedOutput) !== false), "Expect '{$expectedOutput}' to be in the output:"); - $this->assertTrue((\stripos($p->getStderr(), $expectedErr) !== false), "Expect '{$expectedErr}' to be in the stderr"); - } -} diff --git a/tests/PhpPact/Standalone/Runner/verifier.bat b/tests/PhpPact/Standalone/Runner/verifier.bat deleted file mode 100755 index 3639d40f..00000000 --- a/tests/PhpPact/Standalone/Runner/verifier.bat +++ /dev/null @@ -1,11 +0,0 @@ -@ECHO OFF - -REM this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -ECHO "first line" -ECHO "second line" 1>&2 -ECHO "third line" -ECHO "fourth line" 1>&2 -ECHO "fifth line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/Runner/verifier.sh b/tests/PhpPact/Standalone/Runner/verifier.sh deleted file mode 100755 index 8290c308..00000000 --- a/tests/PhpPact/Standalone/Runner/verifier.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -# this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -echoerr() { echo "$@" 1>&2; } - -echo "first line" -echoerr "second line" -echo "third line" -echoerr "fourth line" -echo "fifth line" - -exit 42 \ No newline at end of file From e40184753fe4f7fecd39f93839af738ce588d749 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 13 Mar 2023 11:16:31 +0700 Subject: [PATCH 051/298] Use Rust FFI: Remove pact broker --- README.md | 37 +- UPGRADE-9.0.md | 13 + composer.json | 24 +- example/phpunit.all.xml | 13 - example/phpunit.consumer.xml | 14 - example/phpunit.core.xml | 13 - phpstan.neon | 6 - phpunit.xml | 3 - src/PhpPact/Config/PactConfig.php | 2 +- .../Consumer/Listener/PactTestListener.php | 101 ------ src/PhpPact/Standalone/Broker/Broker.php | 181 ---------- .../Standalone/Broker/BrokerConfig.php | 316 ------------------ .../Standalone/Installer/Model/Scripts.php | 5 - .../Standalone/Broker/BrokerConfigTest.php | 88 ----- .../PhpPact/Standalone/Broker/BrokerTest.php | 104 ------ 15 files changed, 50 insertions(+), 870 deletions(-) delete mode 100644 src/PhpPact/Consumer/Listener/PactTestListener.php delete mode 100644 src/PhpPact/Standalone/Broker/Broker.php delete mode 100644 src/PhpPact/Standalone/Broker/BrokerConfig.php delete mode 100644 tests/PhpPact/Standalone/Broker/BrokerConfigTest.php delete mode 100644 tests/PhpPact/Standalone/Broker/BrokerTest.php diff --git a/README.md b/README.md index f4c998c6..c081aee2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ Table of contents - [Specifications](#specifications) - [Installation](#installation) - [Basic Consumer Usage](#basic-consumer-usage) - - [Publish Contracts To Pact Broker](#publish-contracts-to-pact-broker) - [Create Consumer Unit Test](#create-consumer-unit-test) - [Create Mock Request](#create-mock-request) - [Create Mock Response](#create-mock-response) @@ -25,6 +24,10 @@ Table of contents - [Make the Request](#make-the-request) - [Verify Interactions](#verify-interactions) - [Make Assertions](#make-assertions) + - [Delete Old Pact](#delete-old-pact) + - [Publish Contracts To Pact Broker](#publish-contracts-to-pact-broker) + - [CLI](#cli) + - [Github Actions](#github-actions) - [Basic Provider Usage](#basic-provider-usage) - [Create Unit Test](#create-unit-test) - [Start API](#start-api) @@ -81,12 +84,6 @@ Composer hosts older versions under `mattersight/phppact`, which is abandoned. P All of the following code will be used exclusively for the Consumer. -### Publish Contracts To Pact Broker - -When all tests in test suite are passed, you may want to publish generated contract files to pact broker automatically. - -The easiest way to configure this is to use a [PHPUnit Listener](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.test-listeners). A default listener is included in this project, see [PactTestListener.php](/src/PhpPact/Consumer/Listener/PactTestListener.php). This utilizes environmental variables for configurations. These env variables can either be added to the system or to the phpunit.xml configuration file. Here is an example [phpunit.xml](/example/phpunit.consumer.xml) file configured to use the default. Keep in mind that both the test suite and the arguments array must be the same value. - ### Create Consumer Unit Test Create a standard PHPUnit test case class and function. @@ -203,6 +200,32 @@ Verify that the data you would expect given the response configured is correct. $this->assertEquals('Hello, Bob', $result); // Make your assertions. ``` +### Delete Old Pact + +If the value of `PACT_FILE_WRITE_MODE` is `merge`, before running the test, we need to delete the old pact manually: + +```shell +rm /path/to/pacts/consumer-provider.json +``` + +### Publish Contracts To Pact Broker + +When all tests in test suite are passed, you may want to publish generated contract files to pact broker. + +#### CLI + +Run this command using CLI tool: + +```shell +pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken +``` + +See more at https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts#publish-using-cli-tools + +#### Github Actions + +See how to use at https://github.com/pactflow/actions/tree/main/publish-pact-files + ## Basic Provider Usage All of the following code will be used exclusively for Providers. This will run the Pacts against the real Provider and either verify or fail validation on the Pact Broker. diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md index aca9d300..630b8282 100644 --- a/UPGRADE-9.0.md +++ b/UPGRADE-9.0.md @@ -50,3 +50,16 @@ UPGRADE FROM 8.x to 9.0 $this->assertTrue($verifyResult); ``` + +* Consumer + * Pact file write mode has been changed from 'overwrite' to 'merge'. Make sure old pact files are removed before running tests. + + ```shell + rm /path/to/pacts/*.json + ``` + + * Pact files now can ONLY be uploaded to Pact Broker by downloading and running Pact CLI manually. + + ```shell + pact-broker publish /path/to/pacts/*.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken + ``` diff --git a/composer.json b/composer.json index b1ce6078..65ad4cd1 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "composer/semver": "^1.4.0|^3.2.0", "symfony/process": "^4.4|^5.4|^6.0", "guzzlehttp/guzzle": "^6.5.8|^7.4.5", - "phpunit/phpunit": ">=8.5.23 <10", "tienvx/composer-downloads-plugin": "^1.2.0" }, "require-dev": { @@ -32,7 +31,8 @@ "slim/psr7": "^1.2.0", "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", - "phpstan/phpstan": "^1.9" + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": ">=8.5.23 <10" }, "autoload": { "psr-4": { @@ -64,25 +64,13 @@ "static-code-analysis": "phpstan", "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", - "test": "phpunit --debug -c example/phpunit.all.xml" + "test": [ + "php -r \"array_map('unlink', glob('./example/output/*.json'));\"", + "phpunit --debug -c example/phpunit.all.xml" + ] }, "extra": { "downloads": { - "pact-ruby-standalone": { - "version": "1.91.0", - "variables": { - "{$os}": "PHP_OS_FAMILY === 'Windows' ? 'win32' : (PHP_OS === 'Darwin' ? 'osx' : 'linux')", - "{$architecture}": "PHP_OS === 'Linux' ? '-x86_64' : ''", - "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'zip' : 'tar.gz'", - "{$keep}": "PHP_OS_FAMILY === 'Windows' ? 'pact-broker.bat' : 'pact-broker'" - }, - "url": "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{$version}/pact-{$version}-{$os}{$architecture}.{$extension}", - "path": "bin/pact-ruby-standalone", - "ignore": [ - "bin/*", - "!bin/{$keep}" - ] - }, "pact-ffi-headers": { "version": "0.4.4", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 3cc67cbf..91e84d44 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -14,23 +14,10 @@ ./tests/MessageConsumer - - - - - - PhpPact Consumer Example Tests - - - - - - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml index 0e521db3..8975f039 100644 --- a/example/phpunit.consumer.xml +++ b/example/phpunit.consumer.xml @@ -5,25 +5,11 @@ ./tests/Consumer - - - - - - PhpPact Example Tests - - - - - - - - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml index 57e6e2e4..49403bdb 100644 --- a/example/phpunit.core.xml +++ b/example/phpunit.core.xml @@ -5,23 +5,10 @@ ../tests - - - - - - PhpPact Consumer Example Tests - - - - - - - diff --git a/phpstan.neon b/phpstan.neon index 02aff4b2..1d5823a9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,9 +2,3 @@ parameters: level: 7 paths: - src - ignoreErrors: - - - messages: - - '#Method PhpPact\\Standalone\\Broker\\Broker::[a-zA-Z0-9\\_]+\(\) return type has no value type specified in iterable type array\.#' - - '#Method PhpPact\\Standalone\\Broker\\Broker::[a-zA-Z0-9\\_]+\(\) has parameter \$options with no value type specified in iterable type array\.#' - path: src/PhpPact/Standalone/Broker/Broker.php diff --git a/phpunit.xml b/phpunit.xml index af9ebf68..2c3b6972 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,10 +21,7 @@ - - - diff --git a/src/PhpPact/Config/PactConfig.php b/src/PhpPact/Config/PactConfig.php index 833f91fb..08c53897 100644 --- a/src/PhpPact/Config/PactConfig.php +++ b/src/PhpPact/Config/PactConfig.php @@ -39,7 +39,7 @@ class PactConfig implements PactConfigInterface * pact file is deleted before running tests when using this option so that * interactions deleted from the code are not maintained in the file. */ - private string $pactFileWriteMode = self::MODE_OVERWRITE; + private string $pactFileWriteMode = self::MODE_MERGE; /** * {@inheritdoc} diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php deleted file mode 100644 index f4a19c6b..00000000 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ /dev/null @@ -1,101 +0,0 @@ - - */ - private array $testSuiteNames = []; - - private MockServerEnvConfig $mockServerConfig; - - private bool $failed = false; - - /** - * @param array $testSuiteNames test suite names that need evaluated with the listener - * - * @throws MissingEnvVariableException - */ - public function __construct(array $testSuiteNames) - { - $this->testSuiteNames = $testSuiteNames; - $this->mockServerConfig = new MockServerEnvConfig(); - } - - public function addError(Test $test, Throwable $t, float $time): void - { - $this->failed = true; - } - - public function addFailure(Test $test, AssertionFailedError $e, float $time): void - { - $this->failed = true; - } - - /** - * Publish JSON results to PACT Broker and stop the Mock Server. - */ - public function endTestSuite(TestSuite $suite): void - { - if (in_array($suite->getName(), $this->testSuiteNames)) { - if ($this->failed === true) { - print 'A unit test has failed. Skipping PACT file upload.'; - } elseif (!($pactBrokerUri = getenv('PACT_BROKER_URI'))) { - print 'PACT_BROKER_URI environment variable was not set. Skipping PACT file upload.'; - } elseif (!($consumerVersion = getenv('PACT_CONSUMER_VERSION'))) { - print 'PACT_CONSUMER_VERSION environment variable was not set. Skipping PACT file upload.'; - } elseif (!($tag = getenv('PACT_CONSUMER_TAG'))) { - print 'PACT_CONSUMER_TAG environment variable was not set. Skipping PACT file upload.'; - } else { - $brokerConfig = new BrokerConfig(); - $brokerConfig->setPacticipant($this->mockServerConfig->getConsumer()); - $brokerConfig->setPactLocations($this->mockServerConfig->getPactDir()); - $brokerConfig->setBrokerUri(new Uri($pactBrokerUri)); - $brokerConfig->setConsumerVersion($consumerVersion); - $brokerConfig->setVersion($consumerVersion); - $brokerConfig->setTag($tag); - if (($user = getenv('PACT_BROKER_HTTP_AUTH_USER')) && - ($pass = getenv('PACT_BROKER_HTTP_AUTH_PASS')) - ) { - $brokerConfig->setBrokerUsername($user); - $brokerConfig->setBrokerPassword($pass); - } - - if ($bearerToken = getenv('PACT_BROKER_BEARER_TOKEN')) { - $brokerConfig->setBrokerToken($bearerToken); - } - - $broker = new Broker($brokerConfig); - $broker->createVersionTag(); - $broker->publish(); - print 'Pact file has been uploaded to the Broker successfully.'; - } - } - } -} diff --git a/src/PhpPact/Standalone/Broker/Broker.php b/src/PhpPact/Standalone/Broker/Broker.php deleted file mode 100644 index 529bf80d..00000000 --- a/src/PhpPact/Standalone/Broker/Broker.php +++ /dev/null @@ -1,181 +0,0 @@ -config = $config; - $this->command = Scripts::getBroker(); - } - - public function canIDeploy(): array - { - return $this->runThenDecodeJson([ - 'can-i-deploy', - '--pacticipant', $this->config->getPacticipant(), - '--version', $this->config->getVersion(), - ]); - } - - /** - * @return array parameters to be passed into the process - */ - public function getArguments(): array - { - $parameters = []; - - if ($this->config->getBrokerUri() !== null) { - $parameters[] = '--broker-base-url'; - $parameters[] = $this->config->getBrokerUri(); - } - - if ($this->config->getBrokerToken() !== null) { - $parameters[] = '--broker-token'; - $parameters[] = $this->config->getBrokerToken(); - } - - if ($this->config->getBrokerUsername() !== null) { - $parameters[] = '--broker-username'; - $parameters[] = $this->config->getBrokerUsername(); - } - - if ($this->config->getBrokerPassword() !== null) { - $parameters[] = '--broker-password'; - $parameters[] = $this->config->getBrokerPassword(); - } - - return $parameters; - } - - public function createOrUpdatePacticipant(): array - { - return $this->runThenDecodeJson([ - 'create-or-update-pacticipant', - '--name', $this->config->getName(), - '--repository-url', $this->config->getRepositoryUrl(), - ]); - } - - public function createOrUpdateWebhook(): array - { - return $this->runThenDecodeJson([ - 'create-or-update-webhook', - $this->config->getUrl(), - '--request', $this->config->getRequest(), - '--header', $this->config->getHeader(), - '--data', $this->config->getData(), - '--user', $this->config->getUser(), - '--consumer', $this->config->getConsumer(), - '--provider', $this->config->getProvider(), - '--description', $this->config->getDescription(), - '--uuid', $this->config->getUuid(), - ]); - } - - public function createVersionTag(): array - { - return $this->runThenDecodeJson([ - 'create-version-tag', - '--pacticipant', $this->config->getPacticipant(), - '--version', $this->config->getVersion(), - '--tag', $this->config->getTag(), - ]); - } - - public function createWebhook(): array - { - return $this->runThenDecodeJson([ - 'create-webhook', - $this->config->getUrl(), - '--request', $this->config->getRequest(), - '--header', $this->config->getHeader(), - '--data', $this->config->getData(), - '--user', $this->config->getUser(), - '--consumer', $this->config->getConsumer(), - '--provider', $this->config->getProvider(), - '--description', $this->config->getDescription(), - ]); - } - - public function describeVersion(): array - { - return $this->runThenDecodeJson([ - 'describe-version', - '--pacticipant', $this->config->getPacticipant(), - '--output', 'json', - ]); - } - - public function listLatestPactVersions(): array - { - return $this->runThenDecodeJson([ - 'list-latest-pact-versions', - '--output', 'json', - ]); - } - - public function publish(): void - { - $options = [ - 'publish', - $this->config->getPactLocations(), - '--consumer-app-version', $this->config->getConsumerVersion(), - ]; - - if (null !== $this->config->getBranch()) { - $options[] = '--branch'; - $options[] = $this->config->getBranch(); - } - - if (null !== $this->config->getTag()) { - $options[] = '--tag'; - $options[] = $this->config->getTag(); - } - - $this->run($options); - } - - public function testWebhook(): array - { - return $this->runThenDecodeJson([ - 'test-webhook', - '--uuid', $this->config->getUuid(), - ]); - } - - public function generateUuid(): string - { - return \rtrim($this->run(['generate-uuid'])); - } - - private function run(array $options): string - { - $process = new Process([ - $this->command, - ...$options, - ...$this->getArguments(), - ]); - $process->run(); - - if (!$process->isSuccessful()) { - throw new Exception("PactPHP Process returned non-zero exit code: {$process->getExitCode()}", $process->getExitCode()); - } - - return $process->getOutput(); - } - - private function runThenDecodeJson(array $options): array - { - return \json_decode($this->run($options), true, 512, JSON_THROW_ON_ERROR); - } -} diff --git a/src/PhpPact/Standalone/Broker/BrokerConfig.php b/src/PhpPact/Standalone/Broker/BrokerConfig.php deleted file mode 100644 index 48eac520..00000000 --- a/src/PhpPact/Standalone/Broker/BrokerConfig.php +++ /dev/null @@ -1,316 +0,0 @@ -repositoryUrl; - } - - public function setRepositoryUrl(?string $repositoryUrl): self - { - $this->repositoryUrl = $repositoryUrl; - - return $this; - } - - public function getUrl(): ?string - { - return $this->url; - } - - public function setUrl(?string $url): self - { - $this->url = $url; - - return $this; - } - - public function getVersion(): ?string - { - return $this->version; - } - - public function setVersion(?string $version): self - { - $this->version = $version; - - return $this; - } - - public function getBranch(): ?string - { - return $this->branch; - } - - public function setBranch(?string $branch): self - { - $this->branch = $branch; - - return $this; - } - - public function getTag(): ?string - { - return $this->tag; - } - - public function setTag(?string $tag): self - { - $this->tag = $tag; - - return $this; - } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): self - { - $this->name = $name; - - return $this; - } - - public function getRequest(): ?string - { - return $this->request; - } - - public function setRequest(?string $request): self - { - $this->request = $request; - - return $this; - } - - public function getHeader(): ?string - { - return $this->header; - } - - public function setHeader(?string $header): self - { - $this->header = $header; - - return $this; - } - - public function getData(): ?string - { - return $this->data; - } - - public function setData(?string $data): self - { - $this->data = $data; - - return $this; - } - - public function getUser(): ?string - { - return $this->user; - } - - public function setUser(?string $user): self - { - $this->user = $user; - - return $this; - } - - public function getConsumer(): ?string - { - return $this->consumer; - } - - public function setConsumer(?string $consumer): self - { - $this->consumer = $consumer; - - return $this; - } - - public function getProvider(): ?string - { - return $this->provider; - } - - public function setProvider(?string $provider): self - { - $this->provider = $provider; - - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): self - { - $this->description = $description; - - return $this; - } - - public function getUuid(): ?string - { - return $this->uuid; - } - - public function setUuid(?string $uuid): self - { - $this->uuid = $uuid; - - return $this; - } - - public function isVerbose(): bool - { - return $this->verbose; - } - - public function setVerbose(bool $verbose): self - { - $this->verbose = $verbose; - - return $this; - } - - public function getBrokerUri(): ?UriInterface - { - return $this->brokerUri; - } - - public function setBrokerUri(?UriInterface $brokerUri): self - { - $this->brokerUri = $brokerUri; - - return $this; - } - - public function getBrokerToken(): ?string - { - return $this->brokerToken; - } - - public function setBrokerToken(?string $brokerToken): self - { - $this->brokerToken = $brokerToken; - - return $this; - } - - public function getBrokerUsername(): ?string - { - return $this->brokerUsername; - } - - public function setBrokerUsername(?string $brokerUsername): self - { - $this->brokerUsername = $brokerUsername; - - return $this; - } - - public function getBrokerPassword(): ?string - { - return $this->brokerPassword; - } - - public function setBrokerPassword(?string $brokerPassword): self - { - $this->brokerPassword = $brokerPassword; - - return $this; - } - - public function getPacticipant(): string - { - return $this->pacticipant; - } - - public function setPacticipant(?string $pacticipant): self - { - $this->pacticipant = $pacticipant; - - return $this; - } - - public function getConsumerVersion(): ?string - { - return $this->consumerVersion; - } - - public function setConsumerVersion(?string $consumerVersion): self - { - $this->consumerVersion = $consumerVersion; - - return $this; - } - - public function getPactLocations(): ?string - { - return $this->pactLocations; - } - - public function setPactLocations(string $locations): self - { - $this->pactLocations = $locations; - - return $this; - } -} diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index 3c7747c1..ac33f2a7 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -30,9 +30,4 @@ public static function getStubService(): string { return self::$destinationDir . '/bin/pact-stub-server/pact-stub-server'; } - - public static function getBroker(): string - { - return self::$destinationDir . '/bin/pact-ruby-standalone/bin/pact-broker'; - } } diff --git a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php b/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php deleted file mode 100644 index 2e799e12..00000000 --- a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php +++ /dev/null @@ -1,88 +0,0 @@ -setBrokerUri($brokerUri) - ->setBrokerToken($brokerToken) - ->setBrokerUsername($brokerUsername) - ->setBrokerPassword($brokerPassword) - ->setVerbose($verbose) - ->setPacticipant($pacticipant) - ->setRequest($request) - ->setHeader($header) - ->setData($data) - ->setUser($user) - ->setUrl($url) - ->setConsumer($consumer) - ->setProvider($provider) - ->setDescription($description) - ->setUuid($uuid) - ->setVersion($version) - ->setBranch($branch) - ->setTag($tag) - ->setName($name) - ->setRepositoryUrl($repositoryUrl) - ->setConsumerVersion($consumerVersion) - ->setPactLocations($pactLocations); - - static::assertSame($brokerUri, $subject->getBrokerUri()); - static::assertSame($brokerToken, $subject->getBrokerToken()); - static::assertSame($brokerUsername, $subject->getBrokerUsername()); - static::assertSame($brokerPassword, $subject->getBrokerPassword()); - static::assertSame($verbose, $subject->isVerbose()); - static::assertSame($pacticipant, $subject->getPacticipant()); - static::assertSame($request, $subject->getRequest()); - static::assertSame($header, $subject->getHeader()); - static::assertSame($data, $subject->getData()); - static::assertSame($user, $subject->getUser()); - static::assertSame($url, $subject->getUrl()); - static::assertSame($consumer, $subject->getConsumer()); - static::assertSame($provider, $subject->getProvider()); - static::assertSame($description, $subject->getDescription()); - static::assertSame($uuid, $subject->getUuid()); - static::assertSame($version, $subject->getVersion()); - static::assertSame($branch, $subject->getBranch()); - static::assertSame($tag, $subject->getTag()); - static::assertSame($name, $subject->getName()); - static::assertSame($repositoryUrl, $subject->getRepositoryUrl()); - static::assertSame($consumerVersion, $subject->getConsumerVersion()); - static::assertSame($pactLocations, $subject->getPactLocations()); - } -} diff --git a/tests/PhpPact/Standalone/Broker/BrokerTest.php b/tests/PhpPact/Standalone/Broker/BrokerTest.php deleted file mode 100644 index bdbd7908..00000000 --- a/tests/PhpPact/Standalone/Broker/BrokerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -setBrokerToken('someToken') - ->setBrokerUsername('someusername') - ->setBrokerPassword('somepassword') - ))->getArguments(); - - $this->assertSame([ - '--broker-token', - 'someToken', - '--broker-username', - 'someusername', - '--broker-password', - 'somepassword', - ], $arguments); - } - - /** - * @test - */ - public function getArgumentsEmptyConfig(): void - { - $this->assertEmpty((new Broker(new BrokerConfig()))->getArguments()); - } - - /** - * @test - */ - //public function generateUuid(): void - //{ - // $this->assertContains('-', (new Broker(new BrokerConfig()))->generateUuid()); - //} - - /** - * @test - * - * @throws \Exception - */ - public function describeVersion(): void - { - $config = new BrokerConfig(); - $config->setPacticipant('Animal Profile Service') - ->setBrokerUri(new Uri('https://test.pactflow.io')) - ->setBrokerUsername('dXfltyFMgNOFZAxr8io9wJ37iUpY42M') - ->setBrokerPassword('O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1'); - $broker = new Broker($config); - - $result = $broker->describeVersion(); - - $this->assertArrayHasKey('number', $result); - } - - /** - * @test - * - * @throws \Exception - */ - public function listLatestPactVersions(): void - { - $config = new BrokerConfig(); - $config->setPacticipant("\"Animal Profile Service\"") - ->setBrokerUri(new Uri('https://test.pactflow.io')) - ->setBrokerUsername('dXfltyFMgNOFZAxr8io9wJ37iUpY42M') - ->setBrokerPassword('O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1'); - $broker = new Broker($config); - - $result = $broker->listLatestPactVersions(); - $this->assertArrayHasKey('pacts', $result); - } - - /** - * @test - * - * @throws \Exception - */ - public function publishLogsStdError(): void - { - $config = new BrokerConfig(); - $config->setPactLocations('not a directory'); - $broker = new Broker($config); - try { - $broker->publish(); - } catch(\Exception $e) { - $this->assertEquals(1, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: 1", $e->getMessage()); - } - } -} From e799d37061f1ef460c4358a2278ca02a4891c235 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 15 Mar 2023 23:58:03 +0700 Subject: [PATCH 052/298] Use Rust FFI: Allow set content type --- .../Consumer/AbstractMessageBuilder.php | 7 + .../Consumer/Model/ConsumerRequest.php | 161 ++---------------- .../Consumer/Model/Interaction/BodyTrait.php | 36 ++++ .../Model/Interaction/ContentTypeTrait.php | 20 +++ .../Model/Interaction/HeadersTrait.php | 52 ++++++ .../Model/Interaction/MethodTrait.php | 20 +++ .../Consumer/Model/Interaction/PathTrait.php | 27 +++ .../Consumer/Model/Interaction/QueryTrait.php | 52 ++++++ .../Model/Interaction/StatusTrait.php | 20 +++ src/PhpPact/Consumer/Model/Message.php | 20 ++- .../Consumer/Model/ProviderResponse.php | 92 +--------- .../Interaction/InteractionRegistry.php | 4 +- .../Registry/Interaction/MessageRegistry.php | 10 +- 13 files changed, 274 insertions(+), 247 deletions(-) create mode 100644 src/PhpPact/Consumer/Model/Interaction/BodyTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/MethodTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/PathTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/QueryTrait.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/StatusTrait.php diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index 71a23275..d25a743c 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -56,4 +56,11 @@ public function withContent(mixed $contents): self return $this; } + + public function withContentType(?string $contentType): self + { + $this->message->setContentType($contentType); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 4632dee1..58d98740 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -2,159 +2,22 @@ namespace PhpPact\Consumer\Model; -use JsonException; +use PhpPact\Consumer\Model\Interaction\BodyTrait; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Interaction\HeadersTrait; +use PhpPact\Consumer\Model\Interaction\MethodTrait; +use PhpPact\Consumer\Model\Interaction\PathTrait; +use PhpPact\Consumer\Model\Interaction\QueryTrait; /** * Request initiated by the consumer. */ class ConsumerRequest { - private string $method; - - private string $path; - - /** - * @var array - */ - private array $headers = []; - - private ?string $body = null; - - /** - * @var array - */ - private array $query = []; - - public function getMethod(): string - { - return $this->method; - } - - public function setMethod(string $method): self - { - $this->method = $method; - - return $this; - } - - public function getPath(): string - { - return $this->path; - } - - /** - * @param string|array $path - * - * @throws JsonException - */ - public function setPath(string|array $path): self - { - $this->path = is_array($path) ? json_encode($path, JSON_THROW_ON_ERROR) : $path; - - return $this; - } - - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * @param array $headers - */ - public function setHeaders(array $headers): self - { - $this->headers = []; - foreach ($headers as $header => $value) { - $this->addHeader($header, $value); - } - - return $this; - } - - /** - * @param string|string[] $value - */ - public function addHeader(string $header, array|string $value): self - { - $this->headers[$header] = []; - if (is_array($value)) { - array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); - } else { - $this->addHeaderValue($header, $value); - } - - return $this; - } - - private function addHeaderValue(string $header, string $value): void - { - $this->headers[$header][] = $value; - } - - public function getBody(): ?string - { - return $this->body; - } - - /** - * @param array|string|null $body - * - * @throws JsonException - */ - public function setBody(array|string|null $body): self - { - if (\is_string($body) || \is_null($body)) { - $this->body = $body; - } else { - $this->body = \json_encode($body, JSON_THROW_ON_ERROR); - $this->addHeader('Content-Type', 'application/json'); - } - - return $this; - } - - /** - * @return array - */ - public function getQuery(): array - { - return $this->query; - } - - /** - * @param array $query - */ - public function setQuery(array $query): self - { - $this->query = []; - foreach ($query as $key => $value) { - $this->addQueryParameter($key, $value); - } - - return $this; - } - - /** - * @param string|string[] $value - */ - public function addQueryParameter(string $key, array|string $value): self - { - $this->query[$key] = []; - if (is_array($value)) { - array_walk($value, fn (string $value) => $this->addQueryParameterValue($key, $value)); - } else { - $this->addQueryParameterValue($key, $value); - } - - return $this; - } - - private function addQueryParameterValue(string $key, string $value): void - { - $this->query[$key][] = $value; - } + use HeadersTrait; + use BodyTrait; + use ContentTypeTrait; + use MethodTrait; + use PathTrait; + use QueryTrait; } diff --git a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php new file mode 100644 index 00000000..3a7c6b37 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php @@ -0,0 +1,36 @@ +body; + } + + /** + * @param array|string|null $body + * + * @throws JsonException + */ + public function setBody(array|string|null $body): self + { + if (\is_string($body) || \is_null($body)) { + $this->body = $body; + } else { + $this->body = \json_encode($body, JSON_THROW_ON_ERROR); + if (!isset($this->contentType)) { + $this->setContentType('application/json'); + } + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php new file mode 100644 index 00000000..257f6cbe --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php @@ -0,0 +1,20 @@ +contentType; + } + + public function setContentType(?string $contentType): self + { + $this->contentType = $contentType; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php new file mode 100644 index 00000000..ebfa0486 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php @@ -0,0 +1,52 @@ + + */ + private array $headers = []; + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): self + { + $this->headers = []; + foreach ($headers as $header => $value) { + $this->addHeader($header, $value); + } + + return $this; + } + + /** + * @param string[]|string $value + */ + public function addHeader(string $header, array|string $value): self + { + $this->headers[$header] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); + } else { + $this->addHeaderValue($header, $value); + } + + return $this; + } + + private function addHeaderValue(string $header, string $value): void + { + $this->headers[$header][] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php b/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php new file mode 100644 index 00000000..84ab36cc --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php @@ -0,0 +1,20 @@ +method; + } + + public function setMethod(string $method): self + { + $this->method = $method; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/PathTrait.php b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php new file mode 100644 index 00000000..9eceebe2 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php @@ -0,0 +1,27 @@ +path; + } + + /** + * @param string|array $path + * + * @throws JsonException + */ + public function setPath(array|string $path): self + { + $this->path = is_array($path) ? json_encode($path, JSON_THROW_ON_ERROR) : $path; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php new file mode 100644 index 00000000..5d907857 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php @@ -0,0 +1,52 @@ + + */ + private array $query = []; + + /** + * @return array + */ + public function getQuery(): array + { + return $this->query; + } + + /** + * @param array $query + */ + public function setQuery(array $query): self + { + $this->query = []; + foreach ($query as $key => $value) { + $this->addQueryParameter($key, $value); + } + + return $this; + } + + /** + * @param string|string[] $value + */ + public function addQueryParameter(string $key, array|string $value): self + { + $this->query[$key] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addQueryParameterValue($key, $value)); + } else { + $this->addQueryParameterValue($key, $value); + } + + return $this; + } + + private function addQueryParameterValue(string $key, string $value): void + { + $this->query[$key][] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php new file mode 100644 index 00000000..31e68f4b --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php @@ -0,0 +1,20 @@ +status; + } + + public function setStatus(int $status): self + { + $this->status = $status; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 1b8b3a4e..796a683f 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -2,12 +2,16 @@ namespace PhpPact\Consumer\Model; +use JsonException; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; + /** * Message metadata and contents to be posted to the Mock Server for PACT tests. */ class Message { use ProviderStates; + use ContentTypeTrait; private string $description; @@ -16,7 +20,7 @@ class Message */ private array $metadata = []; - private mixed $contents; + private ?string $contents = null; public function getDescription(): string { @@ -56,14 +60,24 @@ private function setMetadataValue(string $key, string $value): void $this->metadata[$key] = $value; } - public function getContents(): mixed + public function getContents(): ?string { return $this->contents; } + /** + * @throws JsonException + */ public function setContents(mixed $contents): self { - $this->contents = $contents; + if (\is_string($contents) || \is_null($contents)) { + $this->contents = $contents; + } else { + $this->contents = \json_encode($contents, JSON_THROW_ON_ERROR); + if (!isset($this->contentType)) { + $this->setContentType('application/json'); + } + } return $this; } diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 4ec6ba51..3201811f 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -2,94 +2,18 @@ namespace PhpPact\Consumer\Model; -use JsonException; +use PhpPact\Consumer\Model\Interaction\BodyTrait; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Interaction\HeadersTrait; +use PhpPact\Consumer\Model\Interaction\StatusTrait; /** * Response expectation that would be in response to a Consumer request from the Provider. */ class ProviderResponse { - private int $status; - - /** - * @var array - */ - private array $headers = []; - - private ?string $body = null; - - public function getStatus(): int - { - return $this->status; - } - - public function setStatus(int $status): self - { - $this->status = $status; - - return $this; - } - - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * @param array $headers - */ - public function setHeaders(array $headers): self - { - $this->headers = []; - foreach ($headers as $header => $value) { - $this->addHeader($header, $value); - } - - return $this; - } - - /** - * @param string[]|string $value - */ - public function addHeader(string $header, array|string $value): self - { - $this->headers[$header] = []; - if (is_array($value)) { - array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); - } else { - $this->addHeaderValue($header, $value); - } - - return $this; - } - - private function addHeaderValue(string $header, string $value): void - { - $this->headers[$header][] = $value; - } - - public function getBody(): ?string - { - return $this->body; - } - - /** - * @param array|string|null $body - * - * @throws JsonException - */ - public function setBody(array|string|null $body): self - { - if (\is_string($body) || \is_null($body)) { - $this->body = $body; - } else { - $this->body = \json_encode($body, JSON_THROW_ON_ERROR); - $this->addHeader('Content-Type', 'application/json'); - } - - return $this; - } + use HeadersTrait; + use BodyTrait; + use ContentTypeTrait; + use StatusTrait; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php index 313d8c90..7a917416 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php @@ -76,7 +76,7 @@ private function with(ConsumerRequest $request): self ->withRequest($request->getMethod(), $request->getPath()) ->withQueryParameters($request->getQuery()) ->withHeaders($request->getHeaders()) - ->withBody(null, $request->getBody()); + ->withBody($request->getContentType(), $request->getBody()); return $this; } @@ -86,7 +86,7 @@ private function willRespondWith(ProviderResponse $response): self $this->responseRegistry ->withResponse($response->getStatus()) ->withHeaders($response->getHeaders()) - ->withBody(null, $response->getBody()); + ->withBody($response->getContentType(), $response->getBody()); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index 11f70f91..2da4e9b5 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -25,20 +25,12 @@ public function __construct( public function registerMessage(Message $message): void { - if (\is_string($message->getContents())) { - $contents = $message->getContents(); - $contentType = 'text/plain'; - } else { - $contents = \json_encode($message->getContents(), JSON_THROW_ON_ERROR); - $contentType = 'application/json'; - } - $this ->newInteraction($message->getDescription()) ->given($message->getProviderStates()) ->expectsToReceive($message->getDescription()) ->withMetadata($message->getMetadata()) - ->withContents($contentType, $contents); + ->withContents($message->getContentType(), $message->getContents()); } protected function newInteraction(string $description): self From 6f53e7a73edb1dfc83f76ca2ca16897fcb2e7883 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 2 Mar 2023 11:52:51 +0700 Subject: [PATCH 053/298] Default port zero --- src/PhpPact/Standalone/MockService/MockServerConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPact/Standalone/MockService/MockServerConfig.php b/src/PhpPact/Standalone/MockService/MockServerConfig.php index 39bbe743..fb062e6f 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfig.php @@ -19,7 +19,7 @@ class MockServerConfig extends PactConfig implements MockServerConfigInterface /** * Port on which to run the service. A value of zero will result in the operating system allocating an available port. */ - private int $port = 7200; + private int $port = 0; /** * @var bool From cbe6de11ad1624aafbd18b0c6edbfff51313ab7b Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 13 Mar 2023 17:12:42 +0700 Subject: [PATCH 054/298] Make mock server host and port optional --- example/phpunit.all.xml | 2 -- example/phpunit.consumer.xml | 2 -- example/phpunit.core.xml | 2 -- phpunit.xml | 2 -- .../Standalone/MockService/MockServerEnvConfig.php | 10 ++++++++-- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 02a047d5..e36de9e7 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -29,8 +29,6 @@ - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml index 0e521db3..7e45945e 100644 --- a/example/phpunit.consumer.xml +++ b/example/phpunit.consumer.xml @@ -17,8 +17,6 @@ - - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml index 57e6e2e4..e33f650e 100644 --- a/example/phpunit.core.xml +++ b/example/phpunit.core.xml @@ -17,8 +17,6 @@ - - diff --git a/phpunit.xml b/phpunit.xml index af9ebf68..a2b66f81 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,8 +18,6 @@ - - diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index 7e90a93d..a8183b2a 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -14,8 +14,14 @@ class MockServerEnvConfig extends MockServerConfig */ public function __construct() { - $this->setHost($this->parseEnv('PACT_MOCK_SERVER_HOST')); - $this->setPort((int) $this->parseEnv('PACT_MOCK_SERVER_PORT')); + if ($host = $this->parseEnv('PACT_MOCK_SERVER_HOST', false)) { + $this->setHost($host); + } + + if ($port = $this->parseEnv('PACT_MOCK_SERVER_PORT', false)) { + $this->setPort((int) $port); + } + $this->setConsumer($this->parseEnv('PACT_CONSUMER_NAME')); $this->setProvider($this->parseEnv('PACT_PROVIDER_NAME')); $this->setPactDir($this->parseEnv('PACT_OUTPUT_DIR', false)); From b373a320a587244e9b8ff9294b53d993a25670d2 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 7 May 2023 14:33:39 +0700 Subject: [PATCH 055/298] Add generators and matchers --- src/PhpPact/Consumer/Matcher/HttpStatus.php | 30 + src/PhpPact/Consumer/Matcher/Matcher.php | 407 ++++++++++++- .../PhpPact/Consumer/Matcher/MatcherTest.php | 538 +++++++++++++++++- 3 files changed, 945 insertions(+), 30 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/HttpStatus.php diff --git a/src/PhpPact/Consumer/Matcher/HttpStatus.php b/src/PhpPact/Consumer/Matcher/HttpStatus.php new file mode 100644 index 00000000..527bd13e --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/HttpStatus.php @@ -0,0 +1,30 @@ + + */ + public static function all(): array + { + return [ + self::INFORMATION, + self::SUCCESS, + self::REDIRECT, + self::CLIENT_ERROR, + self::SERVER_ERROR, + self::NON_ERROR, + self::ERROR, + ]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index c3a41538..a8da98f2 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -58,32 +58,80 @@ public function like(mixed $value): array /** * Expect an array of similar data as the value passed in. * + * @return array + */ + public function eachLike(mixed $value): array + { + return $this->atLeastLike($value, 1); + } + + /** + * @param mixed $value example of what the expected data would be + * @param int $min minimum number of objects to verify against + * + * @return array + */ + public function atLeastLike(mixed $value, int $min): array + { + return [ + 'value' => array_fill(0, $min, $value), + 'pact:matcher:type' => 'type', + 'min' => $min, + ]; + } + + /** + * @return array + */ + public function atMostLike(mixed $value, int $max): array + { + return [ + 'value' => [$value], + 'pact:matcher:type' => 'type', + 'max' => $max, + ]; + } + + /** * @param mixed $value example of what the expected data would be * @param int $min minimum number of objects to verify against * * @return array */ - public function eachLike(mixed $value, int $min = 1): array + public function atLeastAndMostLike(mixed $value, int $min, int $max): array { + if ($min <= 0 || $min > $max) { + throw new Exception('Invalid minimum number of elements'); + } + return [ 'value' => array_fill(0, $min, $value), 'pact:matcher:type' => 'type', 'min' => $min, + 'max' => $max, ]; } /** * Validate that a value will match a regex pattern. * - * @param mixed $value example of what the expected data would be + * @param string|null $value example of what the expected data would be * @param string $pattern valid Ruby regex pattern * * @return array * * @throws Exception */ - public function term(mixed $value, string $pattern): array + public function term(?string $value, string $pattern): array { + if (null === $value) { + return [ + 'regex' => $pattern, + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + } + $result = preg_match("/$pattern/", $value); if ($result === false || $result === 0) { @@ -106,7 +154,7 @@ public function term(mixed $value, string $pattern): array * * @throws Exception */ - public function regex(mixed $value, string $pattern): array + public function regex(?string $value, string $pattern): array { return $this->term($value, $pattern); } @@ -211,14 +259,74 @@ public function decimal(float $float = 13.01): array return $this->like($float); } + /** + * @return array + */ + public function booleanV3(?bool $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomBoolean', + 'pact:matcher:type' => 'boolean', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'boolean', + ]; + } + + /** + * @return array + */ + public function integerV3(?int $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'integer', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'integer', + ]; + } + + /** + * @return array + */ + public function decimalV3(?float $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomDecimal', + 'pact:matcher:type' => 'decimal', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'decimal', + ]; + } + /** * @return array * * @throws Exception */ - public function hexadecimal(string $hex = '3F'): array + public function hexadecimal(?string $value = null): array { - return $this->term($hex, self::HEX_FORMAT); + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomHexadecimal', + ] + $this->term(null, self::HEX_FORMAT); + } + + return $this->term($value, self::HEX_FORMAT); } /** @@ -226,9 +334,15 @@ public function hexadecimal(string $hex = '3F'): array * * @throws Exception */ - public function uuid(string $uuid = 'ce118b6e-d8e1-11e7-9296-cec278b6b50a'): array + public function uuid(?string $value = null): array { - return $this->term($uuid, self::UUID_V4_FORMAT); + if (null === $value) { + return [ + 'pact:generator:type' => 'Uuid', + ] + $this->term(null, self::UUID_V4_FORMAT); + } + + return $this->term($value, self::UUID_V4_FORMAT); } /** @@ -260,4 +374,281 @@ public function email(string $email = 'hello@pact.io'): array { return $this->term($email, self::EMAIL_FORMAT); } + + /** + * @return array + * + * @throws Exception + */ + public function ipv4AddressV3(?string $ip = null): array + { + if (null === $ip) { + return $this->term(null, self::IPV4_FORMAT); + } + + return $this->ipv4Address($ip); + } + + /** + * @return array + * + * @throws Exception + */ + public function ipv6AddressV3(?string $ip = null): array + { + if (null === $ip) { + return $this->term(null, self::IPV6_FORMAT); + } + + return $this->ipv6Address($ip); + } + + /** + * @return array + * + * @throws Exception + */ + public function emailV3(?string $email = null): array + { + if (null === $email) { + return $this->term(null, self::EMAIL_FORMAT); + } + + return $this->email($email); + } + + /** + * Value that must be null. This will only match the JSON Null value. For other content types, it will + * match if the attribute is missing. + * + * @return array + */ + public function nullValue(): array + { + return [ + 'pact:matcher:type' => 'null', + ]; + } + + /** + * @return array + */ + public function date(string $format = 'yyyy-MM-dd', ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'Date', + 'pact:matcher:type' => 'date', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'date', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function time(string $format = 'HH:mm::ss', ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'time', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function datetime(string $format = "YYYY-mm-DD'T'HH:mm:ss", ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'datetime', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function string(?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomString', + ] + $this->like('some string'); // No matcher for string? + } + + return $this->like($value); // No matcher for string? + } + + /** + * @param array $macher + * + * @return array + */ + public function fromProviderState(array $macher, string $expression): array + { + return $macher + [ + 'pact:generator:type' => 'ProviderState', + 'expression' => $expression, + ]; + } + + /** + * Value that must be equal to the example. This is mainly used to reset the matching rules which cascade. + * + * @return array + */ + public function equal(mixed $value): array + { + return [ + 'pact:matcher:type' => 'equality', + 'value' => $value, + ]; + } + + /** + * Value that must include the example value as a substring. + * + * @return array + */ + public function includes(string $value): array + { + return [ + 'pact:matcher:type' => 'include', + 'value' => $value, + ]; + } + + /** + * Value must be a number + * + * @param int|float|null $value Example value. If omitted a random integer value will be generated. + * + * @return array + */ + public function number(int|float|null $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'number', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'number', + ]; + } + + /** + * Matches the items in an array against a number of variants. Matching is successful if each variant + * occurs once in the array. Variants may be objects containing matching rules. + * + * @param array $variants + * + * @return array + */ + public function arrayContaining(array $variants): array + { + return [ + 'pact:matcher:type' => 'arrayContains', + 'variants' => $variants, + ]; + } + + /** + * Value must be present and not empty (not null or the empty string or empty array or empty object) + * + * @return array + */ + public function notEmpty(mixed $value): array + { + return [ + 'value' => $value, + 'pact:matcher:type' => 'notEmpty', + ]; + } + + /** + * Value must be valid based on the semver specification + * + * @return array + */ + public function semver(string $value): array + { + return [ + 'value' => $value, + 'pact:matcher:type' => 'semver', + ]; + } + + /** + * Matches the response status code. + * + * @return array + */ + public function statusCode(string $status): array + { + if (!in_array($status, HttpStatus::all())) { + throw new Exception(sprintf("Status '%s' is not supported. Supported status are: %s", $status, implode(', ', HttpStatus::all()))); + } + + return [ + 'status' => $status, + 'pact:matcher:type' => 'statusCode', + ]; + } + + /** + * Match the values in a map, ignoring the keys + * + * @param array $values + * + * @return array + */ + public function values(array $values): array + { + return [ + 'value' => $values, + 'pact:matcher:type' => 'values', + ]; + } + + /** + * Match binary data by its content type (magic file check) + * + * @return array + */ + public function contentType(string $contentType): array + { + return [ + 'value' => $contentType, + 'pact:matcher:type' => 'contentType', + ]; + } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 6ad9025a..39b0e168 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Matcher; use Exception; +use PhpPact\Consumer\Matcher\HttpStatus; use PhpPact\Consumer\Matcher\Matcher; use PHPUnit\Framework\TestCase; @@ -35,14 +36,10 @@ public function testLike() } /** - * @throws Exception + * @dataProvider dataProviderForEachLikeTest */ - public function testEachLikeStdClass() + public function testEachLike(object|array $value) { - $object = new \stdClass(); - $object->value1 = $this->matcher->like(1); - $object->value2 = 2; - $expected = \json_encode([ 'value' => [ [ @@ -51,27 +48,70 @@ public function testEachLikeStdClass() 'pact:matcher:type' => 'type', ], 'value2' => 2, - ] + ], ], 'pact:matcher:type' => 'type', 'min' => 1, ]); - $actual = \json_encode($this->matcher->eachLike($object, 1)); + $actual = \json_encode($this->matcher->eachLike($value)); $this->assertEquals($expected, $actual); } + public function dataProviderForEachLikeTest() + { + $value1Matcher = [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ]; + + $object = new \stdClass(); + $object->value1 = $value1Matcher; + $object->value2 = 2; + + $array = [ + 'value1' => $value1Matcher, + 'value2' => 2, + ]; + + return [ + [$object], + [$array], + ]; + } + /** - * @throws Exception + * @dataProvider dataProviderForEachLikeTest */ - public function testEachLikeArray() + public function testAtLeastLike(object|array $value) { - $object = [ - 'value1' => $this->matcher->like(1), + $eachValueMatcher = [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], 'value2' => 2, ]; + $expected = \json_encode([ + 'value' => [ + $eachValueMatcher, + $eachValueMatcher, + ], + 'pact:matcher:type' => 'type', + 'min' => 2, + ]); + $actual = \json_encode($this->matcher->atLeastLike($value, 2)); + + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider dataProviderForEachLikeTest + */ + public function testAtMostLike(object|array $value) + { $expected = \json_encode([ 'value' => [ [ @@ -80,13 +120,50 @@ public function testEachLikeArray() 'pact:matcher:type' => 'type', ], 'value2' => 2, - ] + ], ], 'pact:matcher:type' => 'type', - 'min' => 1, + 'max' => 2, + ]); + + $actual = \json_encode($this->matcher->atMostLike($value, 2)); + + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testAtLeastAndMostLikeInvalidMin() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid minimum number of elements'); + $this->matcher->atLeastAndMostLike('text', 10, 1); + } + + /** + * @dataProvider dataProviderForEachLikeTest + */ + public function testAtLeastAndMostLike(object|array $value) + { + $eachValueMatcher = [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ]; + $expected = \json_encode([ + 'value' => [ + $eachValueMatcher, + $eachValueMatcher, + ], + 'pact:matcher:type' => 'type', + 'min' => 2, + 'max' => 4, ]); - $actual = \json_encode($this->matcher->eachLike($object, 1)); + $actual = \json_encode($this->matcher->atLeastAndMostLike($value, 2, 4)); $this->assertEquals($expected, $actual); } @@ -97,6 +174,7 @@ public function testEachLikeArray() public function testRegexNoMatch() { $this->expectException(Exception::class); + $this->expectExceptionMessage('The pattern BadPattern is not valid for value SomeWord. Failed with error code 0.'); $this->matcher->regex('SomeWord', 'BadPattern'); } @@ -119,7 +197,7 @@ public function testRegex() /** * @throws Exception */ - public function testDate() + public function testDateISO8601() { $expected = [ 'value' => '2010-01-17', @@ -137,7 +215,7 @@ public function testDate() * * @throws Exception */ - public function testTime($time) + public function testTimeISO8601($time) { $expected = [ 'value' => $time, @@ -171,7 +249,7 @@ public function dataProviderForTimeTest() * * @throws Exception */ - public function testDateTime($dateTime) + public function testDateTimeISO8601($dateTime) { $expected = [ 'value' => $dateTime, @@ -203,7 +281,7 @@ public function dataProviderForDateTimeTest() * * @throws Exception */ - public function testDateTimeWithMillis($dateTime) + public function testDateTimeWithMillisISO8601($dateTime) { $expected = [ 'value' => $dateTime, @@ -276,6 +354,72 @@ public function testDecimal() $this->assertEquals('{"value":13.01,"pact:matcher:type":"type"}', $json); } + public function testIntegerV3() + { + $expected = [ + 'value' => 13, + 'pact:matcher:type' => 'integer', + ]; + $actual = $this->matcher->integerV3(13); + + $this->assertEquals($expected, $actual); + } + + public function testRandomIntegerV3() + { + $expected = [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'integer', + ]; + $actual = $this->matcher->integerV3(); + + $this->assertEquals($expected, $actual); + } + + public function testBooleanV3() + { + $expected = [ + 'value' => true, + 'pact:matcher:type' => 'boolean', + ]; + $actual = $this->matcher->booleanV3(true); + + $this->assertEquals($expected, $actual); + } + + public function testRandomBooleanV3() + { + $expected = [ + 'pact:generator:type' => 'RandomBoolean', + 'pact:matcher:type' => 'boolean', + ]; + $actual = $this->matcher->booleanV3(); + + $this->assertEquals($expected, $actual); + } + + public function testDecimalV3() + { + $expected = [ + 'value' => 13.01, + 'pact:matcher:type' => 'decimal', + ]; + $actual = $this->matcher->decimalV3(13.01); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDecimalV3() + { + $expected = [ + 'pact:generator:type' => 'RandomDecimal', + 'pact:matcher:type' => 'decimal', + ]; + $actual = $this->matcher->decimalV3(); + + $this->assertEquals($expected, $actual); + } + /** * @throws Exception */ @@ -286,8 +430,24 @@ public function testHexadecimal() 'regex' => '^[0-9a-fA-F]+$', 'pact:matcher:type' => 'regex', ]; + $actual = $this->matcher->hexadecimal('3F'); + + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomHexadecimal() + { + $expected = [ + 'regex' => '^[0-9a-fA-F]+$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'RandomHexadecimal', + ]; + $actual = $this->matcher->hexadecimal(); - $this->assertEquals($expected, $this->matcher->hexadecimal()); + $this->assertEquals($expected, $actual); } /** @@ -300,8 +460,24 @@ public function testUuid() 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', 'pact:matcher:type' => 'regex', ]; + $actual = $this->matcher->uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a'); - $this->assertEquals($expected, $this->matcher->uuid()); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomUuid() + { + $expected = [ + 'pact:generator:type' => 'Uuid', + 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', + 'pact:matcher:type' => 'regex', + ]; + $actual = $this->matcher->uuid(); + + $this->assertEquals($expected, $actual); } /** @@ -332,7 +508,6 @@ public function testIpv6Address() $this->assertEquals($expected, $this->matcher->ipv6Address()); } - /** * @throws Exception */ @@ -345,4 +520,323 @@ public function testEmail() ]; $this->assertEquals($expected, $this->matcher->email()); } + + /** + * @throws Exception + */ + public function testIpv4AddressV3() + { + $expected = $this->matcher->ipv4Address(); + $actual = $this->matcher->ipv4AddressV3('127.0.0.13'); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testIpv6AddressV3() + { + $expected = $this->matcher->ipv6Address(); + $actual = $this->matcher->ipv6AddressV3('::ffff:192.0.2.128'); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testEmailV3() + { + $expected = $this->matcher->email(); + $actual = $this->matcher->emailV3('hello@pact.io'); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomIpv4AddressV3() + { + $expected = [ + 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->ipv4AddressV3(); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomIpv6AddressV3() + { + $expected = [ + 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->ipv6AddressV3(); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomEmailV3() + { + $expected = [ + 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->emailV3(); + $this->assertEquals($expected, $actual); + } + + public function testNullValue() + { + $expected = [ + 'pact:matcher:type' => 'null', + ]; + $actual = $this->matcher->nullValue(); + $this->assertEquals($expected, $actual); + } + + public function testDate() + { + $expected = [ + 'value' => '2022-11-21', + 'pact:matcher:type' => 'date', + 'format' => 'yyyy-MM-dd', + ]; + $actual = $this->matcher->date('yyyy-MM-dd', '2022-11-21'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDate() + { + $expected = [ + 'pact:generator:type' => 'Date', + 'pact:matcher:type' => 'date', + 'format' => 'yyyy-MM-dd', + ]; + $actual = $this->matcher->date(); + + $this->assertEquals($expected, $actual); + } + + public function testTime() + { + $expected = [ + 'value' => '21:45::31', + 'pact:matcher:type' => 'time', + 'format' => 'HH:mm::ss', + ]; + $actual = $this->matcher->time('HH:mm::ss', '21:45::31'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomTime() + { + $expected = [ + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'format' => 'HH:mm::ss', + ]; + $actual = $this->matcher->time(); + + $this->assertEquals($expected, $actual); + } + + public function testDateTime() + { + $expected = [ + 'value' => '2015-08-06T16:53:10', + 'pact:matcher:type' => 'datetime', + 'format' => "YYYY-mm-DD'T'HH:mm:ss", + ]; + $actual = $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2015-08-06T16:53:10'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDateTime() + { + $expected = [ + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'format' => "YYYY-mm-DD'T'HH:mm:ss", + ]; + $actual = $this->matcher->datetime(); + + $this->assertEquals($expected, $actual); + } + + public function testString() + { + $expected = [ + 'pact:matcher:type' => 'type', + 'value' => 'test string', + ]; + $actual = $this->matcher->string('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomString() + { + $expected = [ + 'pact:generator:type' => 'RandomString', + 'pact:matcher:type' => 'type', + 'value' => 'some string', + ]; + $actual = $this->matcher->string(); + + $this->assertEquals($expected, $actual); + } + + public function testFromProviderState() + { + $expected = [ + 'regex' => Matcher::UUID_V4_FORMAT, + 'pact:matcher:type' => 'regex', + 'value' => 'f2392c53-6e55-48f7-8e08-18e4bf99c795', + 'pact:generator:type' => 'ProviderState', + 'expression' => '${id}', + ]; + $actual = $this->matcher->fromProviderState($this->matcher->uuid('f2392c53-6e55-48f7-8e08-18e4bf99c795'), '${id}'); + + $this->assertEquals($expected, $actual); + } + + public function testEqual() + { + $expected = [ + 'pact:matcher:type' => 'equality', + 'value' => 'test string', + ]; + $actual = $this->matcher->equal('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testIncludes() + { + $expected = [ + 'pact:matcher:type' => 'include', + 'value' => 'test string', + ]; + $actual = $this->matcher->includes('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testNumber() + { + $expected = [ + 'value' => 13.01, + 'pact:matcher:type' => 'number', + ]; + $actual = $this->matcher->number(13.01); + + $this->assertEquals($expected, $actual); + } + + public function testRandomNumber() + { + $expected = [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'number', + ]; + $actual = $this->matcher->number(); + + $this->assertEquals($expected, $actual); + } + + public function testArrayContaining() + { + $expected = [ + 'pact:matcher:type' => 'arrayContains', + 'variants' => [ + 'item 1', + 'item 2' + ], + ]; + $actual = $this->matcher->arrayContaining([ + 'item 1', + 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + + public function testNotEmpty() + { + $expected = [ + 'value' => 'not empty string', + 'pact:matcher:type' => 'notEmpty', + ]; + $actual = $this->matcher->notEmpty('not empty string'); + + $this->assertEquals($expected, $actual); + } + + public function testSemver() + { + $expected = [ + 'value' => '1.2.3', + 'pact:matcher:type' => 'semver', + ]; + $actual = $this->matcher->semver('1.2.3'); + + $this->assertEquals($expected, $actual); + } + + public function testInvalidStatusCode() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Status 'invalid' is not supported. Supported status are: info, success, redirect, clientError, serverError, nonError, error"); + $this->matcher->statusCode('invalid'); + } + + public function testValidStatusCode() + { + $expected = [ + 'status' => 'success', + 'pact:matcher:type' => 'statusCode', + ]; + $actual = $this->matcher->statusCode(HttpStatus::SUCCESS); + + $this->assertEquals($expected, $actual); + } + + public function testValues() + { + $expected = [ + 'pact:matcher:type' => 'values', + 'value' => [ + 'item 1', + 'item 2' + ], + ]; + $actual = $this->matcher->values([ + 'item 1', + 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + + public function testContentType() + { + $expected = [ + 'value' => 'image/jpeg', + 'pact:matcher:type' => 'contentType', + ]; + $actual = $this->matcher->contentType('image/jpeg'); + + $this->assertEquals($expected, $actual); + } } From cd499130c54da33c1498e5b3a4786bb49e452e96 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 6 Mar 2023 16:26:26 +0700 Subject: [PATCH 056/298] Move endpoint from config to parameter --- README.md | 5 ++--- UPGRADE-9.0.md | 7 +++++++ .../StubService/Service/StubServerHttpService.php | 4 ++-- .../Service/StubServerHttpServiceInterface.php | 2 +- .../Standalone/StubService/StubServerConfig.php | 15 --------------- .../StubService/StubServerConfigInterface.php | 4 ---- .../Service/StubServerHttpServiceTest.php | 11 +++++------ .../Standalone/StubServer/StubServerTest.php | 8 +++----- 8 files changed, 20 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f4c998c6..5df03992 100644 --- a/README.md +++ b/README.md @@ -400,13 +400,12 @@ $endpoint = 'test'; $config = (new StubServerConfig()) ->setFiles($files) - ->setPort($port) - ->setEndpoint($endpoint); + ->setPort($port); $stubServer = new StubServer($config); $stubServer->start(); $service = new StubServerHttpService(new GuzzleClient(), $config); -echo $service->getJson(); // output: {"results":[{"name":"Games"}]} +echo $service->getJson($endpoint); // output: {"results":[{"name":"Games"}]} ``` diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md index aca9d300..323b034a 100644 --- a/UPGRADE-9.0.md +++ b/UPGRADE-9.0.md @@ -50,3 +50,10 @@ UPGRADE FROM 8.x to 9.0 $this->assertTrue($verifyResult); ``` + +* Stub Server + * Endpoint now can be set by: + ```php + $service = new StubServerHttpService(new GuzzleClient(), $this->config); + $service->getJson($endpoint); + ``` diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php index 81f5f726..f40100a1 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php @@ -26,9 +26,9 @@ public function __construct(ClientInterface $client, StubServerConfigInterface $ * {@inheritdoc} * @throws \JsonException */ - public function getJson(): string + public function getJson(string $endpoint): string { - $uri = $this->config->getBaseUri()->withPath('/' . $this->config->getEndpoint()); + $uri = $this->config->getBaseUri()->withPath('/' . $endpoint); $response = $this->client->get($uri, [ 'headers' => [ 'Content-Type' => 'application/json', diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php index e1b84aa0..325637e2 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php @@ -7,5 +7,5 @@ interface StubServerHttpServiceInterface /** * Get the current state of the PACT JSON file and write it to disk. */ - public function getJson(): string; + public function getJson(string $endpoint): string; } diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index beb55ee4..42d62315 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -46,8 +46,6 @@ class StubServerConfig implements StubServerConfigInterface private bool $emptyProviderState = false; private bool $insecureTls = false; - private string $endpoint; - public function getBrokerUrl(): ?UriInterface { return $this->brokerUrl; @@ -256,17 +254,4 @@ public function getBaseUri(): UriInterface { return new Uri("http://localhost:{$this->getPort()}"); } - - - public function getEndpoint(): string - { - return $this->endpoint; - } - - public function setEndpoint(string $endpoint): StubServerConfigInterface - { - $this->endpoint = $endpoint; - - return $this; - } } diff --git a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php index ecfe26c0..25d0decb 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php @@ -150,8 +150,4 @@ public function setProviderNames(array $providerNames): self; public function getProviderNames(): array; public function getBaseUri(): UriInterface; - - public function getEndpoint(): string; - - public function setEndpoint(string $endpoint): self; } diff --git a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php index 9a9769b0..734e978c 100644 --- a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php +++ b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php @@ -28,14 +28,12 @@ class StubServerHttpServiceTest extends TestCase */ protected function setUp(): void { - $files = [__DIR__ . '/../../../../_resources/someconsumer-someprovider.json']; - $port = 7201; - $endpoint = 'test'; + $files = [__DIR__ . '/../../../../_resources/someconsumer-someprovider.json']; + $port = 7201; $this->config = (new StubServerConfig()) ->setFiles($files) - ->setPort($port) - ->setEndpoint($endpoint); + ->setPort($port); $this->stubServer = new StubServer($this->config); $this->stubServer->start(); @@ -49,7 +47,8 @@ protected function tearDown(): void public function testGetJson() { - $result = $this->service->getJson(); + $endpoint = 'test'; + $result = $this->service->getJson($endpoint); $this->assertEquals('{"results":[{"name":"Games"}]}', $result); } } diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubServer/StubServerTest.php index 687e5800..4ea6cb51 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerTest.php @@ -14,14 +14,12 @@ class StubServerTest extends TestCase public function testStartAndStop() { try { - $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; - $port = 7201; - $endpoint= 'test'; + $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; + $port = 7201; $subject = (new StubServerConfig()) ->setFiles($files) - ->setPort($port) - ->setEndpoint($endpoint); + ->setPort($port); $stubServer = new StubServer($subject); $pid = $stubServer->start(); From 05fca684607eb5c6ca87640cfe33aa85c88347fc Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 6 Mar 2023 17:39:54 +0700 Subject: [PATCH 057/298] Allow random port for stub server --- .../Standalone/StubService/StubServer.php | 7 ++++++- .../StubService/StubServerConfig.php | 4 ++-- .../StubService/StubServerConfigInterface.php | 4 ++-- .../Standalone/StubServer/StubServerTest.php | 20 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index 551e884a..7e97dacd 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -35,7 +35,12 @@ public function start(): ?int echo $buffer; }); $this->process->waitUntil(function (string $type, string $output) { - return false !== \strpos($output, 'Server started on port'); + $result = preg_match('/Server started on port (\d+)/', $output, $matches); + if ($result === 1 && $this->config->getPort() === 0) { + $this->config->setPort((int)$matches[1]); + } + + return $result; }); return $this->process->getPid(); diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index beb55ee4..9fb29ae7 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -11,7 +11,7 @@ class StubServerConfig implements StubServerConfigInterface { private ?UriInterface $brokerUrl = null; - private ?int $port = null; + private int $port = 0; private ?string $extension = null; private ?string $logLevel = null; @@ -108,7 +108,7 @@ public function getLogLevel(): ?string return $this->logLevel; } - public function getPort(): ?int + public function getPort(): int { return $this->port; } diff --git a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php index ecfe26c0..8f1f0798 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php @@ -54,9 +54,9 @@ public function getLogLevel(): ?string; public function setLogLevel(string $logLevel): self; /** - * @return null|int the port of the stub service + * @return int the port of the stub service */ - public function getPort(): ?int; + public function getPort(): int; /** * @param int $port Port to run on (defaults to random port assigned by the OS) diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubServer/StubServerTest.php index 687e5800..5f0e130f 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerTest.php @@ -31,4 +31,24 @@ public function testStartAndStop() $this->assertTrue($result); } } + + /** + * @throws \Exception + */ + public function testRandomPort(): void + { + try { + $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; + + $subject = (new StubServerConfig()) + ->setFiles($files); + + $stubServer = new StubServer($subject); + $stubServer->start(); + $this->assertGreaterThan(0, $subject->getPort()); + } finally { + $result = $stubServer->stop(); + $this->assertTrue($result); + } + } } From 194ad08b56605f44667929858ad242d222021b03 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 4 Apr 2023 10:01:34 +0700 Subject: [PATCH 058/298] Fix stub server hang on too low log level --- .../LogLevelNotSupportedException.php | 9 ++++++++ .../Standalone/StubService/StubServer.php | 23 +++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php diff --git a/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php b/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php new file mode 100644 index 00000000..05de2a78 --- /dev/null +++ b/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php @@ -0,0 +1,9 @@ +process->start(function (string $type, string $buffer) { echo $buffer; }); - $this->process->waitUntil(function (string $type, string $output) { - $result = preg_match('/Server started on port (\d+)/', $output, $matches); - if ($result === 1 && $this->config->getPort() === 0) { - $this->config->setPort((int)$matches[1]); - } - return $result; - }); + $logLevel = $this->config->getLogLevel(); + if (is_null($logLevel) || in_array($logLevel, ['info', 'debug', 'trace'])) { + $this->process->waitUntil(function (string $type, string $output) { + $result = preg_match('/Server started on port (\d+)/', $output, $matches); + if ($result === 1 && $this->config->getPort() === 0) { + $this->config->setPort((int)$matches[1]); + } + + return $result; + }); + } else { + if ($this->config->getPort() === 0) { + throw new LogLevelNotSupportedException(sprintf("Setting random port for stub server required log level 'info', 'debug' or 'trace'. '%s' given.", $logLevel)); + } + } return $this->process->getPid(); } From abf6559aa4a678818f9c775bd9bb51a780f16760 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 12 May 2023 20:41:12 +0700 Subject: [PATCH 059/298] Remove duplicated code in config --- .../ExampleMessageConsumerTest.php | 2 +- src/PhpPact/Config/PactConfig.php | 195 ++++++++++++++++++ src/PhpPact/Config/PactConfigInterface.php | 79 +++++++ src/PhpPact/Consumer/MessageBuilder.php | 2 +- .../MockService/MockServerConfig.php | 192 +---------------- .../MockService/MockServerConfigInterface.php | 10 - .../MockService/MockServerEnvConfig.php | 2 - .../PactMessage/PactMessageConfig.php | 150 +------------- tests/PhpPact/Config/PactConfigTest.php | 66 ++++++ .../MockServer/MockServerConfigTest.php | 8 +- .../PactMessage/PactMessageConfigTest.php | 14 ++ 11 files changed, 368 insertions(+), 352 deletions(-) create mode 100644 src/PhpPact/Config/PactConfig.php create mode 100644 src/PhpPact/Config/PactConfigInterface.php create mode 100644 tests/PhpPact/Config/PactConfigTest.php create mode 100644 tests/PhpPact/Standalone/PactMessage/PactMessageConfigTest.php diff --git a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php index 6c17c6ce..e9bda5d0 100644 --- a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php +++ b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php @@ -6,7 +6,7 @@ use Exception; use PhpPact\Consumer\MessageBuilder; -use PhpPact\Standalone\PactConfigInterface; +use PhpPact\Config\PactConfigInterface; use PhpPact\Standalone\PactMessage\PactMessageConfig; use PHPUnit\Framework\TestCase; use stdClass; diff --git a/src/PhpPact/Config/PactConfig.php b/src/PhpPact/Config/PactConfig.php new file mode 100644 index 00000000..a54893f5 --- /dev/null +++ b/src/PhpPact/Config/PactConfig.php @@ -0,0 +1,195 @@ +consumer; + } + + /** + * {@inheritdoc} + */ + public function setConsumer(string $consumer): self + { + $this->consumer = $consumer; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getProvider(): string + { + return $this->provider; + } + + /** + * {@inheritdoc} + */ + public function setProvider(string $provider): self + { + $this->provider = $provider; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactDir(): string + { + if ($this->pactDir === null) { + return \sys_get_temp_dir(); + } + + return $this->pactDir; + } + + /** + * {@inheritdoc} + */ + public function setPactDir(?string $pactDir): self + { + if ($pactDir === null) { + return $this; + } + + if ('\\' !== \DIRECTORY_SEPARATOR) { + $pactDir = \str_replace('\\', \DIRECTORY_SEPARATOR, $pactDir); + } + + $this->pactDir = $pactDir; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactFileWriteMode(): string + { + return $this->pactFileWriteMode; + } + + /** + * {@inheritdoc} + */ + public function setPactFileWriteMode(string $pactFileWriteMode): self + { + $options = [self::MODE_OVERWRITE, self::MODE_MERGE]; + + if (!\in_array($pactFileWriteMode, $options)) { + $implodedOptions = \implode(', ', $options); + + throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); + } + + $this->pactFileWriteMode = $pactFileWriteMode; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPactSpecificationVersion(): string + { + return $this->pactSpecificationVersion; + } + + /** + * {@inheritdoc} + * + * @throws \UnexpectedValueException + */ + public function setPactSpecificationVersion(string $pactSpecificationVersion): self + { + /* + * Parse the version but do not assign it. If it is an invalid version, an exception is thrown + */ + $parser = new VersionParser(); + $parser->normalize($pactSpecificationVersion); + + $this->pactSpecificationVersion = $pactSpecificationVersion; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLog(): ?string + { + return $this->log; + } + + /** + * {@inheritdoc} + */ + public function setLog(string $log): self + { + $this->log = $log; + + return $this; + } + + public function getLogLevel(): ?string + { + return $this->logLevel; + } + + public function setLogLevel(string $logLevel): PactConfigInterface + { + $logLevel = \strtoupper($logLevel); + if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { + throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); + } + $this->logLevel = $logLevel; + + return $this; + } +} diff --git a/src/PhpPact/Config/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php new file mode 100644 index 00000000..a1597a8c --- /dev/null +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -0,0 +1,79 @@ +secure = $secure; @@ -135,156 +99,6 @@ public function getBaseUri(): UriInterface return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); } - /** - * {@inheritdoc} - */ - public function getConsumer(): string - { - return $this->consumer; - } - - /** - * {@inheritdoc} - */ - public function setConsumer(string $consumer): self - { - $this->consumer = $consumer; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProvider(): string - { - return $this->provider; - } - - /** - * {@inheritdoc} - */ - public function setProvider(string $provider): self - { - $this->provider = $provider; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactDir(): string - { - if ($this->pactDir === null) { - return \sys_get_temp_dir(); - } - - return $this->pactDir; - } - - /** - * {@inheritdoc} - */ - public function setPactDir(?string $pactDir): self - { - if ($pactDir === null) { - return $this; - } - - if ('\\' !== \DIRECTORY_SEPARATOR) { - $pactDir = \str_replace('\\', \DIRECTORY_SEPARATOR, $pactDir); - } - - $this->pactDir = $pactDir; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactFileWriteMode(): string - { - return $this->pactFileWriteMode; - } - - /** - * {@inheritdoc} - */ - public function setPactFileWriteMode(string $pactFileWriteMode): self - { - $options = ['overwrite', 'merge']; - - if (!\in_array($pactFileWriteMode, $options)) { - $implodedOptions = \implode(', ', $options); - - throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); - } - - $this->pactFileWriteMode = $pactFileWriteMode; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactSpecificationVersion(): string - { - return $this->pactSpecificationVersion; - } - - /** - * {@inheritdoc} - */ - public function setPactSpecificationVersion(string $pactSpecificationVersion): self - { - /* - * Parse the version but do not assign it. If it is an invalid version, an exception is thrown - */ - $parser = new VersionParser(); - $parser->normalize($pactSpecificationVersion); - - $this->pactSpecificationVersion = $pactSpecificationVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getLog(): ?string - { - return $this->log; - } - - /** - * {@inheritdoc} - */ - public function setLog(string $log): self - { - $this->log = $log; - - return $this; - } - - public function getLogLevel(): ?string - { - return $this->logLevel; - } - - public function setLogLevel(string $logLevel): PactConfigInterface - { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); - } - $this->logLevel = $logLevel; - - return $this; - } - public function hasCors(): bool { return $this->cors; diff --git a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php index b61f8d1f..2682d875 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php @@ -41,16 +41,6 @@ public function setSecure(bool $secure): self; public function getBaseUri(): UriInterface; - /** - * @return string 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten - */ - public function getPactFileWriteMode(): string; - - /** - * @param string $pactFileWriteMode 'merge' or 'overwrite' merge means that interactions are added and overwrite means that the entire file is overwritten - */ - public function setPactFileWriteMode(string $pactFileWriteMode): self; - public function hasCors(): bool; public function setCors(mixed $flag): self; diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index fae19e83..7a832881 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -9,8 +9,6 @@ */ class MockServerEnvConfig extends MockServerConfig { - public const DEFAULT_SPECIFICATION_VERSION = '2.0.0'; - /** * @throws MissingEnvVariableException */ diff --git a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php b/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php index d386fe88..56314f6c 100644 --- a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php +++ b/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php @@ -2,157 +2,11 @@ namespace PhpPact\Standalone\PactMessage; -use Composer\Semver\VersionParser; -use PhpPact\Standalone\PactConfigInterface; +use PhpPact\Config\PactConfig; /** * Configuration defining the default PhpPact Ruby Standalone server. - * Class MockServerConfig. */ -class PactMessageConfig implements PactConfigInterface +class PactMessageConfig extends PactConfig { - /** - * Consumer name. - */ - private string $consumer; - - /** - * Provider name. - */ - private string $provider; - - /** - * Directory to which the pacts will be written. - */ - private ?string $pactDir = null; - - /** - * The pact specification version to use when writing the pact. Note that only versions 1 and 2 are currently supported. - */ - private string $pactSpecificationVersion; - - /** - * File to which to log output. - */ - private string $log; - - private string $logLevel; - - /** - * {@inheritdoc} - */ - public function getConsumer(): string - { - return $this->consumer; - } - - /** - * {@inheritdoc} - */ - public function setConsumer(string $consumer): PactConfigInterface - { - $this->consumer = $consumer; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProvider(): string - { - return $this->provider; - } - - /** - * {@inheritdoc} - */ - public function setProvider(string $provider): PactConfigInterface - { - $this->provider = $provider; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactDir(): string - { - if ($this->pactDir === null) { - return \sys_get_temp_dir(); - } - - return $this->pactDir; - } - - /** - * {@inheritdoc} - */ - public function setPactDir($pactDir): PactConfigInterface - { - $this->pactDir = $pactDir; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPactSpecificationVersion(): string - { - return $this->pactSpecificationVersion; - } - - /** - * {@inheritdoc} - * - * @throws \UnexpectedValueException - */ - public function setPactSpecificationVersion($pactSpecificationVersion): PactConfigInterface - { - /* - * Parse the version but do not assign it. If it is an invalid version, an exception is thrown - */ - $parser = new VersionParser(); - $parser->normalize($pactSpecificationVersion); - - $this->pactSpecificationVersion = $pactSpecificationVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getLog(): string - { - return $this->log; - } - - /** - * {@inheritdoc} - */ - public function setLog(string $log): PactConfigInterface - { - $this->log = $log; - - return $this; - } - - public function getLogLevel(): string - { - return $this->logLevel; - } - - public function setLogLevel(string $logLevel): PactConfigInterface - { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); - } - $this->logLevel = $logLevel; - - return $this; - } } diff --git a/tests/PhpPact/Config/PactConfigTest.php b/tests/PhpPact/Config/PactConfigTest.php new file mode 100644 index 00000000..9b43621c --- /dev/null +++ b/tests/PhpPact/Config/PactConfigTest.php @@ -0,0 +1,66 @@ +config = new PactConfig(); + } + + public function testSetters(): void + { + $provider = 'test-provider'; + $consumer = 'test-consumer'; + $pactDir = 'test-pact-dir/'; + $pactSpecificationVersion = '2.0.0'; + $log = 'test-log-dir/'; + $logLevel = 'ERROR'; + $pactFileWriteMode = 'merge'; + + $this->config + ->setProvider($provider) + ->setConsumer($consumer) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($pactSpecificationVersion) + ->setLog($log) + ->setLogLevel($logLevel) + ->setPactFileWriteMode($pactFileWriteMode); + + static::assertSame($provider, $this->config->getProvider()); + static::assertSame($consumer, $this->config->getConsumer()); + static::assertSame($pactDir, $this->config->getPactDir()); + static::assertSame($pactSpecificationVersion, $this->config->getPactSpecificationVersion()); + static::assertSame($log, $this->config->getLog()); + static::assertSame($logLevel, $this->config->getLogLevel()); + static::assertSame($pactFileWriteMode, $this->config->getPactFileWriteMode()); + } + + public function testInvalidPactSpecificationVersion(): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('Invalid version string "invalid"'); + $this->config->setPactSpecificationVersion('invalid'); + } + + public function testInvalidLogLevel(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('LogLevel VERBOSE not supported.'); + $this->config->setLogLevel('VERBOSE'); + } + + public function testInvalidPactFileWriteMode(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid PhpPact File Write Mode, value must be one of the following: overwrite, merge."); + $this->config->setPactFileWriteMode('APPEND'); + } +} diff --git a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php index 30a43b0b..2bca2957 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php @@ -17,6 +17,8 @@ public function testSetters() $pactFileWriteMode = 'merge'; $log = 'test-log-dir/'; $cors = true; + $healthCheckTimeout = 11; + $healthCheckRetrySec = 22; $pactSpecificationVersion = '2.0'; $subject = (new MockServerConfig()) @@ -28,7 +30,9 @@ public function testSetters() ->setPactFileWriteMode($pactFileWriteMode) ->setLog($log) ->setPactSpecificationVersion($pactSpecificationVersion) - ->setCors($cors); + ->setCors($cors) + ->setHealthCheckTimeout($healthCheckTimeout) + ->setHealthCheckRetrySec($healthCheckRetrySec); static::assertSame($host, $subject->getHost()); static::assertSame($port, $subject->getPort()); @@ -39,5 +43,7 @@ public function testSetters() static::assertSame($log, $subject->getLog()); static::assertSame($pactSpecificationVersion, $subject->getPactSpecificationVersion()); static::assertSame($cors, $subject->hasCors()); + static::assertSame($healthCheckTimeout, $subject->getHealthCheckTimeout()); + static::assertSame($healthCheckRetrySec, $subject->getHealthCheckRetrySec()); } } diff --git a/tests/PhpPact/Standalone/PactMessage/PactMessageConfigTest.php b/tests/PhpPact/Standalone/PactMessage/PactMessageConfigTest.php new file mode 100644 index 00000000..e03aca21 --- /dev/null +++ b/tests/PhpPact/Standalone/PactMessage/PactMessageConfigTest.php @@ -0,0 +1,14 @@ +config = new PactMessageConfig(); + } +} From 9dbbde197aed79009fb1e08fdbfa0bc4ab6ab1ec Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 29 May 2023 22:49:33 +0700 Subject: [PATCH 060/298] Fix stub server always load pact files from pact broker --- src/PhpPact/Standalone/StubService/StubServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index 551e884a..52994774 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -29,7 +29,7 @@ public function __construct(StubServerConfigInterface $config) */ public function start(): ?int { - $this->process = new Process([Scripts::getStubService(), ...$this->getArguments()]); + $this->process = new Process([Scripts::getStubService(), ...$this->getArguments()], null, ['PACT_BROKER_BASE_URL' => false]); $this->process->start(function (string $type, string $buffer) { echo $buffer; From 498c2a804093c6fa6cb3ff193aa6adb5014fa44b Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 24 Jul 2023 15:28:42 +0100 Subject: [PATCH 061/298] chore: support aarch64 in pact-stub-server --- composer.json | 3 +- example/pacts/someconsumer-someprovider.json | 77 +++++++++++-------- .../pacts/test_consumer-test_provider.json | 59 +++++++------- 3 files changed, 78 insertions(+), 61 deletions(-) diff --git a/composer.json b/composer.json index 65ad4cd1..0770643b 100644 --- a/composer.json +++ b/composer.json @@ -91,9 +91,10 @@ "version": "0.5.3", "variables": { "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", + "{$architecture}": "in_array(php_uname('m'), ['arm64', 'aarch64']) ? 'aarch64' : 'x86_64'", "{$extension}": "PHP_OS_FAMILY === 'Windows' ? '.exe' : ''" }, - "url": "https://github.com/pact-foundation/pact-stub-server/releases/download/v{$version}/pact-stub-server-{$os}-x86_64{$extension}.gz", + "url": "https://github.com/pact-foundation/pact-stub-server/releases/download/v{$version}/pact-stub-server-{$os}-{$architecture}{$extension}.gz", "path": "bin/pact-stub-server/pact-stub-server{$extension}", "executable": true } diff --git a/example/pacts/someconsumer-someprovider.json b/example/pacts/someconsumer-someprovider.json index f9a78bce..2d917286 100644 --- a/example/pacts/someconsumer-someprovider.json +++ b/example/pacts/someconsumer-someprovider.json @@ -2,61 +2,76 @@ "consumer": { "name": "someConsumer" }, - "provider": { - "name": "someProvider" - }, "interactions": [ { - "description": "A get request to /goodbye/{name}", - "providerState": "Get Goodbye", + "description": "A get request to /hello/{name}", "request": { - "method": "GET", - "path": "/goodbye/Bob", "headers": { "Content-Type": "application/json" - } + }, + "method": "GET", + "path": "/hello/Bob" }, "response": { - "status": 200, + "body": { + "message": "Hello, Bob" + }, "headers": { "Content-Type": "application/json" }, - "body": { - "message": "Goodbye, Bob" - } - }, - "metadata": null + "matchingRules": { + "body": { + "$.message": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "(Hello, )[A-Za-z]+" + } + ] + } + }, + "header": {} + }, + "status": 200 + } }, { - "description": "A get request to /hello/{name}", + "description": "A get request to /goodbye/{name}", + "providerStates": [ + { + "name": "Get Goodbye" + } + ], "request": { - "method": "GET", - "path": "/hello/Bob", "headers": { "Content-Type": "application/json" - } + }, + "method": "GET", + "path": "/goodbye/Bob" }, "response": { - "status": 200, + "body": { + "message": "Goodbye, Bob" + }, "headers": { "Content-Type": "application/json" }, - "body": { - "message": "Hello, Bob" - }, - "matchingRules": { - "$.body.message": { - "match": "regex", - "regex": "(Hello, )[A-Za-z]+" - } - } - }, - "metadata": null + "status": 200 + } } ], "metadata": { + "pactRust": { + "ffi": "0.4.4", + "mockserver": "1.0.3", + "models": "1.0.13" + }, "pactSpecification": { - "version": "2.0.0" + "version": "3.0.0" } + }, + "provider": { + "name": "someProvider" } } \ No newline at end of file diff --git a/example/pacts/test_consumer-test_provider.json b/example/pacts/test_consumer-test_provider.json index ae5e2dab..13f684cd 100644 --- a/example/pacts/test_consumer-test_provider.json +++ b/example/pacts/test_consumer-test_provider.json @@ -2,52 +2,53 @@ "consumer": { "name": "test_consumer" }, - "provider": { - "name": "test_provider" - }, "messages": [ { - "description": "an alligator named Mary exists", + "contents": { + "song": "And the wind whispers Mary" + }, + "description": "footprints dressed in red", + "metadata": { + "contentType": "application/json", + "queue": "And the clowns have all gone to bed", + "routing_key": "And the clowns have all gone to bed" + }, "providerStates": [ { - "name": "a message" + "name": "You can hear happiness staggering on down the street" } - ], + ] + }, + { "contents": { "text": "Hello Mary" }, - "matchingRules": { - "body": { - } - }, - "metaData": { + "description": "an alligator named Mary exists", + "metadata": { + "contentType": "application/json", "queue": "wind cries", "routing_key": "wind cries" - } - }, - { - "description": "footprints dressed in red", + }, "providerStates": [ { - "name": "You can hear happiness staggering on down the street" - } - ], - "contents": { - "song": "And the wind whispers Mary" - }, - "matchingRules": { - "body": { + "name": "a message", + "params": { + "foo": "bar" + } } - }, - "metaData": { - "queue": "And the clowns have all gone to bed", - "routing_key": "And the clowns have all gone to bed" - } + ] } ], "metadata": { + "pactRust": { + "ffi": "0.4.4", + "models": "1.0.13" + }, "pactSpecification": { "version": "3.0.0" } + }, + "provider": { + "name": "test_provider" } -} +} \ No newline at end of file From 90c10051a321afc3c07e4a4aacf8789b34227c2f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 24 Jul 2023 15:29:44 +0100 Subject: [PATCH 062/298] ci: support for arm64 linux - add libffi-dev - add php-ffi ext --- .cirrus.yml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 00000000..4e24d43e --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,52 @@ +BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE + arch_check_script: + - uname -am + install_script: + - composer install + cirrus_ci_macos_local_script: | + if [ "$(uname -s)" == 'Darwin' ] && [ "$CIRRUS_CLI" == 'true' ]; then + chmod +x vendor/bin/php-cs-fixer + chmod +x vendor/bin/phpstan + chmod +x vendor/bin/phpunit + chmod +x bin/pact-stub-server/pact-stub-server + fi + lint_script: + - composer run lint + static_analysis_script: + - composer run static-code-analysis + test_script: + - composer test + +linux_arm64_task: + env: + COMPOSER_ALLOW_SUPERUSER: 1 + matrix: + - VERSION: 8.2 + - VERSION: 8.1 + - VERSION: 8.0 + container: + image: php:$VERSION + pre_req_script: + - apt update --yes && apt install --yes zip unzip git libffi-dev + - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php + - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer + - docker-php-ext-install sockets + - docker-php-ext-install ffi + version_check_script: + - php --version + << : *BUILD_TEST_TASK_TEMPLATE + +macos_arm64_task: +# https://www.markhesketh.com/switching-multiple-php-versions-on-macos/ + env: + matrix: + - VERSION: 8.2 + - VERSION: 8.1 + - VERSION: 8.0 + macos_instance: + image: ghcr.io/cirruslabs/macos-ventura-base:latest + pre_req_script: + - brew install php@$VERSION composer + version_check_script: + - php --version + << : *BUILD_TEST_TASK_TEMPLATE \ No newline at end of file From b8eafd3b33197e3324c3406689214b5dee7949e6 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 24 Jul 2023 15:30:31 +0100 Subject: [PATCH 063/298] chore(docs): add developer docs --- DEVELOPING.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 DEVELOPING.md diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 00000000..31b4f294 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,57 @@ +# Pact-PHP + +## Pre Reqs + +- PHP 8.x or greater +- FFI and Sockets extensions enabled in your php.ini + +## Steps + +1. Run `composer install` + 1. This will install php dependencies to `vendor` + 2. This will install pact libraries to `bin` +2. Run `composer test` + 1. This will run our unit tests +3. Run `composer lint` + 1. This will run the phpcs-lint +4. Run `composer fix` + 1. This will correct any auto fixable linter errors +5. Run `composer static-code-analysis` + 1. Run static code analysis + +## CI Locally + +### MacOS ARM + +#### Pre Reqs + +- MacOS ARM +- Tart.run +- Cirrus-CLI + +#### Steps + +Run all versions of PHP + +- `cirrus run --output github-actions macos_arm64 -e CIRRUS_CLI=true` + +Run a specified version of PHP + +- `cirrus run --output github-actions 'macos_arm64 VERSION:8.2' -e CIRRUS_CLI=true` + +### Linux ARM + +#### Pre Reqs + +- Docker +- x86_64 or arm64/aarch64 host + +#### Steps + +Run all versions of PHP + +- `cirrus run --output github-actions linux_arm64` + +Run a specified version of PHP + +- `cirrus run --output github-actions 'macos_arm64 VERSION:8.2'` \ No newline at end of file From bcdc7cc56697311d60c3067bdf531e0c0c8e1df4 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 24 Jul 2023 16:01:04 +0100 Subject: [PATCH 064/298] chore(docs): update migration doc to reflect 10.x --- UPGRADE-10.0.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ UPGRADE-9.0.md | 72 ------------------------- 2 files changed, 140 insertions(+), 72 deletions(-) create mode 100644 UPGRADE-10.0.md delete mode 100644 UPGRADE-9.0.md diff --git a/UPGRADE-10.0.md b/UPGRADE-10.0.md new file mode 100644 index 00000000..d634ea28 --- /dev/null +++ b/UPGRADE-10.0.md @@ -0,0 +1,140 @@ +# UPGRADE FROM 9.x to 10.0 + +We have migrated from the pact-ruby core, to the pact-reference(rust) core. + +This migrates from a CLI driven process for the Pact Framework, to an FFI process based framework. + +- Pre-requisites + + - PHP 8.x + + - PHP FFI Extension installed + +- Environment Variables + + - These environment variables are no longer required can be removed: + - PACT_CORS + - PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT + - PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC + +- Consumer + + - The `PhpPact\Consumer\Listener\PactTestListener` listener should be removed from your phpunit config + - Default Pact file write mode has been changed from 'overwrite' to 'merge'. Make sure old pact files are removed before running tests. + + ```shell + rm /path/to/pacts/*.json + ``` + + - Pact files now can ONLY be uploaded to Pact Broker by downloading and running Pact CLI manually. + + ```shell + pact-broker publish /path/to/pacts/*.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken + ``` + +- Verifier + + - `$config->setProviderName("providerName")` is now available via `$config->getProviderInfo()->setName("backend")` + - This is further chainable with the following options:- + - `->setHost('localhost')` + - `->setPort('8080')` + - `->setScheme('http')` + - `->setPath('/')` + - Different pacts sources can be configured via `addXxx` methods + - NB:- You must add at least one source, otherwise the verifier will pass, but not verify any Pact files. + - Types:- + - `addUrl` - Verify Provider by Pact Url retrieved by Broker (Webhooks) + - `addBroker` Verify Provider by dynamically fetched Pacts (Provider change) + - `addFile` / `addDir` - Verify Provider by local file or directory + + Example Usage: + + ```php + $config = new VerifierConfig(); + $config + ->setLogLevel('DEBUG'); + $config + ->getProviderInfo() + ->setName("personProvider") + ->setHost('localhost') + ->setPort('8080') + ->setScheme('http') + ->setPath('/'); + + if ($isCi = getenv('CI')) { + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderVersion(exec('git rev-parse --short HEAD')) + ->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD')); + $config->setPublishOptions($publishOptions); + } + + $broker = new Broker(); + $broker->setUsername(getenv('PACT_BROKER_USERNAME')); + $broker->setPassword(getenv('PACT_BROKER_PASSWORD')); + $broker->setUsername(getenv('PACT_BROKER_TOKEN')); + $verifier = new Verifier($config); + + // 1. verify with a broker, but using a pact url to verify a specific pact + // PACT_URL=http://localhost:9292/pacts/provider/personProvider/consumer/personConsumer/latest + if ($pact_url = getenv('PACT_URL')) { + $url = new Url(); + $url->setUrl(new Uri($pact_url)); + $verifier->addUrl($url); + } + // 2. verify files from local directory or file + // results will not be published + else if ($pactDir = getenv('PACT_DIR')) { + $verifier->addDirectory($pactDir); + } else if ($pactFile = getenv('PACT_FILE')) { + $verifier->addFile($pactFile); + } else { + // 2. verify with broker by fetching dynamic pacts (with consumer version selectors) + // if you don't setConsumerVersionSelectors then it will fetch the latest pact for the named provider + if ($pactBrokerBaseUrl = getenv('PACT_BROKER_BASE_URL')) { + $broker->setUrl(new Uri($pactBrokerBaseUrl)); + } else { + $broker->setUrl(new Uri('http://localhost:9292')); + } + // we need to set the provider branch here for PactBrokerWithDynamicConfiguration + // as $publishOptions->setProviderBranch value set above isn't used. + $broker->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD')); + // NOTE - this needs to be a boolean, not a string value, otherwise it doesn't pass through the selector. + // Maybe a pact-php or pact-rust thing + $selectors = (new ConsumerVersionSelectors()) + ->addSelector(' { "mainBranch" : true } ') + ->addSelector(' { "deployedOrReleased" : true } '); + $broker->setConsumerVersionSelectors($selectors); + $broker->setEnablePending(true); + $broker->setIncludeWipPactSince('2020-01-30'); + $verifier->addBroker($broker); + } + + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + ``` + +- Stub Server + + - No longer defaults to port 7201, picks free port at random. + - Endpoint now can be set by: + + ```php + $service = new StubServerHttpService(new GuzzleClient(), $this->config); + $service->getJson($endpoint); + ``` + +- Example Migrations to 10.x (Pull Request Diffs) + - PHP Verifier https://github.com/acmachado14/simple-pact/compare/main...YOU54F:simple-pact:ffi-next + - PHP Consumer https://github.com/YOU54F/pact-testing/compare/main...YOU54F:pact-testing:ffi-next + - PHP Consumer & Verifier + - Consumer https://github.com/YOU54F/014-pact-http-consumer-php/compare/main...YOU54F:014-pact-http-consumer-php:ffi-next + - Verifier https://github.com/YOU54F/015-pact-http-producer-php/compare/main...YOU54F:015-pact-http-producer-php:ffi-next + + +Examples of Additional Features now possible + +- Pact Plugins + - CSV https://github.com/tienvx/pact-php-csv + - Protobuf/gRPC https://github.com/tienvx/pact-php-protobuf \ No newline at end of file diff --git a/UPGRADE-9.0.md b/UPGRADE-9.0.md deleted file mode 100644 index 1d43ce8e..00000000 --- a/UPGRADE-9.0.md +++ /dev/null @@ -1,72 +0,0 @@ -UPGRADE FROM 8.x to 9.0 -======================= - -* Environment Variables - * These environment variables can be removed: - * PACT_CORS - * PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT - * PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC - -* Verifier - * Different pacts sources can be configured via `addXxx` methods - - Example Usage: - ```php - $config = new VerifierConfig(); - $config - ->setPort(8000) - ->setProviderName('someProvider') - ->setProviderVersion('1.0.0'); - - $url = new Url(); - $url - ->setUrl(new Uri('http://localhost')) - ->setProviderName('someProvider') - ->setUsername('user') - ->setPassword('pass') - ->setToken('token'); - - $selectors = (new ConsumerVersionSelectors()) - ->addSelector('{"tag":"foo","latest":true}') - ->addSelector('{"tag":"bar","latest":true}'); - - $broker = new Broker(); - $broker - ->setUrl(new Uri('http://localhost')) - ->setProviderName('someProvider') - ->setUsername('user') - ->setPassword('pass') - ->setToken('token') - ->setConsumerVersionSelectors($selectors); - - $verifier = new Verifier($config); - $verifier - ->addFile('C:\SomePath\consumer-provider.json'); - ->addDirectory('C:\OtherPath'); - ->addUrl($url); - ->addBroker($broker); - - $verifyResult = $verifier->verify(); - - $this->assertTrue($verifyResult); - ``` - -* Consumer - * Pact file write mode has been changed from 'overwrite' to 'merge'. Make sure old pact files are removed before running tests. - - ```shell - rm /path/to/pacts/*.json - ``` - - * Pact files now can ONLY be uploaded to Pact Broker by downloading and running Pact CLI manually. - - ```shell - pact-broker publish /path/to/pacts/*.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken - ``` - -* Stub Server - * Endpoint now can be set by: - ```php - $service = new StubServerHttpService(new GuzzleClient(), $this->config); - $service->getJson($endpoint); - ``` From f7f3407f79cef73c6eb9810eccc3ba1a9e24fdfb Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 25 Jul 2023 21:52:07 +0700 Subject: [PATCH 065/298] chore: Update pact ffi library to 0.4.6 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0770643b..1e99e79a 100644 --- a/composer.json +++ b/composer.json @@ -72,12 +72,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.4", + "version": "0.4.6", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.4", + "version": "0.4.6", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From c1a2926c872775dcf52c4699e6cfd0b939912240 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 26 Jul 2023 00:23:05 +0700 Subject: [PATCH 066/298] chore: Replace atLeastAndMostLike by arrayLike --- src/PhpPact/Consumer/Matcher/Matcher.php | 17 +++---- .../PhpPact/Consumer/Matcher/MatcherTest.php | 45 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index a8da98f2..91fdfc1a 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -93,23 +93,24 @@ public function atMostLike(mixed $value, int $max): array } /** - * @param mixed $value example of what the expected data would be - * @param int $min minimum number of objects to verify against + * @param array $values example of what the expected data would be + * @param int $min minimum number of objects to verify against + * @param int $min maximum number of objects to verify against * * @return array */ - public function atLeastAndMostLike(mixed $value, int $min, int $max): array + public function arrayLike(array $values, ?int $min = null, ?int $max = null): array { - if ($min <= 0 || $min > $max) { + if (($min !== null && $min <= 0) || ($min !== null && $max !== null && $min > $max)) { throw new Exception('Invalid minimum number of elements'); } return [ - 'value' => array_fill(0, $min, $value), + 'value' => $values, 'pact:matcher:type' => 'type', - 'min' => $min, - 'max' => $max, - ]; + ] + + ($min === null ? [] : ['min' => $min]) + + ($max === null ? [] : ['max' => $max]); } /** diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 39b0e168..9c1fe35b 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -134,36 +134,45 @@ public function testAtMostLike(object|array $value) /** * @throws Exception */ - public function testAtLeastAndMostLikeInvalidMin() + public function testArrayLikeNegativeMin() { $this->expectException(Exception::class); $this->expectExceptionMessage('Invalid minimum number of elements'); - $this->matcher->atLeastAndMostLike('text', 10, 1); + $this->matcher->arrayLike(['text'], -2, 9); } /** - * @dataProvider dataProviderForEachLikeTest + * @throws Exception */ - public function testAtLeastAndMostLike(object|array $value) + public function testArrayLikeMinLargerThanMax() { - $eachValueMatcher = [ - 'value1' => [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ], - 'value2' => 2, + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid minimum number of elements'); + $this->matcher->arrayLike(['text'], 10, 1); + } + + public function dataProviderForArrayLikeTest() + { + return [ + [null, null, []], + [2, null, ['min' => 2]], + [null, 5, ['max' => 5]], + [3, 8, ['min' => 3, 'max' => 8]], ]; + } + + /** + * @dataProvider dataProviderForArrayLikeTest + */ + public function testArrayLike(?int $min, ?int $max, array $expectedMinMax) + { + $values = ['test1', 'test2']; $expected = \json_encode([ - 'value' => [ - $eachValueMatcher, - $eachValueMatcher, - ], + 'value' => $values, 'pact:matcher:type' => 'type', - 'min' => 2, - 'max' => 4, - ]); + ] + $expectedMinMax); - $actual = \json_encode($this->matcher->atLeastAndMostLike($value, 2, 4)); + $actual = \json_encode($this->matcher->arrayLike($values, $min, $max)); $this->assertEquals($expected, $actual); } From 0b002bf07deaca01c7d2731e90810a232d81f22d Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 26 Jul 2023 01:14:33 +0700 Subject: [PATCH 067/298] chore: Define array's element type for parameter $values --- src/PhpPact/Consumer/Matcher/Matcher.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 91fdfc1a..4b0ed3fb 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -93,9 +93,9 @@ public function atMostLike(mixed $value, int $max): array } /** - * @param array $values example of what the expected data would be - * @param int $min minimum number of objects to verify against - * @param int $min maximum number of objects to verify against + * @param array $values example of what the expected data would be + * @param int $min minimum number of objects to verify against + * @param int $min maximum number of objects to verify against * * @return array */ From ec0c70aa1f833297ff9f03fcb00ca279385d2d6f Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 00:20:42 +0700 Subject: [PATCH 068/298] chore: Replace arrayLike by constrainedArrayLike --- src/PhpPact/Consumer/Matcher/Matcher.php | 40 ++++++++++------ .../PhpPact/Consumer/Matcher/MatcherTest.php | 48 ++++++++++--------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 4b0ed3fb..0fbbf393 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -93,24 +93,36 @@ public function atMostLike(mixed $value, int $max): array } /** - * @param array $values example of what the expected data would be - * @param int $min minimum number of objects to verify against - * @param int $min maximum number of objects to verify against - * - * @return array - */ - public function arrayLike(array $values, ?int $min = null, ?int $max = null): array - { - if (($min !== null && $min <= 0) || ($min !== null && $max !== null && $min > $max)) { - throw new Exception('Invalid minimum number of elements'); + * @param mixed $value example of what the expected data would be + * @param int $min minimum number of objects to verify against + * @param int $max maximum number of objects to verify against + * @param int|null $count number of examples to generate, defaults to one + * + * @return array + */ + public function constrainedArrayLike(mixed $value, int $min = null, int $max = null, ?int $count = null): array + { + $elements = $count ?? $min; + if ($count !== null) { + if ($count < $min) { + throw new Exception( + "constrainedArrayLike has a minimum of {$min} but {$count} elements where requested." . + ' Make sure the count is greater than or equal to the min.' + ); + } elseif ($count > $max) { + throw new Exception( + "constrainedArrayLike has a maximum of {$max} but {$count} elements where requested." . + ' Make sure the count is less than or equal to the max.' + ); + } } return [ - 'value' => $values, + 'min' => $min, + 'max' => $max, 'pact:matcher:type' => 'type', - ] + - ($min === null ? [] : ['min' => $min]) + - ($max === null ? [] : ['max' => $max]); + 'value' => array_fill(0, $elements, $value), + ]; } /** diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 9c1fe35b..f5ab12eb 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -134,45 +134,49 @@ public function testAtMostLike(object|array $value) /** * @throws Exception */ - public function testArrayLikeNegativeMin() + public function testConstrainedArrayLikeCountLessThanMin() { $this->expectException(Exception::class); - $this->expectExceptionMessage('Invalid minimum number of elements'); - $this->matcher->arrayLike(['text'], -2, 9); + $this->expectExceptionMessage('constrainedArrayLike has a minimum of 2 but 1 elements where requested.' . + ' Make sure the count is greater than or equal to the min.'); + $this->matcher->constrainedArrayLike('text', 2, 4, 1); } /** * @throws Exception */ - public function testArrayLikeMinLargerThanMax() + public function testConstrainedArrayLikeCountLargerThanMax() { $this->expectException(Exception::class); - $this->expectExceptionMessage('Invalid minimum number of elements'); - $this->matcher->arrayLike(['text'], 10, 1); - } - - public function dataProviderForArrayLikeTest() - { - return [ - [null, null, []], - [2, null, ['min' => 2]], - [null, 5, ['max' => 5]], - [3, 8, ['min' => 3, 'max' => 8]], - ]; + $this->expectExceptionMessage('constrainedArrayLike has a maximum of 5 but 7 elements where requested.' . + ' Make sure the count is less than or equal to the max.'); + $this->matcher->constrainedArrayLike('text', 3, 5, 7); } /** - * @dataProvider dataProviderForArrayLikeTest + * @dataProvider dataProviderForEachLikeTest */ - public function testArrayLike(?int $min, ?int $max, array $expectedMinMax) + public function testConstrainedArrayLike(object|array $value) { - $values = ['test1', 'test2']; + $eachValueMatcher = [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ]; $expected = \json_encode([ - 'value' => $values, + 'min' => 2, + 'max' => 4, 'pact:matcher:type' => 'type', - ] + $expectedMinMax); + 'value' => [ + $eachValueMatcher, + $eachValueMatcher, + $eachValueMatcher, + ], + ]); - $actual = \json_encode($this->matcher->arrayLike($values, $min, $max)); + $actual = \json_encode($this->matcher->constrainedArrayLike($value, 2, 4, 3)); $this->assertEquals($expected, $actual); } From 25778072112236fd0c2b6858b8eaaf5a977b567c Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 01:18:02 +0700 Subject: [PATCH 069/298] chore: Disable process timeout --- example/tests/Provider/PactVerifyTest.php | 2 +- tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index 4f61e93b..ec0aa712 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -25,7 +25,7 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../src/Provider/public/'; - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); $this->process->start(); $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index ba3fb34c..6a1e1138 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -19,7 +19,7 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../../_public/'; - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); $this->process->start(); $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); From 8882d61b27f79da4fd7ed737dd72401bf4d92a5c Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 08:01:33 +0700 Subject: [PATCH 070/298] chore: Closing socket connection after opening it --- example/tests/Provider/PactVerifyTest.php | 10 +++++++++- .../Standalone/ProviderVerifier/VerifierTest.php | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index ec0aa712..39f7141b 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -28,7 +28,15 @@ protected function setUp(): void $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); $this->process->start(); - $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); } /** diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 6a1e1138..8ce92189 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -22,7 +22,15 @@ protected function setUp(): void $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); $this->process->start(); - $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); } /** From ef33b348152be48ca59160d5cdb0bb884794febc Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 08:14:46 +0700 Subject: [PATCH 071/298] Revert "chore: Disable process timeout" This reverts commit 25778072112236fd0c2b6858b8eaaf5a977b567c. --- example/tests/Provider/PactVerifyTest.php | 2 +- tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index 39f7141b..e36e0b75 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -25,7 +25,7 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../src/Provider/public/'; - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); $this->process->start(); $this->process->waitUntil(function (): bool { diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 8ce92189..245d60a5 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -19,7 +19,7 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../../_public/'; - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath], null, null, null, null); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); $this->process->start(); $this->process->waitUntil(function (): bool { From 1cf3b76a3b2e46676ec75fd8cd73da108b2e1183 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 08:37:43 +0700 Subject: [PATCH 072/298] chore: Update ffi library --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1e99e79a..373592e1 100644 --- a/composer.json +++ b/composer.json @@ -72,12 +72,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.6", + "version": "0.4.7", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.6", + "version": "0.4.7", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From 7decbfd68e23060829bafdba60f9aa36233e2536 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 29 Jul 2023 12:28:08 +0700 Subject: [PATCH 073/298] feat: Use auto-installing plugins feature --- .../Model/Config/PluginDirTrait.php | 20 ------------------- .../ProviderVerifier/Model/VerifierConfig.php | 2 -- .../Model/VerifierConfigInterface.php | 4 ---- .../Standalone/ProviderVerifier/Verifier.php | 12 +---------- 4 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Config/PluginDirTrait.php diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PluginDirTrait.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PluginDirTrait.php deleted file mode 100644 index de8f51c6..00000000 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PluginDirTrait.php +++ /dev/null @@ -1,20 +0,0 @@ -pluginDir; - } - - public function setPluginDir(?string $pluginDir): self - { - $this->pluginDir = $pluginDir; - - return $this; - } -} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php index e9355d8c..209d9c1b 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php @@ -9,7 +9,6 @@ use PhpPact\Standalone\ProviderVerifier\Model\Config\ConsumerFiltersInterface; use PhpPact\Standalone\ProviderVerifier\Model\Config\FilterInfo; use PhpPact\Standalone\ProviderVerifier\Model\Config\FilterInfoInterface; -use PhpPact\Standalone\ProviderVerifier\Model\Config\PluginDirTrait; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderInfo; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderInfoInterface; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderState; @@ -22,7 +21,6 @@ class VerifierConfig implements VerifierConfigInterface { use LogLevelTrait; - use PluginDirTrait; private CallingAppInterface $callingApp; private ProviderInfoInterface $providerInfo; diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php index 6f13cb83..bad14ed8 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php @@ -58,8 +58,4 @@ public function getVerificationOptions(): VerificationOptionsInterface; public function getLogLevel(): ?string; public function setLogLevel(string $logLevel): self; - - public function getPluginDir(): ?string; - - public function setPluginDir(?string $pluginDir): self; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index d5b6d900..0188a7ba 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -27,8 +27,7 @@ public function __construct(VerifierConfigInterface $config) ->setVerificationOptions($config) ->setPublishOptions($config) ->setConsumerFilters($config) - ->setLogLevel($config) - ->setPluginDir($config); + ->setLogLevel($config); } private function newHandle(VerifierConfigInterface $config): self @@ -151,15 +150,6 @@ private function setLogLevel(VerifierConfigInterface $config): self return $this; } - private function setPluginDir(VerifierConfigInterface $config): self - { - if ($pluginDir = $config->getPluginDir()) { - \putenv("PACT_PLUGIN_DIR={$pluginDir}"); - } - - return $this; - } - public function addFile(string $file): self { $this->client->call('pactffi_verifier_add_file_source', $this->handle, $file); From 31120f1e78e3811f611e9b003569af63ebfa9cc4 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 1 Aug 2023 20:28:56 +0700 Subject: [PATCH 074/298] fix: Min and max are not nullable --- src/PhpPact/Consumer/Matcher/Matcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 0fbbf393..81f04f5b 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -100,7 +100,7 @@ public function atMostLike(mixed $value, int $max): array * * @return array */ - public function constrainedArrayLike(mixed $value, int $min = null, int $max = null, ?int $count = null): array + public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $count = null): array { $elements = $count ?? $min; if ($count !== null) { From 085d68c0fcfebc3fc7856cc8e63519958884b405 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 1 Aug 2023 20:29:31 +0700 Subject: [PATCH 075/298] chore: Add constrainedArrayLike matcher to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 34e0513d..cef1868c 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ timestampRFC3339 | Regex match a timestamp using the RFC3339 format. | Value (De like | Match a value against its data type. | Value | $matcher->like(12) somethingLike | Alias to like matcher. | Value | $matcher->somethingLike(12) eachLike | Match on an object like the example. | Value, Min (Defaults to 1) | $matcher->eachLike(12) +constrainedArrayLike | Behaves like the `eachLike` matcher, but also applies a minimum and maximum length validation on the length of the array. The optional `count` parameter controls the number of examples generated. | Value, Min, Max, count (Defaults to null) | $matcher->constrainedArrayLike('test', 1, 5, 3) boolean | Match against boolean true. | none | $matcher->boolean() integer | Match a value against integer. | Value (Defaults to 13) | $matcher->integer() decimal | Match a value against float. | Value (Defaults to 13.01) | $matcher->decimal() From 3d8a05526f2d25d20f8515125524edad2b89a18e Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 19 Aug 2023 01:05:21 +0700 Subject: [PATCH 076/298] chore: reorganize examples (create separate directory for each example) --- composer.json | 27 +++----- example/README.md | 24 ------- example/json/consumer/phpunit.xml | 11 +++ .../src}/Service/HttpClientService.php | 2 +- .../Service/ConsumerServiceGoodbyeTest.php | 20 +++--- .../Service/ConsumerServiceHelloTest.php | 14 +++- .../pacts/jsonConsumer-jsonProvider.json} | 10 +-- example/json/provider/phpunit.xml | 11 +++ .../provider}/public/index.php | 16 +---- example/json/provider/src/ExampleProvider.php | 27 ++++++++ .../json/provider/tests/PactVerifyTest.php | 68 +++++++++++++++++++ example/message/consumer/phpunit.xml | 11 +++ .../consumer/src}/ExampleMessageConsumer.php | 0 .../consumer/src}/receive.php | 2 +- .../tests}/ExampleMessageConsumerTest.php | 19 +++--- .../messageConsumer-messageProvider.json} | 8 +-- example/message/provider/phpunit.xml | 11 +++ example/message/provider/public/index.php | 36 ++++++++++ .../provider/src}/ExampleMessageProvider.php | 0 .../provider/src}/ExampleProvider.php | 12 +--- .../provider/src}/send.php | 2 +- .../provider/tests}/PactVerifyTest.php | 15 ++-- example/pacts/README.md | 11 --- example/phpunit.all.xml | 22 ------ example/phpunit.consumer.xml | 13 ---- example/phpunit.core.xml | 13 ---- example/phpunit.message.consumer.xml | 8 --- example/phpunit.provider.xml | 8 --- phpunit.xml | 16 ++++- src/PhpPact/FFI/Model/ArrayData.php | 2 +- 30 files changed, 251 insertions(+), 188 deletions(-) delete mode 100644 example/README.md create mode 100644 example/json/consumer/phpunit.xml rename example/{src/Consumer => json/consumer/src}/Service/HttpClientService.php (97%) rename example/{tests/Consumer => json/consumer/tests}/Service/ConsumerServiceGoodbyeTest.php (70%) rename example/{tests/Consumer => json/consumer/tests}/Service/ConsumerServiceHelloTest.php (81%) rename example/{pacts/someconsumer-someprovider.json => json/pacts/jsonConsumer-jsonProvider.json} (91%) create mode 100644 example/json/provider/phpunit.xml rename example/{src/Provider => json/provider}/public/index.php (66%) create mode 100644 example/json/provider/src/ExampleProvider.php create mode 100644 example/json/provider/tests/PactVerifyTest.php create mode 100644 example/message/consumer/phpunit.xml rename example/{src/MessageConsumer => message/consumer/src}/ExampleMessageConsumer.php (100%) rename example/{src/MessageConsumer => message/consumer/src}/receive.php (93%) rename example/{tests/MessageConsumer => message/consumer/tests}/ExampleMessageConsumerTest.php (77%) rename example/{pacts/test_consumer-test_provider.json => message/pacts/messageConsumer-messageProvider.json} (90%) create mode 100644 example/message/provider/phpunit.xml create mode 100644 example/message/provider/public/index.php rename example/{src/MessageProvider => message/provider/src}/ExampleMessageProvider.php (100%) rename example/{src/Provider => message/provider/src}/ExampleProvider.php (87%) rename example/{src/MessageProvider => message/provider/src}/send.php (94%) rename example/{tests/Provider => message/provider/tests}/PactVerifyTest.php (75%) delete mode 100644 example/pacts/README.md delete mode 100644 example/phpunit.all.xml delete mode 100644 example/phpunit.consumer.xml delete mode 100644 example/phpunit.core.xml delete mode 100644 example/phpunit.message.consumer.xml delete mode 100644 example/phpunit.provider.xml diff --git a/composer.json b/composer.json index 373592e1..f506240c 100644 --- a/composer.json +++ b/composer.json @@ -42,21 +42,14 @@ "autoload-dev": { "psr-4": { "PhpPactTest\\": "tests/PhpPact", - "Consumer\\": [ - "example/src/Consumer", - "example/tests/Consumer" - ], - "MessageConsumer\\": [ - "example/src/MessageConsumer", - "example/tests/MessageConsumer" - ], - "MessageProvider\\": [ - "example/src/MessageProvider", - "example/tests/MessageProvider" - ], - "Provider\\": [ - "example/src/Provider" - ] + "JsonConsumer\\": "example/json/consumer/src", + "JsonConsumer\\Tests\\": "example/json/consumer/tests", + "JsonProvider\\": "example/json/provider/src", + "JsonProvider\\Tests\\": "example/json/provider/tests", + "MessageConsumer\\": "example/message/consumer/src", + "MessageConsumer\\Tests\\": "example/message/consumer/tests", + "MessageProvider\\": "example/message/provider/src", + "MessageProvider\\Tests\\": "example/message/provider/tests" } }, "scripts": { @@ -65,8 +58,8 @@ "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", "test": [ - "php -r \"array_map('unlink', glob('./example/output/*.json'));\"", - "phpunit --debug -c example/phpunit.all.xml" + "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", + "phpunit --debug" ] }, "extra": { diff --git a/example/README.md b/example/README.md deleted file mode 100644 index ab66a43b..00000000 --- a/example/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Pact PHP Usage examples - -This folder contains some integration tests which demonstrate the functionality of `pact-php`. -All examples could be run within tests. - -## Consumer Tests - - docker-compose up -d - vendor/bin/phpunit -c example/phpunit.consumer.xml - docker-compose down - -## Provider Verification Tests - - vendor/bin/phpunit -c example/phpunit.provider.xml - -## Consumer Tests for Message Processing - - vendor/bin/phpunit -c example/phpunit.message.consumer.xml - -## All tests together - - docker-compose up -d - vendor/bin/phpunit -c example/phpunit.all.xml - docker-compose down diff --git a/example/json/consumer/phpunit.xml b/example/json/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/json/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/src/Consumer/Service/HttpClientService.php b/example/json/consumer/src/Service/HttpClientService.php similarity index 97% rename from example/src/Consumer/Service/HttpClientService.php rename to example/json/consumer/src/Service/HttpClientService.php index 85ab9c53..93831973 100644 --- a/example/src/Consumer/Service/HttpClientService.php +++ b/example/json/consumer/src/Service/HttpClientService.php @@ -1,6 +1,6 @@ 'Goodbye, Bob' ]); - $config = new MockServerEnvConfig(); - $builder = new InteractionBuilder($config); + $config = new MockServerConfig(); + $config + ->setConsumer('jsonConsumer') + ->setProvider('jsonProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); $builder ->given('Get Goodbye') ->uponReceiving('A get request to /goodbye/{name}') diff --git a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php b/example/json/consumer/tests/Service/ConsumerServiceHelloTest.php similarity index 81% rename from example/tests/Consumer/Service/ConsumerServiceHelloTest.php rename to example/json/consumer/tests/Service/ConsumerServiceHelloTest.php index d10782ff..605a3fb6 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/json/consumer/tests/Service/ConsumerServiceHelloTest.php @@ -1,12 +1,13 @@ setConsumer('jsonConsumer') + ->setProvider('jsonProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } $builder = new InteractionBuilder($config); $builder ->uponReceiving('A get request to /hello/{name}') diff --git a/example/pacts/someconsumer-someprovider.json b/example/json/pacts/jsonConsumer-jsonProvider.json similarity index 91% rename from example/pacts/someconsumer-someprovider.json rename to example/json/pacts/jsonConsumer-jsonProvider.json index 2d917286..709eefa4 100644 --- a/example/pacts/someconsumer-someprovider.json +++ b/example/json/pacts/jsonConsumer-jsonProvider.json @@ -1,6 +1,6 @@ { "consumer": { - "name": "someConsumer" + "name": "jsonConsumer" }, "interactions": [ { @@ -63,15 +63,15 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.4", - "mockserver": "1.0.3", - "models": "1.0.13" + "ffi": "0.4.7", + "mockserver": "1.2.3", + "models": "1.1.9" }, "pactSpecification": { "version": "3.0.0" } }, "provider": { - "name": "someProvider" + "name": "jsonProvider" } } \ No newline at end of file diff --git a/example/json/provider/phpunit.xml b/example/json/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/json/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/src/Provider/public/index.php b/example/json/provider/public/index.php similarity index 66% rename from example/src/Provider/public/index.php rename to example/json/provider/public/index.php index 48e8a188..d4c57891 100644 --- a/example/src/Provider/public/index.php +++ b/example/json/provider/public/index.php @@ -1,6 +1,6 @@ withHeader('Content-Type', 'application/json'); }); -$app->post('/pact-messages', function (Request $request, Response $response) use ($provider) { - $body = $request->getParsedBody(); - $message = $provider->dispatchMessage($body['description'], $body['providerStates']); - if ($message) { - $response->getBody()->write(\json_encode($message->getContents())); - - return $response - ->withHeader('Content-Type', 'application/json') - ->withHeader('Pact-Message-Metadata', \base64_encode(\json_encode($message->getMetadata()))); - } - - return $response; -}); - $app->post('/pact-change-state', function (Request $request, Response $response) use ($provider) { $body = $request->getParsedBody(); $provider->changeSate($body['action'], $body['state'], $body['params']); diff --git a/example/json/provider/src/ExampleProvider.php b/example/json/provider/src/ExampleProvider.php new file mode 100644 index 00000000..70daec33 --- /dev/null +++ b/example/json/provider/src/ExampleProvider.php @@ -0,0 +1,27 @@ +currentState = [ + 'action' => $action, + 'state' => $state, + 'params' => $params, + ]; + } +} diff --git a/example/json/provider/tests/PactVerifyTest.php b/example/json/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..73dd84c9 --- /dev/null +++ b/example/json/provider/tests/PactVerifyTest.php @@ -0,0 +1,68 @@ +process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); + + $this->process->start(); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + /** + * This test will run after the web server is started. + */ + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('jsonProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/jsonConsumer-jsonProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/example/message/consumer/phpunit.xml b/example/message/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/message/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/src/MessageConsumer/ExampleMessageConsumer.php b/example/message/consumer/src/ExampleMessageConsumer.php similarity index 100% rename from example/src/MessageConsumer/ExampleMessageConsumer.php rename to example/message/consumer/src/ExampleMessageConsumer.php diff --git a/example/src/MessageConsumer/receive.php b/example/message/consumer/src/receive.php similarity index 93% rename from example/src/MessageConsumer/receive.php rename to example/message/consumer/src/receive.php index 66c12a5c..48996111 100644 --- a/example/src/MessageConsumer/receive.php +++ b/example/message/consumer/src/receive.php @@ -1,6 +1,6 @@ setConsumer('test_consumer') - ->setProvider('test_provider') - ->setPactDir(__DIR__ . '/../../output/'); + ->setConsumer('messageConsumer') + ->setProvider('messageProvider') + ->setPactDir(__DIR__.'/../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + self::$config->setLogLevel($logLevel); + } } /** @@ -36,7 +37,7 @@ public function testProcessText() $contents = new stdClass(); $contents->text = 'Hello Mary'; - $metadata = ['queue'=>'wind cries', 'routing_key'=>'wind cries']; + $metadata = ['queue' => 'wind cries', 'routing_key' => 'wind cries']; $builder ->given('a message', ['foo' => 'bar']) @@ -64,7 +65,7 @@ public function testProcessSong() $contents = new stdClass(); $contents->song = 'And the wind whispers Mary'; - $metadata = ['queue'=>'And the clowns have all gone to bed', 'routing_key'=>'And the clowns have all gone to bed']; + $metadata = ['queue' => 'And the clowns have all gone to bed', 'routing_key' => 'And the clowns have all gone to bed']; $builder ->given('You can hear happiness staggering on down the street') diff --git a/example/pacts/test_consumer-test_provider.json b/example/message/pacts/messageConsumer-messageProvider.json similarity index 90% rename from example/pacts/test_consumer-test_provider.json rename to example/message/pacts/messageConsumer-messageProvider.json index 13f684cd..95669646 100644 --- a/example/pacts/test_consumer-test_provider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -1,6 +1,6 @@ { "consumer": { - "name": "test_consumer" + "name": "messageConsumer" }, "messages": [ { @@ -41,14 +41,14 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.4", - "models": "1.0.13" + "ffi": "0.4.7", + "models": "1.1.9" }, "pactSpecification": { "version": "3.0.0" } }, "provider": { - "name": "test_provider" + "name": "messageProvider" } } \ No newline at end of file diff --git a/example/message/provider/phpunit.xml b/example/message/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/message/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/message/provider/public/index.php b/example/message/provider/public/index.php new file mode 100644 index 00000000..9f4ae661 --- /dev/null +++ b/example/message/provider/public/index.php @@ -0,0 +1,36 @@ +addBodyParsingMiddleware(); + +$provider = new ExampleProvider(); + +$app->post('/pact-messages', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $message = $provider->dispatchMessage($body['description'], $body['providerStates']); + if ($message) { + $response->getBody()->write(\json_encode($message->getContents())); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('Pact-Message-Metadata', \base64_encode(\json_encode($message->getMetadata()))); + } + + return $response; +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $provider->changeSate($body['action'], $body['state'], $body['params']); + + return $response; +}); + +$app->run(); diff --git a/example/src/MessageProvider/ExampleMessageProvider.php b/example/message/provider/src/ExampleMessageProvider.php similarity index 100% rename from example/src/MessageProvider/ExampleMessageProvider.php rename to example/message/provider/src/ExampleMessageProvider.php diff --git a/example/src/Provider/ExampleProvider.php b/example/message/provider/src/ExampleProvider.php similarity index 87% rename from example/src/Provider/ExampleProvider.php rename to example/message/provider/src/ExampleProvider.php index c7fddb72..2def3fa5 100644 --- a/example/src/Provider/ExampleProvider.php +++ b/example/message/provider/src/ExampleProvider.php @@ -1,6 +1,6 @@ messages[$description])) { diff --git a/example/src/MessageProvider/send.php b/example/message/provider/src/send.php similarity index 94% rename from example/src/MessageProvider/send.php rename to example/message/provider/src/send.php index e1dbb74d..125db85e 100644 --- a/example/src/MessageProvider/send.php +++ b/example/message/provider/src/send.php @@ -9,7 +9,7 @@ $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); // build the message with appropriate metadata -$providerMessage = new \MessageProvider\ExampleMessageProvider(['queue'=>'myKey', 'routing_key'=>'myKey']); +$providerMessage = new \MessageProvider\ExampleMessageProvider(['queue' => 'myKey', 'routing_key' => 'myKey']); $content = new \stdClass(); $content->text = 'Hello Mary'; $providerMessage->setContents($content); diff --git a/example/tests/Provider/PactVerifyTest.php b/example/message/provider/tests/PactVerifyTest.php similarity index 75% rename from example/tests/Provider/PactVerifyTest.php rename to example/message/provider/tests/PactVerifyTest.php index e36e0b75..f0c459a1 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/message/provider/tests/PactVerifyTest.php @@ -1,6 +1,6 @@ process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); @@ -54,7 +49,7 @@ public function testPactVerifyConsumer() { $config = new VerifierConfig(); $config->getProviderInfo() - ->setName('someProvider') // Providers name to fetch. + ->setName('messageProvider') // Providers name to fetch. ->setHost('localhost') ->setPort(7202); $config->getProviderState() @@ -71,10 +66,8 @@ public function testPactVerifyConsumer() $config->setLogLevel($level); } - // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. $verifier = new Verifier($config); - $verifier->addFile(__DIR__ . '/../../pacts/someconsumer-someprovider.json'); - $verifier->addFile(__DIR__ . '/../../pacts/test_consumer-test_provider.json'); + $verifier->addFile(__DIR__ . '/../../pacts/messageConsumer-messageProvider.json'); $verifyResult = $verifier->verify(); diff --git a/example/pacts/README.md b/example/pacts/README.md deleted file mode 100644 index 0d5d2a0b..00000000 --- a/example/pacts/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Example Pacts - -The json files in this folder are explicitly here for an easy-to-read output of the test examples. These are *not* the actual test results from running all these tests of this project. By default, the pact files of this project's examples are written to example/output. The tests themselves need to generate the appropropriate files as part of the tests. - -To run the tests locally, try `composer test` - - - - - - diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml deleted file mode 100644 index d3f50c70..00000000 --- a/example/phpunit.all.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - ../tests - - - ./tests/Consumer - - - ./tests/Provider - - - ./tests/MessageConsumer - - - - - - - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml deleted file mode 100644 index 6236e64d..00000000 --- a/example/phpunit.consumer.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - ./tests/Consumer - - - - - - - - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml deleted file mode 100644 index 6cc7fc8b..00000000 --- a/example/phpunit.core.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - ../tests - - - - - - - - diff --git a/example/phpunit.message.consumer.xml b/example/phpunit.message.consumer.xml deleted file mode 100644 index 7170d67e..00000000 --- a/example/phpunit.message.consumer.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ./tests/MessageConsumer - - - diff --git a/example/phpunit.provider.xml b/example/phpunit.provider.xml deleted file mode 100644 index 8f143865..00000000 --- a/example/phpunit.provider.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ./tests/Provider - - - diff --git a/phpunit.xml b/phpunit.xml index 0a7db8ac..ff6fe126 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,8 +10,20 @@ - - ./tests/PhpPact + + ./tests + + + ./example/json/consumer/tests + + + ./example/json/provider/tests + + + ./example/message/consumer/tests + + + ./example/message/provider/tests diff --git a/src/PhpPact/FFI/Model/ArrayData.php b/src/PhpPact/FFI/Model/ArrayData.php index cec3fb75..8945b2d1 100644 --- a/src/PhpPact/FFI/Model/ArrayData.php +++ b/src/PhpPact/FFI/Model/ArrayData.php @@ -54,7 +54,7 @@ public static function createFrom(array $values): ?self public function __destruct() { - for ($i=0; $i < $this->size; $i++) { + for ($i = 0; $i < $this->size; $i++) { FFI::free($this->items[$i]); // @phpstan-ignore-line } } From 9c30bfbb9398056f631a3b1a3a77a44154948e11 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 20 Aug 2023 17:28:29 +0700 Subject: [PATCH 077/298] feat: Support http request binary body --- .cirrus.yml | 4 +- composer.json | 6 +- example/binary/consumer/phpunit.xml | 11 ++++ .../src/Service/HttpClientService.php | 31 +++++++++ .../tests/Service/HttpClientServiceTest.php | 53 +++++++++++++++ .../binary/consumer/tests/_resource/image.jpg | Bin 0 -> 298 bytes .../pacts/binaryConsumer-binaryProvider.json | 56 ++++++++++++++++ example/binary/provider/phpunit.xml | 11 ++++ example/binary/provider/public/image.jpg | Bin 0 -> 357 bytes .../binary/provider/tests/PactVerifyTest.php | 62 ++++++++++++++++++ .../json/provider/tests/PactVerifyTest.php | 4 +- phpunit.xml | 6 ++ .../Consumer/AbstractMessageBuilder.php | 7 -- .../MessageContentsNotAddedException.php | 9 +++ src/PhpPact/Consumer/Model/Body/Binary.php | 30 +++++++++ .../Consumer/Model/Body/ContentTypeTrait.php | 20 ++++++ src/PhpPact/Consumer/Model/Body/Text.php | 28 ++++++++ .../Consumer/Model/ConsumerRequest.php | 2 - .../Consumer/Model/Interaction/BodyTrait.php | 21 +++--- .../Model/Interaction/ContentTypeTrait.php | 20 ------ src/PhpPact/Consumer/Model/Message.php | 17 +++-- .../Consumer/Model/ProviderResponse.php | 2 - .../Interaction/Body/AbstractBodyRegistry.php | 32 +++++++++ .../Body/BodyRegistryInterface.php | 11 ++++ .../Body/MessageContentsRegistry.php | 33 ++++++++++ .../RequestBodyRegistry.php | 2 +- .../ResponseBodyRegistry.php | 2 +- .../Contents/AbstractBodyRegistry.php | 29 -------- .../Contents/ContentsRegistryInterface.php | 8 --- .../Contents/MessageContentsRegistry.php | 25 ------- .../Interaction/InteractionRegistry.php | 4 +- .../Registry/Interaction/MessageRegistry.php | 18 +++-- .../Interaction/Part/AbstractPartRegistry.php | 12 ++-- .../Part/PartRegistryInterface.php | 5 +- .../Interaction/Part/RequestRegistry.php | 6 +- .../Interaction/Part/ResponseRegistry.php | 6 +- src/PhpPact/FFI/Model/StringData.php | 4 +- .../Consumer/Model/ConsumerRequestTest.php | 13 +++- tests/PhpPact/Consumer/Model/MessageTest.php | 7 +- .../Consumer/Model/ProviderResponseTest.php | 7 +- tests/PhpPact/FFI/Model/StringDataTest.php | 43 ++++++++++++ .../ProviderVerifier/VerifierTest.php | 2 +- .../StubServer/StubServerConfigTest.php | 2 +- tests/_resources/image.jpg | Bin 0 -> 277 bytes 44 files changed, 521 insertions(+), 150 deletions(-) create mode 100644 example/binary/consumer/phpunit.xml create mode 100644 example/binary/consumer/src/Service/HttpClientService.php create mode 100644 example/binary/consumer/tests/Service/HttpClientServiceTest.php create mode 100644 example/binary/consumer/tests/_resource/image.jpg create mode 100644 example/binary/pacts/binaryConsumer-binaryProvider.json create mode 100644 example/binary/provider/phpunit.xml create mode 100644 example/binary/provider/public/image.jpg create mode 100644 example/binary/provider/tests/PactVerifyTest.php create mode 100644 src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php create mode 100644 src/PhpPact/Consumer/Model/Body/Binary.php create mode 100644 src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php create mode 100644 src/PhpPact/Consumer/Model/Body/Text.php delete mode 100644 src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php create mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php rename src/PhpPact/Consumer/Registry/Interaction/{Contents => Body}/RequestBodyRegistry.php (73%) rename src/PhpPact/Consumer/Registry/Interaction/{Contents => Body}/ResponseBodyRegistry.php (73%) delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/AbstractBodyRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Contents/MessageContentsRegistry.php create mode 100644 tests/PhpPact/FFI/Model/StringDataTest.php create mode 100644 tests/_resources/image.jpg diff --git a/.cirrus.yml b/.cirrus.yml index dde4e49c..80abe231 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -27,7 +27,7 @@ linux_arm64_task: container: image: php:$VERSION pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev + - apt update --yes && apt install --yes zip unzip git libffi-dev shared-mime-info - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - docker-php-ext-install sockets @@ -46,7 +46,7 @@ macos_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest pre_req_script: - - brew install php@$VERSION composer + - brew install php@$VERSION composer shared-mime-info version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/composer.json b/composer.json index f506240c..e558f038 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,11 @@ "MessageConsumer\\": "example/message/consumer/src", "MessageConsumer\\Tests\\": "example/message/consumer/tests", "MessageProvider\\": "example/message/provider/src", - "MessageProvider\\Tests\\": "example/message/provider/tests" + "MessageProvider\\Tests\\": "example/message/provider/tests", + "BinaryConsumer\\": "example/binary/consumer/src", + "BinaryConsumer\\Tests\\": "example/binary/consumer/tests", + "BinaryProvider\\": "example/binary/provider/src", + "BinaryProvider\\Tests\\": "example/binary/provider/tests" } }, "scripts": { diff --git a/example/binary/consumer/phpunit.xml b/example/binary/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/binary/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/binary/consumer/src/Service/HttpClientService.php b/example/binary/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..3f8d22a7 --- /dev/null +++ b/example/binary/consumer/src/Service/HttpClientService.php @@ -0,0 +1,31 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getImageContent(): string + { + $response = $this->httpClient->get(new Uri("{$this->baseUri}/image.jpg"), [ + 'headers' => ['Accept' => 'image/jpeg'] + ]); + + return $response->getBody(); + } +} diff --git a/example/binary/consumer/tests/Service/HttpClientServiceTest.php b/example/binary/consumer/tests/Service/HttpClientServiceTest.php new file mode 100644 index 00000000..42fe6b82 --- /dev/null +++ b/example/binary/consumer/tests/Service/HttpClientServiceTest.php @@ -0,0 +1,53 @@ +setMethod('GET') + ->setPath('/image.jpg') + ->addHeader('Accept', 'image/jpeg'); + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->addHeader('Content-Type', 'image/jpeg') + ->setBody(new Binary($imageContent, in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg')); + + $config = new MockServerConfig(); + $config + ->setConsumer('binaryConsumer') + ->setProvider('binaryProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('Image file image.jpg exists') + ->uponReceiving('A get request to /image.jpg') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $imageContentResult = $service->getImageContent(); + $verifyResult = $builder->verify(); + + $this->assertTrue($verifyResult); + $this->assertEquals($imageContent, $imageContentResult); + } +} diff --git a/example/binary/consumer/tests/_resource/image.jpg b/example/binary/consumer/tests/_resource/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e93db1c2619f26d7aaa7787c4996ddee0307c53 GIT binary patch literal 298 zcmex=>ukC3pCfH06P@c#efIW!|kTKdi5SVJYhCZcv@iaYT4sQf}0}pldpM9*qj@DMJS`% RmaQ+PcoV-*eM`;%n*cniP@4b% literal 0 HcmV?d00001 diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json new file mode 100644 index 00000000..7ecf427f --- /dev/null +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -0,0 +1,56 @@ +{ + "consumer": { + "name": "binaryConsumer" + }, + "interactions": [ + { + "description": "A get request to /image.jpg", + "providerStates": [ + { + "name": "Image file image.jpg exists" + } + ], + "request": { + "headers": { + "Accept": "image/jpeg" + }, + "method": "GET", + "path": "/image.jpg" + }, + "response": { + "body": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAPAA8BAREA/8QAFgABAQEAAAAAAAAAAAAAAAAAAgME/8QAIBAAAgIDAAIDAQAAAAAAAAAAAQIDBAUREhMhACIxUf/aAAgBAQAAPwBNlclNW6S0s7wbvv4K6iNI2HJcp9Rrnk+T0ByNofwZIjdnyYx0stWrYWAP5y03SBjt+GQ/YnmHbaUkfoA107Br40Wa+DrRLNXhjchECDwyjjlXPpdFSo3Gw5CbDFdmUVOrHcfEU4JYb2PWSJCzbVPUEmh7PQaOZHOyD0x/hHz/2Q==", + "headers": { + "Content-Type": "image/jpeg" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "contentType", + "value": "image/jpeg" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.7", + "mockserver": "1.2.3", + "models": "1.1.9" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "binaryProvider" + } +} \ No newline at end of file diff --git a/example/binary/provider/phpunit.xml b/example/binary/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/binary/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/binary/provider/public/image.jpg b/example/binary/provider/public/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5477a6daca246c7125fcd8d1e1fefdfaba56c9cc GIT binary patch literal 357 zcmex=>ukC3pCfH06P@c#e?HWi-7~=JbQ**%`Lya2H4cR zwd|^$bWmwY-s&w!j~F(Y?bQ7gpBnRILc;|Yi$@o??i94s`WU)=W5bv5s%gisJ@T{t z`6BA_YQxUenO~0YJ|zTwkR&am_2kE7S51MNU)Caprocess = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); + + $this->process->start(); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + /** + * This test will run after the web server is started. + */ + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('binaryProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/binaryConsumer-binaryProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/example/json/provider/tests/PactVerifyTest.php b/example/json/provider/tests/PactVerifyTest.php index 73dd84c9..408dade1 100644 --- a/example/json/provider/tests/PactVerifyTest.php +++ b/example/json/provider/tests/PactVerifyTest.php @@ -17,9 +17,7 @@ class PactVerifyTest extends TestCase */ protected function setUp(): void { - $publicPath = __DIR__ . '/../public/'; - - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); $this->process->start(); $this->process->waitUntil(function (): bool { diff --git a/phpunit.xml b/phpunit.xml index ff6fe126..3c2344a0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -19,6 +19,12 @@ ./example/json/provider/tests + + ./example/binary/consumer/tests + + + ./example/binary/provider/tests + ./example/message/consumer/tests diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index d25a743c..71a23275 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -56,11 +56,4 @@ public function withContent(mixed $contents): self return $this; } - - public function withContentType(?string $contentType): self - { - $this->message->setContentType($contentType); - - return $this; - } } diff --git a/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php b/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php new file mode 100644 index 00000000..4cd40881 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php @@ -0,0 +1,9 @@ +setContents(StringData::createFrom($contents, false)); + $this->setContentType($contentType); + } + + public function getContents(): StringData + { + return $this->contents; + } + + public function setContents(StringData $contents): self + { + $this->contents = $contents; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php new file mode 100644 index 00000000..41acc2cf --- /dev/null +++ b/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php @@ -0,0 +1,20 @@ +contentType; + } + + public function setContentType(string $contentType): self + { + $this->contentType = $contentType; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Body/Text.php b/src/PhpPact/Consumer/Model/Body/Text.php new file mode 100644 index 00000000..1d2e54f7 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Body/Text.php @@ -0,0 +1,28 @@ +setContents($contents); + $this->setContentType($contentType); + } + + public function getContents(): string + { + return $this->contents; + } + + public function setContents(string $contents): self + { + $this->contents = $contents; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 58d98740..1c0b4c08 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\BodyTrait; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; use PhpPact\Consumer\Model\Interaction\HeadersTrait; use PhpPact\Consumer\Model\Interaction\MethodTrait; use PhpPact\Consumer\Model\Interaction\PathTrait; @@ -16,7 +15,6 @@ class ConsumerRequest { use HeadersTrait; use BodyTrait; - use ContentTypeTrait; use MethodTrait; use PathTrait; use QueryTrait; diff --git a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php index 3a7c6b37..3cb31e7f 100644 --- a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php @@ -3,32 +3,29 @@ namespace PhpPact\Consumer\Model\Interaction; use JsonException; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; trait BodyTrait { - use ContentTypeTrait; + private Text|Binary|null $body = null; - private ?string $body = null; - - public function getBody(): ?string + public function getBody(): Text|Binary|null { return $this->body; } /** - * @param array|string|null $body - * * @throws JsonException */ - public function setBody(array|string|null $body): self + public function setBody(mixed $body): self { - if (\is_string($body) || \is_null($body)) { + if (\is_string($body)) { + $this->body = new Text($body, 'text/plain'); + } elseif (\is_null($body) || $body instanceof Text || $body instanceof Binary) { $this->body = $body; } else { - $this->body = \json_encode($body, JSON_THROW_ON_ERROR); - if (!isset($this->contentType)) { - $this->setContentType('application/json'); - } + $this->body = new Text(\json_encode($body, JSON_THROW_ON_ERROR), 'application/json'); } return $this; diff --git a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php deleted file mode 100644 index 257f6cbe..00000000 --- a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php +++ /dev/null @@ -1,20 +0,0 @@ -contentType; - } - - public function setContentType(?string $contentType): self - { - $this->contentType = $contentType; - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 796a683f..8d2d2a4e 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -3,7 +3,8 @@ namespace PhpPact\Consumer\Model; use JsonException; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -11,7 +12,6 @@ class Message { use ProviderStates; - use ContentTypeTrait; private string $description; @@ -20,7 +20,7 @@ class Message */ private array $metadata = []; - private ?string $contents = null; + private Text|Binary|null $contents = null; public function getDescription(): string { @@ -60,7 +60,7 @@ private function setMetadataValue(string $key, string $value): void $this->metadata[$key] = $value; } - public function getContents(): ?string + public function getContents(): Text|Binary|null { return $this->contents; } @@ -70,13 +70,12 @@ public function getContents(): ?string */ public function setContents(mixed $contents): self { - if (\is_string($contents) || \is_null($contents)) { + if (\is_string($contents)) { + $this->contents = new Text($contents, 'text/plain'); + } elseif (\is_null($contents) || $contents instanceof Text || $contents instanceof Binary) { $this->contents = $contents; } else { - $this->contents = \json_encode($contents, JSON_THROW_ON_ERROR); - if (!isset($this->contentType)) { - $this->setContentType('application/json'); - } + $this->contents = new Text(\json_encode($contents, JSON_THROW_ON_ERROR), 'application/json'); } return $this; diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 3201811f..2abbe24b 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\BodyTrait; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; use PhpPact\Consumer\Model\Interaction\HeadersTrait; use PhpPact\Consumer\Model\Interaction\StatusTrait; @@ -14,6 +13,5 @@ class ProviderResponse { use HeadersTrait; use BodyTrait; - use ContentTypeTrait; use StatusTrait; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php new file mode 100644 index 00000000..aa3760d9 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -0,0 +1,32 @@ +client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); + } else { + $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + } + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php new file mode 100644 index 00000000..d7f69d3e --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php @@ -0,0 +1,11 @@ +client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); + } else { + $success = $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + } + if (!$success) { + throw new MessageContentsNotAddedException(); + } + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php similarity index 73% rename from src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php rename to src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php index 1a4ed86e..f1266404 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php @@ -1,6 +1,6 @@ client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } - - abstract protected function getPart(): int; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php deleted file mode 100644 index 3e3394ff..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -client->call('pactffi_message_with_contents', $this->messageRegistry->getId(), $contentType, $data->getValue(), $data->getSize()); - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php index 7a917416..70a4b083 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php @@ -76,7 +76,7 @@ private function with(ConsumerRequest $request): self ->withRequest($request->getMethod(), $request->getPath()) ->withQueryParameters($request->getQuery()) ->withHeaders($request->getHeaders()) - ->withBody($request->getContentType(), $request->getBody()); + ->withBody($request->getBody()); return $this; } @@ -86,7 +86,7 @@ private function willRespondWith(ProviderResponse $response): self $this->responseRegistry ->withResponse($response->getStatus()) ->withHeaders($response->getHeaders()) - ->withBody($response->getContentType(), $response->getBody()); + ->withBody($response->getBody()); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index 2da4e9b5..607b9bdf 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -2,21 +2,23 @@ namespace PhpPact\Consumer\Registry\Interaction; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\MessageContentsRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\MessageContentsRegistry; use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; class MessageRegistry extends AbstractRegistry implements MessageRegistryInterface { - private ContentsRegistryInterface $messageContentsRegistry; + private BodyRegistryInterface $messageContentsRegistry; public function __construct( ClientInterface $client, PactRegistryInterface $pactRegistry, - ?ContentsRegistryInterface $messageContentsRegistry = null + ?BodyRegistryInterface $messageContentsRegistry = null ) { parent::__construct($client, $pactRegistry); $this->messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); @@ -30,7 +32,7 @@ public function registerMessage(Message $message): void ->given($message->getProviderStates()) ->expectsToReceive($message->getDescription()) ->withMetadata($message->getMetadata()) - ->withContents($message->getContentType(), $message->getContents()); + ->withContents($message->getContents()); } protected function newInteraction(string $description): self @@ -40,9 +42,11 @@ protected function newInteraction(string $description): self return $this; } - private function withContents(?string $contentType = null, ?string $contents = null): self + private function withContents(Text|Binary|null $contents): self { - $this->messageContentsRegistry->withContents($contentType, $contents); + if ($contents) { + $this->messageContentsRegistry->withBody($contents); + } return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php index 9531a07c..787d71dc 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php @@ -2,7 +2,9 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -11,13 +13,15 @@ abstract class AbstractPartRegistry implements PartRegistryInterface public function __construct( protected ClientInterface $client, protected InteractionRegistryInterface $interactionRegistry, - private ContentsRegistryInterface $contentsRegistry + private BodyRegistryInterface $bodyRegistry ) { } - public function withBody(?string $contentType = null, ?string $body = null): self + public function withBody(Text|Binary|null $body): self { - $this->contentsRegistry->withContents($contentType, $body); + if ($body) { + $this->bodyRegistry->withBody($body); + } return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php index 90a6516b..5f7f59f8 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php @@ -2,9 +2,12 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; + interface PartRegistryInterface { - public function withBody(?string $contentType = null, ?string $body = null): self; + public function withBody(Text|Binary|null $body): self; /** * @param array $headers diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php index 026dc57c..890ad9b7 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php @@ -2,8 +2,8 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\RequestBodyRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\RequestBodyRegistry; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -14,7 +14,7 @@ class RequestRegistry extends AbstractPartRegistry implements RequestRegistryInt public function __construct( ClientInterface $client, InteractionRegistryInterface $interactionRegistry, - ?ContentsRegistryInterface $requestBodyRegistry = null + ?BodyRegistryInterface $requestBodyRegistry = null ) { parent::__construct($client, $interactionRegistry, $requestBodyRegistry ?? new RequestBodyRegistry($client, $interactionRegistry)); } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php index 3649a408..b74c0078 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php @@ -2,8 +2,8 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\ResponseBodyRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\ResponseBodyRegistry; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -14,7 +14,7 @@ class ResponseRegistry extends AbstractPartRegistry implements ResponseRegistryI public function __construct( ClientInterface $client, InteractionRegistryInterface $interactionRegistry, - ?ContentsRegistryInterface $responseBodyRegistry = null + ?BodyRegistryInterface $responseBodyRegistry = null ) { parent::__construct($client, $interactionRegistry, $responseBodyRegistry ?? new ResponseBodyRegistry($client, $interactionRegistry)); } diff --git a/src/PhpPact/FFI/Model/StringData.php b/src/PhpPact/FFI/Model/StringData.php index 622f457a..82e8fe57 100644 --- a/src/PhpPact/FFI/Model/StringData.php +++ b/src/PhpPact/FFI/Model/StringData.php @@ -23,10 +23,10 @@ public function getSize(): int return $this->size; } - public static function createFrom(string $value): ?self + public static function createFrom(string $value, bool $nullTerminated = true): self { $length = \strlen($value); - $size = $length + 1; + $size = $length + ($nullTerminated ? 1 : 0); $cData = FFI::new("uint8_t[{$size}]"); FFI::memcpy($cData, $value, $length); diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index f30aeca7..6ddcb9d7 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Model; use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ConsumerRequest; use PHPUnit\Framework\TestCase; @@ -24,7 +25,11 @@ public function testSerializing() $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['fruit' => ['apple', 'banana']], $model->getQuery()); $this->assertEquals('/somepath', $model->getPath()); - $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"currentCity":"Austin"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } public function testSerializingWhenPathUsingMatcher() @@ -45,6 +50,10 @@ public function testSerializingWhenPathUsingMatcher() $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['food' => ['milk']], $model->getQuery()); $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); - $this->assertEquals('{"status":"finished"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"status":"finished"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } } diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index 165ed8a8..8182706d 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; use PHPUnit\Framework\TestCase; @@ -29,6 +30,10 @@ public function testSetters() static::assertEquals($providerStateName, $providerStates[0]->getName()); static::assertEquals($providerStateParams, $providerStates[0]->getParams()); static::assertSame($metadata, $subject->getMetadata()); - static::assertSame($contents, $subject->getContents()); + + $messageContents = $subject->getContents(); + $this->assertInstanceOf(Text::class, $messageContents); + $this->assertEquals($contents, $messageContents->getContents()); + $this->assertEquals('text/plain', $messageContents->getContentType()); } } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 2da664be..315322e7 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ProviderResponse; use PHPUnit\Framework\TestCase; @@ -19,6 +20,10 @@ public function testSerializing() $this->assertEquals(200, $model->getStatus()); $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); - $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"currentCity":"Austin"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } } diff --git a/tests/PhpPact/FFI/Model/StringDataTest.php b/tests/PhpPact/FFI/Model/StringDataTest.php new file mode 100644 index 00000000..075399b6 --- /dev/null +++ b/tests/PhpPact/FFI/Model/StringDataTest.php @@ -0,0 +1,43 @@ +getValue(); + $size = \strlen($value) + 1; + + $this->assertSame($size, FFI::sizeof($cData)); + $this->assertSame($value, $this->cDataToString($cData, $size - 1)); // ignore null + } + + public function testCreateBinaryString() + { + $value = file_get_contents(__DIR__ . '/../../../_resources/image.jpg'); + $stringData = StringData::createFrom($value, false); + $cData = $stringData->getValue(); + $size = \strlen($value); + + $this->assertSame($size, FFI::sizeof($cData)); + $this->assertEquals($value, $this->cDataToString($cData, $size)); + } + + private function cDataToString(CData $cData, int $size): string + { + $result = ''; + for ($index = 0; $index < $size; $index++) { + $result .= chr($cData[$index]); // @phpstan-ignore-line + } + + return $result; + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 245d60a5..cf302a7c 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -55,7 +55,7 @@ public function testVerify(): void } $verifier = new Verifier($config); - $verifier->addDirectory(__DIR__ . '/../../../_resources'); + $verifier->addFile(__DIR__ . '/../../../_resources/someconsumer-someprovider.json'); $verifyResult = $verifier->verify(); diff --git a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php index 6ae7c400..1e0e77f3 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php @@ -18,7 +18,7 @@ public function testSetters() $providerStateHeaderName = 'header'; $token = 'token'; $user = 'user:password'; - $dirs = [__DIR__ . '/../../../_resources']; + $dirs = ['/path/to/pacts']; $files = ['/path/to/pact.json']; $urls = ['http://example.com/path/to/file.json']; $consumerNames = ['consumer-1', 'consumer-2']; diff --git a/tests/_resources/image.jpg b/tests/_resources/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c1702a1c5bbad42fbcbd30ddbdc916aa0b95a32 GIT binary patch literal 277 zcmex=>ukC3pCfH06P@c#eU2~M>R!uulu+Z&ahz#%A=1FjcHszbgN&Zte$)2daqA85W-Ir-jas!;h&9l Date: Mon, 21 Aug 2023 18:28:59 +0700 Subject: [PATCH 078/298] feat: Support http request multipart body --- composer.json | 6 +- example/multipart/consumer/phpunit.xml | 11 ++ .../src/Service/HttpClientService.php | 52 +++++++ .../consumer/src/_resource/image.jpg | Bin 0 -> 278 bytes .../tests/Service/HttpClientServiceTest.php | 95 ++++++++++++ .../consumer/tests/_resource/image.jpg | Bin 0 -> 297 bytes .../multipartConsumer-multipartProvider.json | 137 ++++++++++++++++++ example/multipart/provider/phpunit.xml | 11 ++ example/multipart/provider/public/index.php | 23 +++ .../provider/tests/PactVerifyTest.php | 62 ++++++++ phpunit.xml | 6 + .../Exception/BodyNotSupportedException.php | 9 ++ .../Exception/PartNotAddedException.php | 9 ++ .../Exception/PartNotExistException.php | 9 ++ src/PhpPact/Consumer/Model/Body/Multipart.php | 42 ++++++ src/PhpPact/Consumer/Model/Body/Part.php | 42 ++++++ .../Consumer/Model/Interaction/BodyTrait.php | 7 +- src/PhpPact/Consumer/Model/Message.php | 4 + .../Interaction/Body/AbstractBodyRegistry.php | 29 +++- .../Body/BodyRegistryInterface.php | 3 +- .../Body/MessageContentsRegistry.php | 15 +- .../Interaction/Part/AbstractPartRegistry.php | 3 +- .../Part/PartRegistryInterface.php | 3 +- 23 files changed, 559 insertions(+), 19 deletions(-) create mode 100644 example/multipart/consumer/phpunit.xml create mode 100644 example/multipart/consumer/src/Service/HttpClientService.php create mode 100644 example/multipart/consumer/src/_resource/image.jpg create mode 100644 example/multipart/consumer/tests/Service/HttpClientServiceTest.php create mode 100644 example/multipart/consumer/tests/_resource/image.jpg create mode 100644 example/multipart/pacts/multipartConsumer-multipartProvider.json create mode 100644 example/multipart/provider/phpunit.xml create mode 100644 example/multipart/provider/public/index.php create mode 100644 example/multipart/provider/tests/PactVerifyTest.php create mode 100644 src/PhpPact/Consumer/Exception/BodyNotSupportedException.php create mode 100644 src/PhpPact/Consumer/Exception/PartNotAddedException.php create mode 100644 src/PhpPact/Consumer/Exception/PartNotExistException.php create mode 100644 src/PhpPact/Consumer/Model/Body/Multipart.php create mode 100644 src/PhpPact/Consumer/Model/Body/Part.php diff --git a/composer.json b/composer.json index e558f038..2f9e524c 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,11 @@ "BinaryConsumer\\": "example/binary/consumer/src", "BinaryConsumer\\Tests\\": "example/binary/consumer/tests", "BinaryProvider\\": "example/binary/provider/src", - "BinaryProvider\\Tests\\": "example/binary/provider/tests" + "BinaryProvider\\Tests\\": "example/binary/provider/tests", + "MultipartConsumer\\": "example/multipart/consumer/src", + "MultipartConsumer\\Tests\\": "example/multipart/consumer/tests", + "MultipartProvider\\": "example/multipart/provider/src", + "MultipartProvider\\Tests\\": "example/multipart/provider/tests" } }, "scripts": { diff --git a/example/multipart/consumer/phpunit.xml b/example/multipart/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/multipart/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/multipart/consumer/src/Service/HttpClientService.php b/example/multipart/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..9e59282f --- /dev/null +++ b/example/multipart/consumer/src/Service/HttpClientService.php @@ -0,0 +1,52 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function updateUserProfile(): string + { + $response = $this->httpClient->post("{$this->baseUri}/user-profile", [ + 'multipart' => [ + [ + 'name' => 'full_name', + 'contents' => 'Zoey Turcotte' + ], + [ + 'name' => 'profile_image', + 'contents' => file_get_contents(__DIR__ . '/../_resource/image.jpg') + ], + [ + 'name' => 'personal_note', + 'contents' => 'testing', + 'filename' => 'note.txt', + 'headers' => [ + 'X-Foo' => 'this is a note', + 'Content-Type' => 'application/octet-stream', + ], + ], + ], + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ZmluLWFwaTphcGktc2VjcmV0', + ], + ]); + + return $response->getBody(); + } +} diff --git a/example/multipart/consumer/src/_resource/image.jpg b/example/multipart/consumer/src/_resource/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76cec75c32a4fb29e466be468aee6fc084ed3d3e GIT binary patch literal 278 zcmex=>ukC3pCfH06P@c#e6w5(U478N!c+l@&7Fb4v=1ZhQ0e{YOFet`SM|3 z$FJ2cS*L8)sx~oL2YCkQSRCK?Mbd1`s)?(X-85VCELk}{mdo_K+N%p2*?&Y#{N`V! z_xNMQ`hJPMZIvIU%Nd7H3p%*pQ9-!w?b@5m=V{K01z}s AZ~y=R literal 0 HcmV?d00001 diff --git a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php new file mode 100644 index 00000000..eeaebc11 --- /dev/null +++ b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php @@ -0,0 +1,95 @@ +setMethod('POST') + ->setPath('/user-profile') + ->setHeaders([ + 'Accept' => 'application/json', + 'Authorization' => [ + \json_encode($matcher->like('Bearer eyJhbGciOiJIUzI1NiIXVCJ9')) + ], + ]) + ->setBody(new Multipart([ + new Part($fullNameTempFile = $this->createTempFile($fullName), 'full_name', 'text/plain'), + new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', 'image/jpeg'), + new Part($personalNoteTempFile = $this->createTempFile($personalNote), 'personal_note', 'text/plain'), + ])); + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + 'full_name' => $matcher->like($fullName), + 'profile_image' => $matcher->regex($profileImageUrl, self::URL_FORMAT), + 'personal_note' => $matcher->like($personalNote), + ]); + + $config = new MockServerConfig(); + $config + ->setConsumer('multipartConsumer') + ->setProvider('multipartProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('User exists') + ->uponReceiving('A put request to /user-profile') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $userProfileResponse = $service->updateUserProfile(); + $verifyResult = $builder->verify(); + + unlink($fullNameTempFile); + unlink($personalNoteTempFile); + + $this->assertTrue($verifyResult); + $this->assertEquals([ + 'full_name' => $fullName, + 'profile_image' => $profileImageUrl, + 'personal_note' => $personalNote, + ], \json_decode($userProfileResponse, true, 512, JSON_THROW_ON_ERROR)); + } + + private function createTempFile(string $contents): string + { + $path = tempnam(sys_get_temp_dir(), 'pact'); + //$newPath = "$path.txt"; + //rename($path, $newPath); + $newPath = $path; + + $handle = fopen($newPath, 'w'); + fwrite($handle, $contents); + fclose($handle); + + return $newPath; + } +} diff --git a/example/multipart/consumer/tests/_resource/image.jpg b/example/multipart/consumer/tests/_resource/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82b33626e351368322aa8c0ba2594dd4d2d97d41 GIT binary patch literal 297 zcmex=>ukC3pCfH06P@c#eJHSbP!i_yH`Nrf{us8l;lFk{!|^OQHK zdS+`~^nJmbWtuBjGkHxDGj5RxKjN}G;Y5Sc-hKDyCa5i1d1j^juC1}VrX9W*@#I)s R%6$nB7r8nH3z_WyHv!Y + + + + ./tests + + + + + + diff --git a/example/multipart/provider/public/index.php b/example/multipart/provider/public/index.php new file mode 100644 index 00000000..6111dfc8 --- /dev/null +++ b/example/multipart/provider/public/index.php @@ -0,0 +1,23 @@ +addBodyParsingMiddleware(); + +$app->post('/user-profile', function (Request $request, Response $response) { + $fileName = (string)$request->getUploadedFiles()['profile_image']->getClientFilename(); + $response->getBody()->write(\json_encode([ + 'full_name' => (string)$request->getUploadedFiles()['full_name']->getStream(), + 'profile_image' => "http://example.test/$fileName", + 'personal_note' => (string)$request->getUploadedFiles()['personal_note']->getStream(), + ])); + + return $response->withHeader('Content-Type', 'application/json'); +}); + +$app->run(); diff --git a/example/multipart/provider/tests/PactVerifyTest.php b/example/multipart/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..e4fee86b --- /dev/null +++ b/example/multipart/provider/tests/PactVerifyTest.php @@ -0,0 +1,62 @@ +process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); + + $this->process->start(); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + /** + * This test will run after the web server is started. + */ + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('multipartProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/multipartConsumer-multipartProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/phpunit.xml b/phpunit.xml index 3c2344a0..f82b56e4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -25,6 +25,12 @@ ./example/binary/provider/tests + + ./example/multipart/consumer/tests + + + ./example/multipart/provider/tests + ./example/message/consumer/tests diff --git a/src/PhpPact/Consumer/Exception/BodyNotSupportedException.php b/src/PhpPact/Consumer/Exception/BodyNotSupportedException.php new file mode 100644 index 00000000..f2c767bf --- /dev/null +++ b/src/PhpPact/Consumer/Exception/BodyNotSupportedException.php @@ -0,0 +1,9 @@ + $parts + */ + public function __construct(private array $parts) + { + $this->setParts($parts); + } + + /** + * @return array + */ + public function getParts(): array + { + return $this->parts; + } + + /** + * @param array $parts + */ + public function setParts(array $parts): self + { + $this->parts = []; + foreach ($parts as $part) { + $this->addPart($part); + } + + return $this; + } + + public function addPart(Part $part): self + { + $this->parts[] = $part; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Body/Part.php b/src/PhpPact/Consumer/Model/Body/Part.php new file mode 100644 index 00000000..29241242 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Body/Part.php @@ -0,0 +1,42 @@ +setContentType($contentType); + } + + public function getPath(): string + { + return $this->path; + } + + public function setPath(string $path): self + { + $this->path = $path; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php index 3cb31e7f..1481c921 100644 --- a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php @@ -4,13 +4,14 @@ use JsonException; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; trait BodyTrait { - private Text|Binary|null $body = null; + private Text|Binary|Multipart|null $body = null; - public function getBody(): Text|Binary|null + public function getBody(): Text|Binary|Multipart|null { return $this->body; } @@ -22,7 +23,7 @@ public function setBody(mixed $body): self { if (\is_string($body)) { $this->body = new Text($body, 'text/plain'); - } elseif (\is_null($body) || $body instanceof Text || $body instanceof Binary) { + } elseif (\is_null($body) || $body instanceof Text || $body instanceof Binary || $body instanceof Multipart) { $this->body = $body; } else { $this->body = new Text(\json_encode($body, JSON_THROW_ON_ERROR), 'application/json'); diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 8d2d2a4e..90ba3356 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -3,7 +3,9 @@ namespace PhpPact\Consumer\Model; use JsonException; +use PhpPact\Consumer\Exception\BodyNotSupportedException; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; /** @@ -74,6 +76,8 @@ public function setContents(mixed $contents): self $this->contents = new Text($contents, 'text/plain'); } elseif (\is_null($contents) || $contents instanceof Text || $contents instanceof Binary) { $this->contents = $contents; + } elseif ($contents instanceof Multipart) { + throw new BodyNotSupportedException('Message does not support multipart'); } else { $this->contents = new Text(\json_encode($contents, JSON_THROW_ON_ERROR), 'application/json'); } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php index aa3760d9..bd82093e 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -2,8 +2,13 @@ namespace PhpPact\Consumer\Registry\Interaction\Body; +use FFI; +use FFI\CData; use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; +use PhpPact\Consumer\Exception\PartNotAddedException; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; +use PhpPact\Consumer\Model\Body\Part; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -16,13 +21,25 @@ public function __construct( ) { } - public function withBody(Text|Binary $body): void + public function withBody(Text|Binary|Multipart $body): void { - if ($body instanceof Binary) { - $success = $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); - } else { - $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); - } + $success = match ($body::class) { + Binary::class => $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), + Text::class => $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), + Multipart::class => array_reduce( + $body->getParts(), + function (bool $success, Part $part) { + $result = $this->client->call('pactffi_with_multipart_file', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName()); + if ($result->failed instanceof CData) { + throw new PartNotAddedException(FFI::string($result->failed)); + } + + return true; + }, + true + ), + default => false, + }; if (!$success) { throw new InteractionBodyNotAddedException(); } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php index d7f69d3e..d1f668b9 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php @@ -3,9 +3,10 @@ namespace PhpPact\Consumer\Registry\Interaction\Body; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; interface BodyRegistryInterface { - public function withBody(Text|Binary $body): void; + public function withBody(Text|Binary|Multipart $body): void; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php index 251a7088..f352eb88 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php @@ -2,8 +2,10 @@ namespace PhpPact\Consumer\Registry\Interaction\Body; +use PhpPact\Consumer\Exception\BodyNotSupportedException; use PhpPact\Consumer\Exception\MessageContentsNotAddedException; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Registry\Interaction\MessageRegistryInterface; use PhpPact\Consumer\Registry\Interaction\Part\RequestPartTrait; @@ -19,13 +21,14 @@ public function __construct( ) { } - public function withBody(Text|Binary $body): void + public function withBody(Text|Binary|Multipart $body): void { - if ($body instanceof Binary) { - $success = $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); - } else { - $success = $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); - } + $success = match ($body::class) { + Binary::class => $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), + Text::class => $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), + Multipart::class => throw new BodyNotSupportedException('Message does not support multipart'), + default => false, + }; if (!$success) { throw new MessageContentsNotAddedException(); } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php index 787d71dc..f9369e45 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php @@ -3,6 +3,7 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; @@ -17,7 +18,7 @@ public function __construct( ) { } - public function withBody(Text|Binary|null $body): self + public function withBody(Text|Binary|Multipart|null $body): self { if ($body) { $this->bodyRegistry->withBody($body); diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php index 5f7f59f8..383e3a41 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php @@ -3,11 +3,12 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; interface PartRegistryInterface { - public function withBody(Text|Binary|null $body): self; + public function withBody(Text|Binary|Multipart|null $body): self; /** * @param array $headers From 6ef1785ecbc61a3da88631202d8cd5462a16462f Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 30 Aug 2023 07:50:51 +0700 Subject: [PATCH 079/298] chore: Update ffi library --- composer.json | 4 ++-- .../consumer/src/Service/HttpClientService.php | 9 +++++++-- .../multipartConsumer-multipartProvider.json | 18 +++++------------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 2f9e524c..56174f6e 100644 --- a/composer.json +++ b/composer.json @@ -73,12 +73,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.7", + "version": "0.4.8", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.7", + "version": "0.4.8", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", diff --git a/example/multipart/consumer/src/Service/HttpClientService.php b/example/multipart/consumer/src/Service/HttpClientService.php index 9e59282f..f9a79f66 100644 --- a/example/multipart/consumer/src/Service/HttpClientService.php +++ b/example/multipart/consumer/src/Service/HttpClientService.php @@ -25,11 +25,16 @@ public function updateUserProfile(): string 'multipart' => [ [ 'name' => 'full_name', - 'contents' => 'Zoey Turcotte' + 'contents' => 'Zoey Turcotte', + 'filename' => 'full_name.txt', + 'headers' => [ + 'Content-Type' => 'application/octet-stream', + ], ], [ 'name' => 'profile_image', - 'contents' => file_get_contents(__DIR__ . '/../_resource/image.jpg') + 'contents' => file_get_contents(__DIR__ . '/../_resource/image.jpg'), + 'filename' => 'image.jpg', ], [ 'name' => 'personal_note', diff --git a/example/multipart/pacts/multipartConsumer-multipartProvider.json b/example/multipart/pacts/multipartConsumer-multipartProvider.json index 8b7f10ab..eb2fcc33 100644 --- a/example/multipart/pacts/multipartConsumer-multipartProvider.json +++ b/example/multipart/pacts/multipartConsumer-multipartProvider.json @@ -11,11 +11,11 @@ } ], "request": { - "body": "--MxafNCX4RAVZ1d2c\r\nContent-Disposition: form-data; name=\"personal_note\"; filename=\"pactmk99lD\"\r\nContent-Type: application/octet-stream\r\n\r\ntesting\r\n--MxafNCX4RAVZ1d2c--\r\n", + "body": "LS1rdEptZVlIYmtUU2ExanhEDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZ1bGxfbmFtZSI7IGZpbGVuYW1lPSJwYWN0dmZhU1JiIg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0NCg0KQ29sdGVuIFppZW1hbm4NCi0ta3RKbWVZSGJrVFNhMWp4RA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJwcm9maWxlX2ltYWdlIjsgZmlsZW5hbWU9ImltYWdlLmpwZyINCkNvbnRlbnQtVHlwZTogaW1hZ2UvanBlZw0KDQr/2P/gABBKRklGAAEBAAABAAEAAP/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/AAAsIAAwADAEBEQD/xAAWAAEBAQAAAAAAAAAAAAAAAAAEAAX/xAAiEAACAQMDBQEAAAAAAAAAAAABAwIEERIABSEGEyIjQRT/2gAIAQEAAD8AUa1jdqVVShOmTKYY4qW1MC0syis3jITyjExBiBjYXHkDq209MlLCjqqe3HuEMp5TknGYsCR7QJA2BysOSR80euY9O3L3oOymKamrAkqWFjOEGFfERLtgyIAyvb7fnWAmoqnMqR+6tV26lsPRWOTGXmTfGEhEHn4AOBxr/9kNCi0ta3RKbWVZSGJrVFNhMWp4RA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJwZXJzb25hbF9ub3RlIjsgZmlsZW5hbWU9InBhY3Q3OU1jT1QiDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbQ0KDQp0ZXN0aW5nDQotLWt0Sm1lWUhia1RTYTFqeEQtLQ0K", "headers": { "Accept": "application/json", "Authorization": "Bearer eyJhbGciOiJIUzI1NiIXVCJ9", - "Content-Type": "multipart/form-data; boundary=MxafNCX4RAVZ1d2c" + "Content-Type": "multipart/form-data; boundary=ktJmeYHbkTSa1jxD" }, "matchingRules": { "body": { @@ -59,14 +59,6 @@ "Content-Type": { "combine": "AND", "matchers": [ - { - "match": "regex", - "regex": "multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*" - }, - { - "match": "regex", - "regex": "multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*" - }, { "match": "regex", "regex": "multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*" @@ -123,9 +115,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.7", - "mockserver": "1.2.3", - "models": "1.1.9" + "ffi": "0.4.8", + "mockserver": "1.2.4", + "models": "1.1.11" }, "pactSpecification": { "version": "3.0.0" From 7fde6a44d6d6f7458786db19d42fa49199287323 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 30 Aug 2023 08:00:23 +0700 Subject: [PATCH 080/298] chore: Use different mime type on Windows --- .../multipart/consumer/tests/Service/HttpClientServiceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php index eeaebc11..0df7999d 100644 --- a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php @@ -35,7 +35,7 @@ public function testUpdateUserProfile() ]) ->setBody(new Multipart([ new Part($fullNameTempFile = $this->createTempFile($fullName), 'full_name', 'text/plain'), - new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', 'image/jpeg'), + new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg'), new Part($personalNoteTempFile = $this->createTempFile($personalNote), 'personal_note', 'text/plain'), ])); From 2f5baac119fcd9efa746ab8380cf1cfb4b5b2cf5 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 12 Sep 2023 09:50:30 +0700 Subject: [PATCH 081/298] fix: Update link to examples in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cef1868c..046667c2 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ All of the following code will be used exclusively for the Consumer. Create a standard PHPUnit test case class and function. -[Click here](/example/tests/Consumer/Service/ConsumerServiceHelloTest.php) to see the full sample file. +[Click here](/example/json/consumer/tests/Service/ConsumerServiceHelloTest.php) to see the full sample file. ### Create Mock Request @@ -381,7 +381,7 @@ to processing class. Aside from changing default ports, this should be transpa Both the provider and consumer side make heavy use of lambda functions. ### Consumer Side Message Processing -The examples provided are pretty basic. See examples\tests\MessageConsumer. +The examples provided are pretty basic. See [example](/example/message/consumer/tests/ExampleMessageConsumerTest.php). 1. Create the content and metadata (array) 1. Annotate the MessageBuilder appropriate content and states 1. Given = Provider State @@ -425,7 +425,7 @@ Handle these requests on your provider: 1. Return message's content in body 2. Return message's metadata in header `PACT-MESSAGE-METADATA` -[Click here](/example/src/Provider/public/index.php) to see the full sample file. +[Click here](/example/message/provider/public/index.php) to see the full sample file. ## Usage for the optional `pact-stub-service` From 9289a44fcdd404bf5bdbb06088647b744845cdb9 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 22 Sep 2023 12:21:36 +0700 Subject: [PATCH 082/298] Print provider process's log --- .../binary/provider/tests/PactVerifyTest.php | 16 ++------ .../json/provider/tests/PactVerifyTest.php | 16 ++------ .../message/provider/tests/PactVerifyTest.php | 18 ++------- .../provider/tests/PactVerifyTest.php | 16 ++------ tests/PhpPact/Helper/ProviderProcess.php | 40 +++++++++++++++++++ .../ProviderVerifier/VerifierTest.php | 19 ++------- 6 files changed, 55 insertions(+), 70 deletions(-) create mode 100644 tests/PhpPact/Helper/ProviderProcess.php diff --git a/example/binary/provider/tests/PactVerifyTest.php b/example/binary/provider/tests/PactVerifyTest.php index cc30e108..77059516 100644 --- a/example/binary/provider/tests/PactVerifyTest.php +++ b/example/binary/provider/tests/PactVerifyTest.php @@ -4,30 +4,20 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class PactVerifyTest extends TestCase { - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); - + $this->process = new ProviderProcess(__DIR__ . '/../public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** diff --git a/example/json/provider/tests/PactVerifyTest.php b/example/json/provider/tests/PactVerifyTest.php index 408dade1..94ca1599 100644 --- a/example/json/provider/tests/PactVerifyTest.php +++ b/example/json/provider/tests/PactVerifyTest.php @@ -5,30 +5,20 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class PactVerifyTest extends TestCase { - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); - + $this->process = new ProviderProcess(__DIR__ . '/../public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** diff --git a/example/message/provider/tests/PactVerifyTest.php b/example/message/provider/tests/PactVerifyTest.php index f0c459a1..bea7fca0 100644 --- a/example/message/provider/tests/PactVerifyTest.php +++ b/example/message/provider/tests/PactVerifyTest.php @@ -6,32 +6,20 @@ use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class PactVerifyTest extends TestCase { - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $publicPath = __DIR__ . '/../public/'; - - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - + $this->process = new ProviderProcess(__DIR__ . '/../public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** diff --git a/example/multipart/provider/tests/PactVerifyTest.php b/example/multipart/provider/tests/PactVerifyTest.php index e4fee86b..439e2802 100644 --- a/example/multipart/provider/tests/PactVerifyTest.php +++ b/example/multipart/provider/tests/PactVerifyTest.php @@ -4,30 +4,20 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class PactVerifyTest extends TestCase { - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); - + $this->process = new ProviderProcess(__DIR__ . '/../public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** diff --git a/tests/PhpPact/Helper/ProviderProcess.php b/tests/PhpPact/Helper/ProviderProcess.php new file mode 100644 index 00000000..b6d5a81e --- /dev/null +++ b/tests/PhpPact/Helper/ProviderProcess.php @@ -0,0 +1,40 @@ +process = new Process(['php', '-S', "127.0.0.1:$port", '-t', $publicPath]); + } + + public function start(): void + { + $this->process->start(function ($type, $buffer): void { + if (Process::ERR === $type) { + echo 'ERR > '.$buffer; + } else { + echo 'OUT > '.$buffer; + } + }); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', $this->port); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + public function stop(): void + { + $this->process->stop(); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index cf302a7c..507e03ee 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -4,33 +4,20 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class VerifierTest extends TestCase { - /** @var Process */ - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $publicPath = __DIR__ . '/../../../_public/'; - - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - + $this->process = new ProviderProcess(__DIR__ . '/../../../_public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** From dcdbc21bdcf2815978eae0361eb11bff3df87012 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 27 Sep 2023 21:54:39 +0700 Subject: [PATCH 083/298] Print type directly --- tests/PhpPact/Helper/ProviderProcess.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/PhpPact/Helper/ProviderProcess.php b/tests/PhpPact/Helper/ProviderProcess.php index b6d5a81e..83e9a2c8 100644 --- a/tests/PhpPact/Helper/ProviderProcess.php +++ b/tests/PhpPact/Helper/ProviderProcess.php @@ -15,12 +15,8 @@ public function __construct(string $publicPath, private int $port = 7202) public function start(): void { - $this->process->start(function ($type, $buffer): void { - if (Process::ERR === $type) { - echo 'ERR > '.$buffer; - } else { - echo 'OUT > '.$buffer; - } + $this->process->start(function (string $type, string $buffer): void { + echo "\n$type > $buffer"; }); $this->process->waitUntil(function (): bool { $fp = @fsockopen('127.0.0.1', $this->port); From 15046f41b5ad17ed8ac0d68d809eb7518369eefd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 22 Sep 2023 12:37:50 +0700 Subject: [PATCH 084/298] Fix 'JIT on Windows is not currently supported in phpseclib' --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35b5c350..2239e2c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,6 +63,7 @@ jobs: extensions: sockets, curl, zip, ffi php-version: ${{ matrix.php }} coverage: none + ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} - name: Composer install uses: ramsey/composer-install@v2 From 563f0b41af57d72b5b12e27148b9c3980d46c43c Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 22 Aug 2023 16:28:27 +0700 Subject: [PATCH 085/298] Create static files --- .../src/Service/HttpClientService.php | 7 ------- .../tests/Service/HttpClientServiceTest.php | 21 ++----------------- .../consumer/tests/_resource/full_name.txt | 1 + .../consumer/tests/_resource/note.txt | 1 + 4 files changed, 4 insertions(+), 26 deletions(-) create mode 100644 example/multipart/consumer/tests/_resource/full_name.txt create mode 100644 example/multipart/consumer/tests/_resource/note.txt diff --git a/example/multipart/consumer/src/Service/HttpClientService.php b/example/multipart/consumer/src/Service/HttpClientService.php index f9a79f66..8a1ee65f 100644 --- a/example/multipart/consumer/src/Service/HttpClientService.php +++ b/example/multipart/consumer/src/Service/HttpClientService.php @@ -27,9 +27,6 @@ public function updateUserProfile(): string 'name' => 'full_name', 'contents' => 'Zoey Turcotte', 'filename' => 'full_name.txt', - 'headers' => [ - 'Content-Type' => 'application/octet-stream', - ], ], [ 'name' => 'profile_image', @@ -40,10 +37,6 @@ public function updateUserProfile(): string 'name' => 'personal_note', 'contents' => 'testing', 'filename' => 'note.txt', - 'headers' => [ - 'X-Foo' => 'this is a note', - 'Content-Type' => 'application/octet-stream', - ], ], ], 'headers' => [ diff --git a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php index 0df7999d..6fff0d46 100644 --- a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php @@ -34,9 +34,9 @@ public function testUpdateUserProfile() ], ]) ->setBody(new Multipart([ - new Part($fullNameTempFile = $this->createTempFile($fullName), 'full_name', 'text/plain'), + new Part(__DIR__ . '/../_resource/full_name.txt', 'full_name', 'text/plain'), new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg'), - new Part($personalNoteTempFile = $this->createTempFile($personalNote), 'personal_note', 'text/plain'), + new Part(__DIR__ . '/../_resource/note.txt', 'personal_note', 'text/plain'), ])); $response = new ProviderResponse(); @@ -68,9 +68,6 @@ public function testUpdateUserProfile() $userProfileResponse = $service->updateUserProfile(); $verifyResult = $builder->verify(); - unlink($fullNameTempFile); - unlink($personalNoteTempFile); - $this->assertTrue($verifyResult); $this->assertEquals([ 'full_name' => $fullName, @@ -78,18 +75,4 @@ public function testUpdateUserProfile() 'personal_note' => $personalNote, ], \json_decode($userProfileResponse, true, 512, JSON_THROW_ON_ERROR)); } - - private function createTempFile(string $contents): string - { - $path = tempnam(sys_get_temp_dir(), 'pact'); - //$newPath = "$path.txt"; - //rename($path, $newPath); - $newPath = $path; - - $handle = fopen($newPath, 'w'); - fwrite($handle, $contents); - fclose($handle); - - return $newPath; - } } diff --git a/example/multipart/consumer/tests/_resource/full_name.txt b/example/multipart/consumer/tests/_resource/full_name.txt new file mode 100644 index 00000000..de749767 --- /dev/null +++ b/example/multipart/consumer/tests/_resource/full_name.txt @@ -0,0 +1 @@ +Colten Ziemann \ No newline at end of file diff --git a/example/multipart/consumer/tests/_resource/note.txt b/example/multipart/consumer/tests/_resource/note.txt new file mode 100644 index 00000000..9a2c7732 --- /dev/null +++ b/example/multipart/consumer/tests/_resource/note.txt @@ -0,0 +1 @@ +testing \ No newline at end of file From 8ae7b1cafddd053eda8464adde82c7279ecdafbe Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 29 Sep 2023 20:12:54 +0700 Subject: [PATCH 086/298] Update ffi library to fix string values matching issue --- composer.json | 4 ++-- example/binary/pacts/binaryConsumer-binaryProvider.json | 6 +++--- example/json/pacts/jsonConsumer-jsonProvider.json | 6 +++--- example/message/pacts/messageConsumer-messageProvider.json | 4 ++-- .../pacts/multipartConsumer-multipartProvider.json | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 56174f6e..c79aeee8 100644 --- a/composer.json +++ b/composer.json @@ -73,12 +73,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.8", + "version": "0.4.9", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.8", + "version": "0.4.9", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json index 7ecf427f..c6a957a1 100644 --- a/example/binary/pacts/binaryConsumer-binaryProvider.json +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -42,9 +42,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.7", - "mockserver": "1.2.3", - "models": "1.1.9" + "ffi": "0.4.9", + "mockserver": "1.2.4", + "models": "1.1.11" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/json/pacts/jsonConsumer-jsonProvider.json b/example/json/pacts/jsonConsumer-jsonProvider.json index 709eefa4..a169f1f2 100644 --- a/example/json/pacts/jsonConsumer-jsonProvider.json +++ b/example/json/pacts/jsonConsumer-jsonProvider.json @@ -63,9 +63,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.7", - "mockserver": "1.2.3", - "models": "1.1.9" + "ffi": "0.4.9", + "mockserver": "1.2.4", + "models": "1.1.11" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/message/pacts/messageConsumer-messageProvider.json b/example/message/pacts/messageConsumer-messageProvider.json index 95669646..9b9cd512 100644 --- a/example/message/pacts/messageConsumer-messageProvider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -41,8 +41,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.7", - "models": "1.1.9" + "ffi": "0.4.9", + "models": "1.1.11" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/multipart/pacts/multipartConsumer-multipartProvider.json b/example/multipart/pacts/multipartConsumer-multipartProvider.json index eb2fcc33..4f517af1 100644 --- a/example/multipart/pacts/multipartConsumer-multipartProvider.json +++ b/example/multipart/pacts/multipartConsumer-multipartProvider.json @@ -115,7 +115,7 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.8", + "ffi": "0.4.9", "mockserver": "1.2.4", "models": "1.1.11" }, From 407b720af7c766d8324c589c98723e6ca50b2321 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 16 Sep 2023 17:25:03 +0700 Subject: [PATCH 087/298] Use static multipart's boundary --- .../tests/Service/HttpClientServiceTest.php | 13 ++++++++----- .../pacts/multipartConsumer-multipartProvider.json | 2 +- src/PhpPact/Consumer/Model/Body/Multipart.php | 7 ++++++- .../Interaction/Body/AbstractBodyRegistry.php | 4 ++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php index 6fff0d46..18e7b278 100644 --- a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php @@ -33,11 +33,14 @@ public function testUpdateUserProfile() \json_encode($matcher->like('Bearer eyJhbGciOiJIUzI1NiIXVCJ9')) ], ]) - ->setBody(new Multipart([ - new Part(__DIR__ . '/../_resource/full_name.txt', 'full_name', 'text/plain'), - new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg'), - new Part(__DIR__ . '/../_resource/note.txt', 'personal_note', 'text/plain'), - ])); + ->setBody(new Multipart( + [ + new Part(__DIR__ . '/../_resource/full_name.txt', 'full_name', 'text/plain'), + new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg'), + new Part(__DIR__ . '/../_resource/note.txt', 'personal_note', 'text/plain'), + ], + 'ktJmeYHbkTSa1jxD' + )); $response = new ProviderResponse(); $response diff --git a/example/multipart/pacts/multipartConsumer-multipartProvider.json b/example/multipart/pacts/multipartConsumer-multipartProvider.json index 4f517af1..69e198d3 100644 --- a/example/multipart/pacts/multipartConsumer-multipartProvider.json +++ b/example/multipart/pacts/multipartConsumer-multipartProvider.json @@ -11,7 +11,7 @@ } ], "request": { - "body": "LS1rdEptZVlIYmtUU2ExanhEDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZ1bGxfbmFtZSI7IGZpbGVuYW1lPSJwYWN0dmZhU1JiIg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0NCg0KQ29sdGVuIFppZW1hbm4NCi0ta3RKbWVZSGJrVFNhMWp4RA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJwcm9maWxlX2ltYWdlIjsgZmlsZW5hbWU9ImltYWdlLmpwZyINCkNvbnRlbnQtVHlwZTogaW1hZ2UvanBlZw0KDQr/2P/gABBKRklGAAEBAAABAAEAAP/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/AAAsIAAwADAEBEQD/xAAWAAEBAQAAAAAAAAAAAAAAAAAEAAX/xAAiEAACAQMDBQEAAAAAAAAAAAABAwIEERIABSEGEyIjQRT/2gAIAQEAAD8AUa1jdqVVShOmTKYY4qW1MC0syis3jITyjExBiBjYXHkDq209MlLCjqqe3HuEMp5TknGYsCR7QJA2BysOSR80euY9O3L3oOymKamrAkqWFjOEGFfERLtgyIAyvb7fnWAmoqnMqR+6tV26lsPRWOTGXmTfGEhEHn4AOBxr/9kNCi0ta3RKbWVZSGJrVFNhMWp4RA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJwZXJzb25hbF9ub3RlIjsgZmlsZW5hbWU9InBhY3Q3OU1jT1QiDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbQ0KDQp0ZXN0aW5nDQotLWt0Sm1lWUhia1RTYTFqeEQtLQ0K", + "body": "LS1rdEptZVlIYmtUU2ExanhEDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZ1bGxfbmFtZSI7IGZpbGVuYW1lPSJmdWxsX25hbWUudHh0Ig0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluDQoNCkNvbHRlbiBaaWVtYW5uDQotLWt0Sm1lWUhia1RTYTFqeEQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0icHJvZmlsZV9pbWFnZSI7IGZpbGVuYW1lPSJpbWFnZS5qcGciDQpDb250ZW50LVR5cGU6IGltYWdlL2pwZWcNCg0K/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAMAAwBAREA/8QAFgABAQEAAAAAAAAAAAAAAAAABAAF/8QAIhAAAgEDAwUBAAAAAAAAAAAAAQMCBBESAAUhBhMiI0EU/9oACAEBAAA/AFGtY3alVUoTpkymGOKltTAtLMorN4yE8oxMQYgY2Fx5A6ttPTJSwo6qntx7hDKeU5JxmLAke0CQNgcrDkkfNHrmPTty96DspimpqwJKlhYzhBhXxES7YMiAMr2+351gJqKpzKkfurVdupbD0Vjkxl5k3xhIRB5+ADgca//ZDQotLWt0Sm1lWUhia1RTYTFqeEQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0icGVyc29uYWxfbm90ZSI7IGZpbGVuYW1lPSJub3RlLnR4dCINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbg0KDQp0ZXN0aW5nDQotLWt0Sm1lWUhia1RTYTFqeEQtLQ0K", "headers": { "Accept": "application/json", "Authorization": "Bearer eyJhbGciOiJIUzI1NiIXVCJ9", diff --git a/src/PhpPact/Consumer/Model/Body/Multipart.php b/src/PhpPact/Consumer/Model/Body/Multipart.php index 0a2d1ce0..fde72186 100644 --- a/src/PhpPact/Consumer/Model/Body/Multipart.php +++ b/src/PhpPact/Consumer/Model/Body/Multipart.php @@ -7,7 +7,7 @@ class Multipart /** * @param array $parts */ - public function __construct(private array $parts) + public function __construct(private array $parts, private string $boundary) { $this->setParts($parts); } @@ -39,4 +39,9 @@ public function addPart(Part $part): self return $this; } + + public function getBoundary(): string + { + return $this->boundary; + } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php index bd82093e..2bb5d920 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -28,8 +28,8 @@ public function withBody(Text|Binary|Multipart $body): void Text::class => $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), Multipart::class => array_reduce( $body->getParts(), - function (bool $success, Part $part) { - $result = $this->client->call('pactffi_with_multipart_file', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName()); + function (bool $success, Part $part) use ($body) { + $result = $this->client->call('pactffi_with_multipart_file_v2', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); if ($result->failed instanceof CData) { throw new PartNotAddedException(FFI::string($result->failed)); } From ffc04436a17094f30d8be5492f05c9d1558c6f4a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 29 Sep 2023 21:04:27 +0700 Subject: [PATCH 088/298] Use arm container --- .cirrus.yml | 6 +++--- .../binary/consumer/tests/Service/HttpClientServiceTest.php | 2 +- .../consumer/tests/Service/HttpClientServiceTest.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 80abe231..2a3a7c07 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -24,10 +24,10 @@ linux_arm64_task: - VERSION: 8.2 - VERSION: 8.1 - VERSION: 8.0 - container: + arm_container: image: php:$VERSION pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev shared-mime-info + - apt update --yes && apt install --yes zip unzip git libffi-dev - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - docker-php-ext-install sockets @@ -46,7 +46,7 @@ macos_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest pre_req_script: - - brew install php@$VERSION composer shared-mime-info + - brew install php@$VERSION composer version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/example/binary/consumer/tests/Service/HttpClientServiceTest.php b/example/binary/consumer/tests/Service/HttpClientServiceTest.php index 42fe6b82..c25e89bb 100644 --- a/example/binary/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/binary/consumer/tests/Service/HttpClientServiceTest.php @@ -26,7 +26,7 @@ public function testGetImageContent() $response ->setStatus(200) ->addHeader('Content-Type', 'image/jpeg') - ->setBody(new Binary($imageContent, in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg')); + ->setBody(new Binary($imageContent, in_array(php_uname('m'), ['AMD64', 'arm64', 'aarch64']) ? 'application/octet-stream' : 'image/jpeg')); $config = new MockServerConfig(); $config diff --git a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php index 18e7b278..e53e1c9f 100644 --- a/example/multipart/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/multipart/consumer/tests/Service/HttpClientServiceTest.php @@ -36,7 +36,7 @@ public function testUpdateUserProfile() ->setBody(new Multipart( [ new Part(__DIR__ . '/../_resource/full_name.txt', 'full_name', 'text/plain'), - new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg'), + new Part(__DIR__ . '/../_resource/image.jpg', 'profile_image', in_array(php_uname('m'), ['AMD64', 'arm64', 'aarch64']) ? 'application/octet-stream' : 'image/jpeg'), new Part(__DIR__ . '/../_resource/note.txt', 'personal_note', 'text/plain'), ], 'ktJmeYHbkTSa1jxD' From f121fb285ab27c051ea5cb9f5395e7b07ecd4562 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 8 Oct 2023 21:48:25 +0700 Subject: [PATCH 089/298] chore: Remove StubServerHttpService --- README.md | 6 +- UPGRADE-10.0.md | 7 ++- composer.json | 5 +- src/PhpPact/Http/ClientInterface.php | 37 ------------ src/PhpPact/Http/GuzzleClient.php | 60 ------------------- .../Service/StubServerHttpService.php | 40 ------------- .../StubServerHttpServiceInterface.php | 11 ---- .../Service/StubServerHttpServiceTest.php | 54 ----------------- 8 files changed, 11 insertions(+), 209 deletions(-) delete mode 100644 src/PhpPact/Http/ClientInterface.php delete mode 100644 src/PhpPact/Http/GuzzleClient.php delete mode 100644 src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php delete mode 100644 src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php delete mode 100644 tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php diff --git a/README.md b/README.md index 046667c2..4cf034b0 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,9 @@ $config = (new StubServerConfig()) $stubServer = new StubServer($config); $stubServer->start(); -$service = new StubServerHttpService(new GuzzleClient(), $config); +$client = new \GuzzleHttp\Client(); -echo $service->getJson($endpoint); // output: {"results":[{"name":"Games"}]} +$response = $client->get($this->config->getBaseUri() . '/' . $endpoint); + +echo $response->getBody(); // output: {"results":[{"name":"Games"}]} ``` diff --git a/UPGRADE-10.0.md b/UPGRADE-10.0.md index d634ea28..c7c77892 100644 --- a/UPGRADE-10.0.md +++ b/UPGRADE-10.0.md @@ -118,11 +118,12 @@ This migrates from a CLI driven process for the Pact Framework, to an FFI proces - Stub Server - No longer defaults to port 7201, picks free port at random. - - Endpoint now can be set by: + - `PhpPact\Standalone\StubService\Service\StubServerHttpService` is no longer available. Guzzle can be used to request to stub server directly: ```php - $service = new StubServerHttpService(new GuzzleClient(), $this->config); - $service->getJson($endpoint); + $client = new \GuzzleHttp\Client(); + $response = $client->get($this->config->getBaseUri() . '/' . $endpoint); + echo $response->getBody(); ``` - Example Migrations to 10.x (Pull Request Diffs) diff --git a/composer.json b/composer.json index 56174f6e..e7671da2 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", "symfony/process": "^4.4|^5.4|^6.0", - "guzzlehttp/guzzle": "^6.5.8|^7.4.5", + "guzzlehttp/psr7": "^2.4.5", "tienvx/composer-downloads-plugin": "^1.2.0" }, "require-dev": { @@ -32,7 +32,8 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": ">=8.5.23 <10" + "phpunit/phpunit": ">=8.5.23 <10", + "guzzlehttp/guzzle": "^7.8" }, "autoload": { "psr-4": { diff --git a/src/PhpPact/Http/ClientInterface.php b/src/PhpPact/Http/ClientInterface.php deleted file mode 100644 index fc687e11..00000000 --- a/src/PhpPact/Http/ClientInterface.php +++ /dev/null @@ -1,37 +0,0 @@ - $options - */ - public function get(UriInterface $uri, array $options = []): ResponseInterface; - - /** - * Put Request. - * - * @param array $options - */ - public function put(UriInterface $uri, array $options = []): ResponseInterface; - - /** - * Post Request. - * - * @param array $options - */ - public function post(UriInterface $uri, array $options = []): ResponseInterface; - - /** - * Delete Request. - * - * @param array $options - */ - public function delete(UriInterface $uri, array $options = []): ResponseInterface; -} diff --git a/src/PhpPact/Http/GuzzleClient.php b/src/PhpPact/Http/GuzzleClient.php deleted file mode 100644 index e3d3b776..00000000 --- a/src/PhpPact/Http/GuzzleClient.php +++ /dev/null @@ -1,60 +0,0 @@ - $config - */ - public function __construct(array $config = []) - { - $this->client = new Client($config); - } - - /** - * @param array $options - * @throws GuzzleException - */ - public function get(UriInterface $uri, array $options = []): ResponseInterface - { - return $this->client->get($uri, $options); - } - - /** - * @param array $options - * @throws GuzzleException - */ - public function put(UriInterface $uri, array $options = []): ResponseInterface - { - return $this->client->put($uri, $options); - } - - /** - * @param array $options - * @throws GuzzleException - */ - public function delete(UriInterface $uri, array $options = []): ResponseInterface - { - return $this->client->delete($uri, $options); - } - - /** - * @param array $options - * @throws GuzzleException - */ - public function post(UriInterface $uri, array $options = []): ResponseInterface - { - return $this->client->post($uri, $options); - } -} diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php deleted file mode 100644 index f40100a1..00000000 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php +++ /dev/null @@ -1,40 +0,0 @@ -client = $client; - $this->config = $config; - } - - /** - * {@inheritdoc} - * @throws \JsonException - */ - public function getJson(string $endpoint): string - { - $uri = $this->config->getBaseUri()->withPath('/' . $endpoint); - $response = $this->client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - ]); - - return \json_encode(\json_decode($response->getBody()->getContents(), null, 512, JSON_THROW_ON_ERROR), JSON_THROW_ON_ERROR); - } -} diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php deleted file mode 100644 index 325637e2..00000000 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -config = (new StubServerConfig()) - ->setFiles($files) - ->setPort($port); - - $this->stubServer = new StubServer($this->config); - $this->stubServer->start(); - $this->service = new StubServerHttpService(new GuzzleClient(), $this->config); - } - - protected function tearDown(): void - { - $this->stubServer->stop(); - } - - public function testGetJson() - { - $endpoint = 'test'; - $result = $this->service->getJson($endpoint); - $this->assertEquals('{"results":[{"name":"Games"}]}', $result); - } -} From de1cca00663e4922c33bef7cf3adf5de48623270 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 29 Aug 2023 23:25:29 +0700 Subject: [PATCH 090/298] chore: add xml example --- composer.json | 10 +- example/xml/consumer/phpunit.xml | 11 ++ .../src/Service/HttpClientService.php | 28 ++++ .../tests/Service/HttpClientServiceTest.php | 94 ++++++++++++++ .../xml/pacts/xmlConsumer-xmlProvider.json | 122 ++++++++++++++++++ example/xml/provider/phpunit.xml | 11 ++ example/xml/provider/public/index.php | 53 ++++++++ example/xml/provider/tests/PactVerifyTest.php | 66 ++++++++++ 8 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 example/xml/consumer/phpunit.xml create mode 100644 example/xml/consumer/src/Service/HttpClientService.php create mode 100644 example/xml/consumer/tests/Service/HttpClientServiceTest.php create mode 100644 example/xml/pacts/xmlConsumer-xmlProvider.json create mode 100644 example/xml/provider/phpunit.xml create mode 100644 example/xml/provider/public/index.php create mode 100644 example/xml/provider/tests/PactVerifyTest.php diff --git a/composer.json b/composer.json index 48837478..a588344a 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", "phpunit/phpunit": ">=8.5.23 <10", - "guzzlehttp/guzzle": "^7.8" + "guzzlehttp/guzzle": "^7.8", + "tienvx/pact-php-xml": "dev-main" }, "autoload": { "psr-4": { @@ -55,10 +56,17 @@ "BinaryConsumer\\Tests\\": "example/binary/consumer/tests", "BinaryProvider\\": "example/binary/provider/src", "BinaryProvider\\Tests\\": "example/binary/provider/tests", +<<<<<<< HEAD "MultipartConsumer\\": "example/multipart/consumer/src", "MultipartConsumer\\Tests\\": "example/multipart/consumer/tests", "MultipartProvider\\": "example/multipart/provider/src", "MultipartProvider\\Tests\\": "example/multipart/provider/tests" +======= + "XmlConsumer\\": "example/xml/consumer/src", + "XmlConsumer\\Tests\\": "example/xml/consumer/tests", + "XmlProvider\\": "example/xml/provider/src", + "XmlProvider\\Tests\\": "example/xml/provider/tests" +>>>>>>> 7ee934b (chore: add xml example) } }, "scripts": { diff --git a/example/xml/consumer/phpunit.xml b/example/xml/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/xml/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/xml/consumer/src/Service/HttpClientService.php b/example/xml/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..a2193e03 --- /dev/null +++ b/example/xml/consumer/src/Service/HttpClientService.php @@ -0,0 +1,28 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getMovies(): string + { + $response = $this->httpClient->get(new Uri("{$this->baseUri}/movies"), [ + 'headers' => ['Accept' => 'text/xml; charset=UTF8'] + ]); + + return $response->getBody(); + } +} diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php new file mode 100644 index 00000000..8e33abf9 --- /dev/null +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -0,0 +1,94 @@ +setMethod('GET') + ->setPath('/movies') + ->addHeader('Accept', 'text/xml; charset=UTF8'); + + $xmlBuilder = new XmlBuilder('1.0', 'UTF-8'); + $xmlBuilder + ->start('movies') + ->eachLike('movie') + ->start('title') + ->contentLike('PHP: Behind the Parser') + ->end() + ->start('characters') + ->eachLike('character', [], new Options(examples: 2)) + ->start('name') + ->contentLike('Ms. Coder') + ->end() + ->start('actor') + ->contentLike('Onlivia Actora') + ->end() + ->end() + ->end() + ->start('plot') + ->contentLike( + <<end() + ->start('great-lines') + ->eachLike('line') + ->contentLike('PHP solves all my web problems') + ->end() + ->end() + ->start('rating', ['type' => 'thumbs']) + ->contentLike(7) + ->end() + ->start('rating', ['type' => 'stars']) + ->contentLike(5) + ->end() + ->end() + ->end(); + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->addHeader('Content-Type', 'text/xml') + ->setBody( + json_encode($xmlBuilder->getArray()) + ); + + $config = new MockServerConfig(); + $config + ->setConsumer('xmlConsumer') + ->setProvider('xmlProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('Movies exist') + ->uponReceiving('A get request to /movies') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $moviesResult = new \SimpleXMLElement($service->getMovies()); + $verifyResult = $builder->verify(); + + $this->assertTrue($verifyResult); + $this->assertCount(1, $moviesResult); + } +} diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json new file mode 100644 index 00000000..8ead9538 --- /dev/null +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -0,0 +1,122 @@ +{ + "consumer": { + "name": "xmlConsumer" + }, + "interactions": [ + { + "description": "A get request to /movies", + "providerStates": [ + { + "name": "Movies exist" + } + ], + "request": { + "headers": { + "Accept": "text/xml; charset=UTF8" + }, + "method": "GET", + "path": "/movies" + }, + "response": { + "body": "PHP: Behind the ParserMs. CoderOnlivia ActoraSo, this language. It's like, a programming language. Or is it a\nscripting language? All is revealed in this thrilling horror spoof\nof a documentary.PHP solves all my web problems75", + "headers": { + "Content-Type": "text/xml" + }, + "matchingRules": { + "body": { + "$.movies.movie": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.characters.character": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.characters.character.actor.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.characters.character.name.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.great-lines.line": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.great-lines.line.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.plot.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.movies.movie.rating.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + }, + { + "match": "type" + } + ] + }, + "$.movies.movie.title.#text": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.7", + "mockserver": "1.2.3", + "models": "1.1.9" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "xmlProvider" + } +} \ No newline at end of file diff --git a/example/xml/provider/phpunit.xml b/example/xml/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/xml/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/xml/provider/public/index.php b/example/xml/provider/public/index.php new file mode 100644 index 00000000..458ec3e1 --- /dev/null +++ b/example/xml/provider/public/index.php @@ -0,0 +1,53 @@ +createXMLArray() + ->start('movies') + ->start('movie') + ->add('title', 'PHP: Behind the Parser') + ->start('characters') + ->start('character') + ->add('name', 'Ms. Coder') + ->add('actor', 'Onlivia Actora') + ->end() + ->start('character') + ->add('name', 'Mr. Coder') + ->add('actor', 'El ActÓr') + ->end() + ->end() + ->add('plot', <<start('great-lines') + ->add('line', 'PHP solves all my web problems') + ->end() + ->add('rating', 7, ['type' => 'thumbs']) + ->add('rating', 5, ['type' => 'stars']) + ->end() + ->end(); + +$app = AppFactory::create(); + +$app->get('/movies', function (Request $request, Response $response) use ($xmlBuilder) { + $response->getBody()->write($xmlBuilder->getXml()); + + return $response->withHeader('Content-Type', 'text/xml'); +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) { + return $response; +}); + +$app->run(); diff --git a/example/xml/provider/tests/PactVerifyTest.php b/example/xml/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..82a64982 --- /dev/null +++ b/example/xml/provider/tests/PactVerifyTest.php @@ -0,0 +1,66 @@ +process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); + + $this->process->start(); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + /** + * This test will run after the web server is started. + */ + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('xmlProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/xmlConsumer-xmlProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} From a05844965a2c12e02dbd8a96eabe183f46948fc2 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 22 Sep 2023 08:54:44 +0700 Subject: [PATCH 091/298] Use Functional Options pattern --- .../tests/Service/HttpClientServiceTest.php | 105 ++++++++++++------ .../xml/pacts/xmlConsumer-xmlProvider.json | 15 ++- example/xml/provider/public/index.php | 63 +++++------ 3 files changed, 107 insertions(+), 76 deletions(-) diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index 8e33abf9..853eb11e 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -23,43 +23,74 @@ public function testGetMovies() $xmlBuilder = new XmlBuilder('1.0', 'UTF-8'); $xmlBuilder - ->start('movies') - ->eachLike('movie') - ->start('title') - ->contentLike('PHP: Behind the Parser') - ->end() - ->start('characters') - ->eachLike('character', [], new Options(examples: 2)) - ->start('name') - ->contentLike('Ms. Coder') - ->end() - ->start('actor') - ->contentLike('Onlivia Actora') - ->end() - ->end() - ->end() - ->start('plot') - ->contentLike( - <<end() - ->start('great-lines') - ->eachLike('line') - ->contentLike('PHP solves all my web problems') - ->end() - ->end() - ->start('rating', ['type' => 'thumbs']) - ->contentLike(7) - ->end() - ->start('rating', ['type' => 'stars']) - ->contentLike(5) - ->end() - ->end() - ->end(); + ->root( + $xmlBuilder->name('movies'), + $xmlBuilder->add( + $xmlBuilder->eachLike(), + $xmlBuilder->name('movie'), + $xmlBuilder->add( + $xmlBuilder->name('title'), + $xmlBuilder->text( + $xmlBuilder->contentLike('PHP: Behind the Parser'), + ), + ), + $xmlBuilder->add( + $xmlBuilder->name('characters'), + $xmlBuilder->add( + $xmlBuilder->eachLike(examples: 2), + $xmlBuilder->name('character'), + $xmlBuilder->add( + $xmlBuilder->name('name'), + $xmlBuilder->text( + $xmlBuilder->contentLike('Ms. Coder'), + ), + ), + $xmlBuilder->add( + $xmlBuilder->name('actor'), + $xmlBuilder->text( + $xmlBuilder->contentLike('Onlivia Actora'), + ), + ), + ), + ), + $xmlBuilder->add( + $xmlBuilder->name('plot'), + $xmlBuilder->text( + $xmlBuilder->contentLike( + <<add( + $xmlBuilder->name('great-lines'), + $xmlBuilder->add( + $xmlBuilder->eachLike(), + $xmlBuilder->name('line'), + $xmlBuilder->text( + $xmlBuilder->contentLike('PHP solves all my web problems'), + ), + ), + ), + $xmlBuilder->add( + $xmlBuilder->name('rating'), + $xmlBuilder->attribute('type', 'thumbs'), + $xmlBuilder->text( + $xmlBuilder->contentLike(7), + ), + ), + $xmlBuilder->add( + $xmlBuilder->name('rating'), + $xmlBuilder->attribute('type', 'stars'), + $xmlBuilder->text( + $xmlBuilder->contentLike(5), + ), + ), + ), + ); $response = new ProviderResponse(); $response diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index 8ead9538..a8e4a8e9 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -28,7 +28,8 @@ "combine": "AND", "matchers": [ { - "match": "type" + "match": "type", + "min": 1 } ] }, @@ -36,7 +37,8 @@ "combine": "AND", "matchers": [ { - "match": "type" + "match": "type", + "min": 1 } ] }, @@ -60,7 +62,8 @@ "combine": "AND", "matchers": [ { - "match": "type" + "match": "type", + "min": 1 } ] }, @@ -108,9 +111,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.7", - "mockserver": "1.2.3", - "models": "1.1.9" + "ffi": "0.4.8", + "mockserver": "1.2.4", + "models": "1.1.11" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/xml/provider/public/index.php b/example/xml/provider/public/index.php index 458ec3e1..c48aec96 100644 --- a/example/xml/provider/public/index.php +++ b/example/xml/provider/public/index.php @@ -1,47 +1,44 @@ createXMLArray() - ->start('movies') - ->start('movie') - ->add('title', 'PHP: Behind the Parser') - ->start('characters') - ->start('character') - ->add('name', 'Ms. Coder') - ->add('actor', 'Onlivia Actora') - ->end() - ->start('character') - ->add('name', 'Mr. Coder') - ->add('actor', 'El ActÓr') - ->end() - ->end() - ->add('plot', <<get('/movies', function (Request $request, Response $response) { + $response->getBody()->write( + << + + + PHP: Behind the Parser + + + Ms. Coder + Onlivia Actora + + + Mr. Coder + El ActÓr + + + So, this language. It's like, a programming language. Or is it a scripting language? All is revealed in this thrilling horror spoof of a documentary. - PLOT) - ->start('great-lines') - ->add('line', 'PHP solves all my web problems') - ->end() - ->add('rating', 7, ['type' => 'thumbs']) - ->add('rating', 5, ['type' => 'stars']) - ->end() - ->end(); - -$app = AppFactory::create(); - -$app->get('/movies', function (Request $request, Response $response) use ($xmlBuilder) { - $response->getBody()->write($xmlBuilder->getXml()); + + + PHP solves all my web problems + + 7 + 5 + + + XML + ); return $response->withHeader('Content-Type', 'text/xml'); }); From 56e7e2032842ca1f9ae269cf376c6d06da3006b4 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 12 Oct 2023 17:12:16 +0100 Subject: [PATCH 092/298] chore: fix merge commit in composer.json --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a588344a..12b3c0a6 100644 --- a/composer.json +++ b/composer.json @@ -56,17 +56,14 @@ "BinaryConsumer\\Tests\\": "example/binary/consumer/tests", "BinaryProvider\\": "example/binary/provider/src", "BinaryProvider\\Tests\\": "example/binary/provider/tests", -<<<<<<< HEAD "MultipartConsumer\\": "example/multipart/consumer/src", "MultipartConsumer\\Tests\\": "example/multipart/consumer/tests", "MultipartProvider\\": "example/multipart/provider/src", - "MultipartProvider\\Tests\\": "example/multipart/provider/tests" -======= + "MultipartProvider\\Tests\\": "example/multipart/provider/tests", "XmlConsumer\\": "example/xml/consumer/src", "XmlConsumer\\Tests\\": "example/xml/consumer/tests", "XmlProvider\\": "example/xml/provider/src", "XmlProvider\\Tests\\": "example/xml/provider/tests" ->>>>>>> 7ee934b (chore: add xml example) } }, "scripts": { From 95454a6d22f2d23f574afa9fdfccafbfc7386640 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 13 Oct 2023 21:57:54 +0700 Subject: [PATCH 093/298] deps: Update pact-php-xml to 0.1.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 12b3c0a6..b1c13c5b 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "phpstan/phpstan": "^1.9", "phpunit/phpunit": ">=8.5.23 <10", "guzzlehttp/guzzle": "^7.8", - "tienvx/pact-php-xml": "dev-main" + "tienvx/pact-php-xml": "^0.1" }, "autoload": { "psr-4": { From d6fd5a2bb7ca4f80c023583d4192e9d3aa55b532 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 22 Oct 2023 13:39:18 +0700 Subject: [PATCH 094/298] fix: Fix fatal error when reponse's status is not set --- src/PhpPact/Consumer/Model/Interaction/StatusTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php index 31e68f4b..28514806 100644 --- a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php @@ -4,7 +4,7 @@ trait StatusTrait { - private int $status; + private int $status = 200; public function getStatus(): int { From c32fc8208cec1d6c37bc78eacda5baac55d67760 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 22 Oct 2023 15:00:50 +0700 Subject: [PATCH 095/298] chore: Add matchers example --- composer.json | 5 +- example/matchers/consumer/phpunit.xml | 11 + .../src/Service/HttpClientService.php | 28 ++ .../consumer/tests/Service/MatchersTest.php | 140 ++++++ .../matchersConsumer-matchersProvider.json | 469 ++++++++++++++++++ example/matchers/provider/phpunit.xml | 11 + example/matchers/provider/public/index.php | 75 +++ .../provider/tests/PactVerifyTest.php | 47 ++ phpunit.xml | 6 + 9 files changed, 791 insertions(+), 1 deletion(-) create mode 100644 example/matchers/consumer/phpunit.xml create mode 100644 example/matchers/consumer/src/Service/HttpClientService.php create mode 100644 example/matchers/consumer/tests/Service/MatchersTest.php create mode 100644 example/matchers/pacts/matchersConsumer-matchersProvider.json create mode 100644 example/matchers/provider/phpunit.xml create mode 100644 example/matchers/provider/public/index.php create mode 100644 example/matchers/provider/tests/PactVerifyTest.php diff --git a/composer.json b/composer.json index b1c13c5b..9183bff3 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,10 @@ "XmlConsumer\\": "example/xml/consumer/src", "XmlConsumer\\Tests\\": "example/xml/consumer/tests", "XmlProvider\\": "example/xml/provider/src", - "XmlProvider\\Tests\\": "example/xml/provider/tests" + "XmlProvider\\Tests\\": "example/xml/provider/tests", + "MatchersConsumer\\": "example/matchers/consumer/src", + "MatchersConsumer\\Tests\\": "example/matchers/consumer/tests", + "MatchersProvider\\Tests\\": "example/matchers/provider/tests" } }, "scripts": { diff --git a/example/matchers/consumer/phpunit.xml b/example/matchers/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/matchers/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/matchers/consumer/src/Service/HttpClientService.php b/example/matchers/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..9ffff90f --- /dev/null +++ b/example/matchers/consumer/src/Service/HttpClientService.php @@ -0,0 +1,28 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getMatchers(): array + { + $response = $this->httpClient->get("{$this->baseUri}/matchers", [ + 'headers' => ['Accept' => 'application/json'], + 'query' => ['ignore' => 'statusCode'], + ]); + + return \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); + } +} diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php new file mode 100644 index 00000000..275cc2ca --- /dev/null +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -0,0 +1,140 @@ +matcher = new Matcher(); + } + + public function testGetMatchers() + { + $request = new ConsumerRequest(); + $request + ->setMethod('GET') + ->setPath($this->matcher->regex('/matchers', '^\/matchers$')) + ->setQuery([ + 'ignore' => 'statusCode', + ]) + ->addHeader('Accept', 'application/json'); + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + 'like' => $this->matcher->like(['key' => 'value']), + 'eachLike' => $this->matcher->eachLike('item'), + 'atLeastLike' => $this->matcher->atLeastLike(1, 5), + 'atMostLike' => $this->matcher->atMostLike(1, 3), + 'constrainedArrayLike' => $this->matcher->constrainedArrayLike('item', 2, 4), + 'regex' => $this->matcher->regex('500 miles', '^\d+ (miles|kilometers)$'), + 'dateISO8601' => $this->matcher->dateISO8601(), + 'timeISO8601' => $this->matcher->timeISO8601(), + 'dateTimeISO8601' => $this->matcher->dateTimeISO8601(), + 'dateTimeWithMillisISO8601' => $this->matcher->dateTimeWithMillisISO8601(), + 'timestampRFC3339' => $this->matcher->timestampRFC3339(), + 'likeBool' => $this->matcher->boolean(), + 'likeInt' => $this->matcher->integer(), + 'likeDecimal' => $this->matcher->decimal(), + 'boolean' => $this->matcher->booleanV3(false), + 'integer' => $this->matcher->integerV3(9), + 'decimal' => $this->matcher->decimalV3(79.01), + 'hexadecimal' => $this->matcher->hexadecimal('F7A16'), + 'uuid' => $this->matcher->uuid('52c9585e-f345-4964-aa28-a45c64b2b2eb'), + 'ipv4Address' => $this->matcher->ipv4Address(), + 'ipv6Address' => $this->matcher->ipv6Address(), + 'email' => $this->matcher->email(), + 'nullValue' => $this->matcher->nullValue(), + 'date' => $this->matcher->date('yyyy-MM-dd', '2015-05-16'), + 'time' => $this->matcher->time('HH:mm::ss', '23:59::58'), + 'datetime' => $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2000-10-31T01:30:00'), + 'likeString' => $this->matcher->string('some string'), + 'equal' => $this->matcher->equal('exact this value'), + 'includes' => $this->matcher->includes('lazy dog'), + 'number' => $this->matcher->number(123), + 'arrayContaining' => $this->matcher->arrayContaining([ + $this->matcher->string('some text'), + $this->matcher->number(111), + $this->matcher->uuid('2fbd41cc-4bbc-44ea-a419-67f767691407'), + ]), + 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), + 'semver' => $this->matcher->semver('10.0.0-alpha4'), + 'contentType' => $this->matcher->contentType('text/html'), + ]); + + $config = new MockServerConfig(); + $config + ->setConsumer('matchersConsumer') + ->setProvider('matchersProvider') + ->setPactDir(__DIR__.'/../../../pacts') + ->setPactSpecificationVersion('4.0.0'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('Get Matchers') + ->uponReceiving('A get request to /matchers') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $matchersResult = $service->getMatchers(); + $verifyResult = $builder->verify(); + + $this->assertTrue($verifyResult); + $this->assertEquals([ + 'like' => ['key' => 'value'], + 'eachLike' => ['item'], + 'atLeastLike' => [1, 1, 1, 1, 1], + 'atMostLike' => [1], + 'constrainedArrayLike' => ['item', 'item'], + 'regex' => '500 miles', + 'dateISO8601' => '2013-02-01', + 'timeISO8601' => 'T22:44:30.652Z', + 'dateTimeISO8601' => '2015-08-06T16:53:10+01:00', + 'dateTimeWithMillisISO8601' => '2015-08-06T16:53:10.123+01:00', + 'timestampRFC3339' => 'Mon, 31 Oct 2016 15:21:41 -0400', + 'likeBool' => true, + 'likeInt' => 13, + 'likeDecimal' => 13.01, + 'boolean' => false, + 'integer' => 9, + 'decimal' => 79.01, + 'hexadecimal' => 'F7A16', + 'uuid' => '52c9585e-f345-4964-aa28-a45c64b2b2eb', + 'ipv4Address' => '127.0.0.13', + 'ipv6Address' => '::ffff:192.0.2.128', + 'email' => 'hello@pact.io', + 'nullValue' => null, + 'date' => '2015-05-16', + 'time' => '23:59::58', + 'datetime' => '2000-10-31T01:30:00', + 'likeString' => 'some string', + 'equal' => 'exact this value', + 'includes' => 'lazy dog', + 'number' => 123, + 'arrayContaining' => [ + 'some text', + 111, + '2fbd41cc-4bbc-44ea-a419-67f767691407', + ], + 'notEmpty' => ['1', '2', '3'], + 'semver' => '10.0.0-alpha4', + 'contentType' => 'text/html', + ], $matchersResult); + } +} diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json new file mode 100644 index 00000000..e5ad5469 --- /dev/null +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -0,0 +1,469 @@ +{ + "consumer": { + "name": "matchersConsumer" + }, + "interactions": [ + { + "description": "A get request to /matchers", + "pending": false, + "providerStates": [ + { + "name": "Get Matchers" + } + ], + "request": { + "headers": { + "Accept": [ + "application/json" + ] + }, + "matchingRules": { + "header": {}, + "path": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^\\/matchers$" + } + ] + }, + "query": {} + }, + "method": "GET", + "path": "/matchers", + "query": { + "ignore": [ + "statusCode" + ] + } + }, + "response": { + "body": { + "content": { + "arrayContaining": [ + "some text", + 111, + "2fbd41cc-4bbc-44ea-a419-67f767691407" + ], + "atLeastLike": [ + 1, + 1, + 1, + 1, + 1 + ], + "atMostLike": [ + 1 + ], + "boolean": false, + "constrainedArrayLike": [ + "item", + "item" + ], + "contentType": "text/html", + "date": "2015-05-16", + "dateISO8601": "2013-02-01", + "dateTimeISO8601": "2015-08-06T16:53:10+01:00", + "dateTimeWithMillisISO8601": "2015-08-06T16:53:10.123+01:00", + "datetime": "2000-10-31T01:30:00", + "decimal": 79.01, + "eachLike": [ + "item" + ], + "email": "hello@pact.io", + "equal": "exact this value", + "hexadecimal": "F7A16", + "includes": "lazy dog", + "integer": 9, + "ipv4Address": "127.0.0.13", + "ipv6Address": "::ffff:192.0.2.128", + "like": { + "key": "value" + }, + "likeBool": true, + "likeDecimal": 13.01, + "likeInt": 13, + "likeString": "some string", + "notEmpty": [ + "1", + "2", + "3" + ], + "nullValue": null, + "number": 123, + "regex": "500 miles", + "semver": "10.0.0-alpha4", + "time": "23:59::58", + "timeISO8601": "T22:44:30.652Z", + "timestampRFC3339": "Mon, 31 Oct 2016 15:21:41 -0400", + "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb" + }, + "contentType": "application/json", + "encoded": false + }, + "headers": { + "Content-Type": [ + "application/json" + ] + }, + "matchingRules": { + "body": { + "$.arrayContaining": { + "combine": "AND", + "matchers": [ + { + "match": "arrayContains", + "variants": [ + { + "index": 0, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + { + "index": 1, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + } + } + }, + { + "index": 2, + "rules": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" + } + ] + } + } + } + ] + } + ] + }, + "$.atLeastLike": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "min": 5 + } + ] + }, + "$.atMostLike": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "max": 3 + } + ] + }, + "$.boolean": { + "combine": "AND", + "matchers": [ + { + "match": "boolean" + } + ] + }, + "$.constrainedArrayLike": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "max": 4, + "min": 2 + } + ] + }, + "$.contentType": { + "combine": "AND", + "matchers": [ + { + "match": "contentType", + "value": "text/html" + } + ] + }, + "$.date": { + "combine": "AND", + "matchers": [ + { + "format": "yyyy-MM-dd", + "match": "date" + } + ] + }, + "$.dateISO8601": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$" + } + ] + }, + "$.dateTimeISO8601": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$" + } + ] + }, + "$.dateTimeWithMillisISO8601": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$" + } + ] + }, + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "format": "YYYY-mm-DD'T'HH:mm:ss", + "match": "datetime" + } + ] + }, + "$.decimal": { + "combine": "AND", + "matchers": [ + { + "match": "decimal" + } + ] + }, + "$.eachLike": { + "combine": "AND", + "matchers": [ + { + "match": "type", + "min": 1 + } + ] + }, + "$.email": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$" + } + ] + }, + "$.equal": { + "combine": "AND", + "matchers": [ + { + "match": "equality" + } + ] + }, + "$.hexadecimal": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-fA-F]+$" + } + ] + }, + "$.includes": { + "combine": "AND", + "matchers": [ + { + "match": "include", + "value": "lazy dog" + } + ] + }, + "$.integer": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.ipv4Address": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^(\\d{1,3}\\.)+\\d{1,3}$" + } + ] + }, + "$.ipv6Address": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" + } + ] + }, + "$.like": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.likeBool": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.likeDecimal": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.likeInt": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.likeString": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.notEmpty": { + "combine": "AND", + "matchers": [ + { + "match": "notEmpty" + } + ] + }, + "$.nullValue": { + "combine": "AND", + "matchers": [ + { + "match": "null" + } + ] + }, + "$.number": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + }, + "$.regex": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^\\d+ (miles|kilometers)$" + } + ] + }, + "$.semver": { + "combine": "AND", + "matchers": [ + { + "match": "semver" + } + ] + }, + "$.time": { + "combine": "AND", + "matchers": [ + { + "format": "HH:mm::ss", + "match": "time" + } + ] + }, + "$.timeISO8601": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$" + } + ] + }, + "$.timestampRFC3339": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$" + } + ] + }, + "$.uuid": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" + } + ] + } + }, + "header": {} + }, + "status": 200 + }, + "transport": "http", + "type": "Synchronous/HTTP" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.10", + "mockserver": "1.2.4", + "models": "1.1.11" + }, + "pactSpecification": { + "version": "4.0" + } + }, + "provider": { + "name": "matchersProvider" + } +} \ No newline at end of file diff --git a/example/matchers/provider/phpunit.xml b/example/matchers/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/matchers/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php new file mode 100644 index 00000000..bf6961e6 --- /dev/null +++ b/example/matchers/provider/public/index.php @@ -0,0 +1,75 @@ +addBodyParsingMiddleware(); + +$app->get('/matchers', function (Request $request, Response $response) { + $response->getBody()->write(\json_encode([ + 'like' => ['key' => 'another value'], + 'eachLike' => ['item 1', 'item 2'], + 'atLeastLike' => [1, 2, 3, 4, 5, 6], + 'atMostLike' => [1, 2], + 'constrainedArrayLike' => ['item 1', 'item 2', 'item 3'], + 'regex' => '800 kilometers', + 'dateISO8601' => '2001-11-21', + 'timeISO8601' => 'T11:22:15.153Z', + 'dateTimeISO8601' => '2004-02-12T15:19:21+00:00', + 'dateTimeWithMillisISO8601' => '2018-11-07T00:25:00.073+01:00', + 'timestampRFC3339' => 'Thu, 01 Dec 1994 16:00:00 +0700', + 'likeBool' => false, + 'likeInt' => 34, + 'likeDecimal' => 24.12, + 'boolean' => true, + 'integer' => 11, + 'decimal' => 25.1, + 'hexadecimal' => '20AC', + 'uuid' => 'e9d2f3a5-6ecc-4bff-8935-84bb6141325a', + 'ipv4Address' => '192.168.1.1', + 'ipv6Address' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334', + 'email' => 'pact@example.com', + 'nullValue' => null, + 'date' => '1997-12-11', + 'time' => '11:01::02', + 'datetime' => '1997-07-16T19:20:30', + 'likeString' => 'another string', + 'equal' => 'exact this value', + 'includes' => 'The quick brown fox jumps over the lazy dog', + 'number' => 112.3, + 'arrayContaining' => [ + 102.3, + 'eb375cad-48cc-4f7f-981b-ea4f1af90bf2', + ], + 'notEmpty' => [111], + 'semver' => '0.27.1-beta2', + 'contentType' => + << + + + +

My First Heading

+

My first paragraph.

+ + + + HTML, + ])); + + return $response->withHeader('Content-Type', 'application/json'); +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + + printf('%s provider state %s with params: %s', $body['action'], $body['state'], json_encode($body['params'])); + + return $response; +}); + +$app->run(); diff --git a/example/matchers/provider/tests/PactVerifyTest.php b/example/matchers/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..8a823f5d --- /dev/null +++ b/example/matchers/provider/tests/PactVerifyTest.php @@ -0,0 +1,47 @@ +process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process->start(); + } + + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('matchersProvider') + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/matchersConsumer-matchersProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/phpunit.xml b/phpunit.xml index f82b56e4..96a878b0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -37,6 +37,12 @@ ./example/message/provider/tests + + ./example/matchers/consumer/tests + + + ./example/matchers/provider/tests + From 06b4d673087708a89f77b44b46df2743bda6dd3b Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 27 Oct 2023 07:30:27 +0700 Subject: [PATCH 096/298] feat: Value for `like()` now can be nullable --- example/matchers/consumer/tests/Service/MatchersTest.php | 2 ++ .../pacts/matchersConsumer-matchersProvider.json | 9 +++++++++ example/matchers/provider/public/index.php | 1 + src/PhpPact/Consumer/Matcher/Matcher.php | 4 ---- tests/PhpPact/Consumer/Matcher/MatcherTest.php | 7 ++++--- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 275cc2ca..447d6b5a 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -36,6 +36,7 @@ public function testGetMatchers() ->addHeader('Content-Type', 'application/json') ->setBody([ 'like' => $this->matcher->like(['key' => 'value']), + 'likeNull' => $this->matcher->like(null), 'eachLike' => $this->matcher->eachLike('item'), 'atLeastLike' => $this->matcher->atLeastLike(1, 5), 'atMostLike' => $this->matcher->atMostLike(1, 3), @@ -98,6 +99,7 @@ public function testGetMatchers() $this->assertTrue($verifyResult); $this->assertEquals([ 'like' => ['key' => 'value'], + 'likeNull' => null, 'eachLike' => ['item'], 'atLeastLike' => [1, 1, 1, 1, 1], 'atMostLike' => [1], diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index e5ad5469..aca3ab7b 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -84,6 +84,7 @@ "likeBool": true, "likeDecimal": 13.01, "likeInt": 13, + "likeNull": null, "likeString": "some string", "notEmpty": [ "1", @@ -359,6 +360,14 @@ } ] }, + "$.likeNull": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, "$.likeString": { "combine": "AND", "matchers": [ diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index bf6961e6..b991881e 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -12,6 +12,7 @@ $app->get('/matchers', function (Request $request, Response $response) { $response->getBody()->write(\json_encode([ 'like' => ['key' => 'another value'], + 'likeNull' => null, 'eachLike' => ['item 1', 'item 2'], 'atLeastLike' => [1, 2, 3, 4, 5, 6], 'atMostLike' => [1, 2], diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 81f04f5b..e81859fd 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -45,10 +45,6 @@ public function somethingLike(mixed $value): array */ public function like(mixed $value): array { - if ($value === null) { - throw new Exception('Value must not be null.'); - } - return [ 'value' => $value, 'pact:matcher:type' => 'type', diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index f5ab12eb..97af28cb 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -19,10 +19,11 @@ protected function setUp(): void /** * @throws Exception */ - public function testLikeNoValue() + public function testLikeNull(): void { - $this->expectException(Exception::class); - $this->matcher->like(null); + $json = \json_encode($this->matcher->like(null)); + + $this->assertEquals('{"value":null,"pact:matcher:type":"type"}', $json); } /** From 64a2b2a14af621d07525245082d5fe26efee99be Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 27 Oct 2023 11:21:42 +0700 Subject: [PATCH 097/298] fix: Fix missing xml example's testsuites --- .../xml/consumer/tests/Service/HttpClientServiceTest.php | 1 - example/xml/provider/tests/PactVerifyTest.php | 2 +- phpunit.xml | 6 ++++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index 853eb11e..c4e31582 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -2,7 +2,6 @@ namespace XmlConsumer\Tests\Service; -use Tienvx\PactPhpXml\Model\Options; use Tienvx\PactPhpXml\XmlBuilder; use XmlConsumer\Service\HttpClientService; use PhpPact\Consumer\InteractionBuilder; diff --git a/example/xml/provider/tests/PactVerifyTest.php b/example/xml/provider/tests/PactVerifyTest.php index 82a64982..5c66e175 100644 --- a/example/xml/provider/tests/PactVerifyTest.php +++ b/example/xml/provider/tests/PactVerifyTest.php @@ -1,6 +1,6 @@ ./example/multipart/provider/tests + + ./example/xml/consumer/tests + + + ./example/xml/provider/tests + ./example/message/consumer/tests From b566bfc1ce0e557edb8dea2cfe2aae0c257f5a11 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 27 Oct 2023 17:02:53 +0700 Subject: [PATCH 098/298] chore: Reuse code in xml example --- example/xml/provider/tests/PactVerifyTest.php | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/example/xml/provider/tests/PactVerifyTest.php b/example/xml/provider/tests/PactVerifyTest.php index 5c66e175..925afc4c 100644 --- a/example/xml/provider/tests/PactVerifyTest.php +++ b/example/xml/provider/tests/PactVerifyTest.php @@ -5,30 +5,20 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\ProviderProcess; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; class PactVerifyTest extends TestCase { - private Process $process; + private ProviderProcess $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); - + $this->process = new ProviderProcess(__DIR__ . '/../public/'); $this->process->start(); - $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', 7202); - $isOpen = is_resource($fp); - if ($isOpen) { - fclose($fp); - } - - return $isOpen; - }); } /** From 3011465b04e82acefd61f91d2a88b56ad39bff6e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 09:25:12 +0700 Subject: [PATCH 099/298] fix: arrayContains matcher doesn't work if variants is array with keys --- .../consumer/tests/Service/MatchersTest.php | 6 +++--- src/PhpPact/Consumer/Matcher/Matcher.php | 2 +- tests/PhpPact/Consumer/Matcher/MatcherTest.php | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 447d6b5a..48893efe 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -67,9 +67,9 @@ public function testGetMatchers() 'includes' => $this->matcher->includes('lazy dog'), 'number' => $this->matcher->number(123), 'arrayContaining' => $this->matcher->arrayContaining([ - $this->matcher->string('some text'), - $this->matcher->number(111), - $this->matcher->uuid('2fbd41cc-4bbc-44ea-a419-67f767691407'), + 'text' => $this->matcher->string('some text'), + 'number' => $this->matcher->number(111), + 'uuid' => $this->matcher->uuid('2fbd41cc-4bbc-44ea-a419-67f767691407'), ]), 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index e81859fd..24018977 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -586,7 +586,7 @@ public function arrayContaining(array $variants): array { return [ 'pact:matcher:type' => 'arrayContains', - 'variants' => $variants, + 'variants' => array_values($variants), ]; } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 97af28cb..616b769a 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -786,6 +786,23 @@ public function testArrayContaining() $this->assertEquals($expected, $actual); } + public function testArrayContainingWithKeys() + { + $expected = [ + 'pact:matcher:type' => 'arrayContains', + 'variants' => [ + 'item 1', + 'item 2' + ], + ]; + $actual = $this->matcher->arrayContaining([ + 'key 1' => 'item 1', + 'key 2' => 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + public function testNotEmpty() { $expected = [ From 7bafeacd98c3b25e5ddf609e6c3ede24e094d411 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 09:35:48 +0700 Subject: [PATCH 100/298] fix: values matcher doesn't work if values is array with keys --- .../consumer/tests/Service/MatchersTest.php | 10 ++++++++++ .../matchersConsumer-matchersProvider.json | 15 ++++++++++++++- example/matchers/provider/public/index.php | 5 +++++ src/PhpPact/Consumer/Matcher/Matcher.php | 2 +- tests/PhpPact/Consumer/Matcher/MatcherTest.php | 17 +++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 447d6b5a..4174640f 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -73,6 +73,11 @@ public function testGetMatchers() ]), 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), + 'values' => $this->matcher->values([ + 'a' => 'a', + 'b' => 'bb', + 'c' => 'ccc', + ]), 'contentType' => $this->matcher->contentType('text/html'), ]); @@ -136,6 +141,11 @@ public function testGetMatchers() ], 'notEmpty' => ['1', '2', '3'], 'semver' => '10.0.0-alpha4', + 'values' => [ + 'a', + 'bb', + 'ccc', + ], 'contentType' => 'text/html', ], $matchersResult); } diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index aca3ab7b..cc42882d 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -98,7 +98,12 @@ "time": "23:59::58", "timeISO8601": "T22:44:30.652Z", "timestampRFC3339": "Mon, 31 Oct 2016 15:21:41 -0400", - "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb" + "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb", + "values": [ + "a", + "bb", + "ccc" + ] }, "contentType": "application/json", "encoded": false @@ -452,6 +457,14 @@ "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" } ] + }, + "$.values": { + "combine": "AND", + "matchers": [ + { + "match": "values" + } + ] } }, "header": {} diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index b991881e..e4d43c5a 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -48,6 +48,11 @@ ], 'notEmpty' => [111], 'semver' => '0.27.1-beta2', + 'values' => [ + 'a', + 'bb', + 'ccc', + ], 'contentType' => << diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index e81859fd..6fef4d1e 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -643,7 +643,7 @@ public function statusCode(string $status): array public function values(array $values): array { return [ - 'value' => $values, + 'value' => array_values($values), 'pact:matcher:type' => 'values', ]; } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 97af28cb..45393493 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -843,6 +843,23 @@ public function testValues() $this->assertEquals($expected, $actual); } + public function testValuesWithKeys() + { + $expected = [ + 'pact:matcher:type' => 'values', + 'value' => [ + 'item 1', + 'item 2' + ], + ]; + $actual = $this->matcher->values([ + 'key 1' => 'item 1', + 'key 2' => 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + public function testContentType() { $expected = [ From a5a109a6d5d73108d32cdff4f6084af25cb30680 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 10:09:01 +0700 Subject: [PATCH 101/298] feat: Add eachKey matcher --- .../consumer/tests/Service/MatchersTest.php | 7 +++++++ .../matchersConsumer-matchersProvider.json | 18 ++++++++++++++++++ example/matchers/provider/public/index.php | 4 ++++ src/PhpPact/Consumer/Matcher/Matcher.php | 17 +++++++++++++++++ .../PhpPact/Consumer/Matcher/MatcherTest.php | 19 +++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 447d6b5a..4d957a3a 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -74,6 +74,10 @@ public function testGetMatchers() 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), 'contentType' => $this->matcher->contentType('text/html'), + 'eachKey' => $this->matcher->eachKey( + ['page 3' => 'example text'], + [$this->matcher->regex(null, '^page \d+$')] + ), ]); $config = new MockServerConfig(); @@ -137,6 +141,9 @@ public function testGetMatchers() 'notEmpty' => ['1', '2', '3'], 'semver' => '10.0.0-alpha4', 'contentType' => 'text/html', + 'eachKey' => [ + 'page 3' => 'example text', + ], ], $matchersResult); } } diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index aca3ab7b..355535da 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -68,6 +68,9 @@ "dateTimeWithMillisISO8601": "2015-08-06T16:53:10.123+01:00", "datetime": "2000-10-31T01:30:00", "decimal": 79.01, + "eachKey": { + "page 3": "example text" + }, "eachLike": [ "item" ], @@ -258,6 +261,21 @@ } ] }, + "$.eachKey": { + "combine": "AND", + "matchers": [ + { + "match": "eachKey", + "rules": [ + { + "match": "regex", + "regex": "^page \\d+$" + } + ], + "value": "{\"page 3\":\"example text\"}" + } + ] + }, "$.eachLike": { "combine": "AND", "matchers": [ diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index b991881e..819b352f 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -60,6 +60,10 @@ HTML, + 'eachKey' => [ + 'page 1' => 'Hello', + 'page 2' => 'World', + ], ])); return $response->withHeader('Content-Type', 'application/json'); diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index e81859fd..2a766297 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -660,4 +660,21 @@ public function contentType(string $contentType): array 'pact:matcher:type' => 'contentType', ]; } + + /** + * Allows defining matching rules to apply to the keys in a map + * + * @param array $values + * @param array $rules + * + * @return array + */ + public function eachKey(array $values, array $rules): array + { + return [ + 'rules' => $rules, + 'value' => $values, + 'pact:matcher:type' => 'eachKey', + ]; + } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 97af28cb..065bf136 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -853,4 +853,23 @@ public function testContentType() $this->assertEquals($expected, $actual); } + + public function testEachKey() + { + $values = [ + 'page 1' => 'Hello', + 'page 2' => 'World', + ]; + $rules = [ + $this->matcher->regex('page 3', '^page \d+$'), + ]; + $expected = [ + 'rules' => $rules, + 'value' => $values, + 'pact:matcher:type' => 'eachKey', + ]; + $actual = $this->matcher->eachKey($values, $rules); + + $this->assertEquals($expected, $actual); + } } From f08a32ae7a6e5ee4d46763ac09305382d50ec791 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 10:09:01 +0700 Subject: [PATCH 102/298] feat: Add eachValue matcher --- .../consumer/tests/Service/MatchersTest.php | 7 +++++++ .../matchersConsumer-matchersProvider.json | 18 +++++++++++++++++ example/matchers/provider/public/index.php | 4 ++++ src/PhpPact/Consumer/Matcher/Matcher.php | 17 ++++++++++++++++ .../PhpPact/Consumer/Matcher/MatcherTest.php | 20 +++++++++++++++++++ 5 files changed, 66 insertions(+) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 447d6b5a..a2b29cfb 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -74,6 +74,10 @@ public function testGetMatchers() 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), 'contentType' => $this->matcher->contentType('text/html'), + 'eachValue' => $this->matcher->eachValue( + ['vehicle 1' => 'car'], + [$this->matcher->regex(null, 'car|bike|motorbike')] + ), ]); $config = new MockServerConfig(); @@ -137,6 +141,9 @@ public function testGetMatchers() 'notEmpty' => ['1', '2', '3'], 'semver' => '10.0.0-alpha4', 'contentType' => 'text/html', + 'eachValue' => [ + 'vehicle 1' => 'car', + ], ], $matchersResult); } } diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index aca3ab7b..b196b389 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -71,6 +71,9 @@ "eachLike": [ "item" ], + "eachValue": { + "vehicle 1": "car" + }, "email": "hello@pact.io", "equal": "exact this value", "hexadecimal": "F7A16", @@ -267,6 +270,21 @@ } ] }, + "$.eachValue": { + "combine": "AND", + "matchers": [ + { + "match": "eachValue", + "rules": [ + { + "match": "regex", + "regex": "car|bike|motorbike" + } + ], + "value": "{\"vehicle 1\":\"car\"}" + } + ] + }, "$.email": { "combine": "AND", "matchers": [ diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index b991881e..e7f866ab 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -60,6 +60,10 @@ HTML, + 'eachValue' => [ + 'item 1' => 'bike', + 'item 2' => 'motorbike', + ], ])); return $response->withHeader('Content-Type', 'application/json'); diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index e81859fd..c9554d7e 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -660,4 +660,21 @@ public function contentType(string $contentType): array 'pact:matcher:type' => 'contentType', ]; } + + /** + * Allows defining matching rules to apply to the values in a collection. For maps, delgates to the Values matcher. + * + * @param array $values + * @param array $rules + * + * @return array + */ + public function eachValue(array $values, array $rules): array + { + return [ + 'rules' => $rules, + 'value' => $values, + 'pact:matcher:type' => 'eachValue', + ]; + } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 97af28cb..d7bd20ed 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -853,4 +853,24 @@ public function testContentType() $this->assertEquals($expected, $actual); } + + public function testEachValue() + { + $values = [ + 'vehicle 1' => 'car', + 'vehicle 2' => 'bike', + 'vehicle 3' => 'motorbike' + ]; + $rules = [ + $this->matcher->regex('car', 'car|bike|motorbike'), + ]; + $expected = [ + 'rules' => $rules, + 'value' => $values, + 'pact:matcher:type' => 'eachValue', + ]; + $actual = $this->matcher->eachValue($values, $rules); + + $this->assertEquals($expected, $actual); + } } From 76c5007e2bedb828b5d87985110bcf7bb255c3e9 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 23 Oct 2023 09:36:55 +0700 Subject: [PATCH 103/298] feat: allow multi values for regex matcher --- .../src/Service/HttpClientService.php | 2 +- .../consumer/tests/Service/MatchersTest.php | 3 +++ .../matchersConsumer-matchersProvider.json | 15 +++++++++++- src/PhpPact/Consumer/Matcher/Matcher.php | 24 +++++++++++-------- .../PhpPact/Consumer/Matcher/MatcherTest.php | 16 +++++++++++++ 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/example/matchers/consumer/src/Service/HttpClientService.php b/example/matchers/consumer/src/Service/HttpClientService.php index 9ffff90f..21376139 100644 --- a/example/matchers/consumer/src/Service/HttpClientService.php +++ b/example/matchers/consumer/src/Service/HttpClientService.php @@ -20,7 +20,7 @@ public function getMatchers(): array { $response = $this->httpClient->get("{$this->baseUri}/matchers", [ 'headers' => ['Accept' => 'application/json'], - 'query' => ['ignore' => 'statusCode'], + 'query' => 'ignore=statusCode&pages=2&pages=3', ]); return \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 7d03857e..8c50a27b 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -27,6 +27,9 @@ public function testGetMatchers() ->setPath($this->matcher->regex('/matchers', '^\/matchers$')) ->setQuery([ 'ignore' => 'statusCode', + 'pages' => [ + json_encode($this->matcher->regex([1], '\d+')), + ], ]) ->addHeader('Accept', 'application/json'); diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index 1b7c83fc..c9b8a6f4 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -28,13 +28,26 @@ } ] }, - "query": {} + "query": { + "$.pages": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "\\d+" + } + ] + } + } }, "method": "GET", "path": "/matchers", "query": { "ignore": [ "statusCode" + ], + "pages": [ + "1" ] } }, diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 1f934cee..d48bd9cb 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -124,16 +124,16 @@ public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $cou /** * Validate that a value will match a regex pattern. * - * @param string|null $value example of what the expected data would be + * @param string|string[]|null $values example of what the expected data would be * @param string $pattern valid Ruby regex pattern * * @return array * * @throws Exception */ - public function term(?string $value, string $pattern): array + public function term(string|array|null $values, string $pattern): array { - if (null === $value) { + if (null === $values) { return [ 'regex' => $pattern, 'pact:matcher:type' => 'regex', @@ -141,16 +141,18 @@ public function term(?string $value, string $pattern): array ]; } - $result = preg_match("/$pattern/", $value); + foreach ((array) $values as $value) { + $result = preg_match("/$pattern/", $value); - if ($result === false || $result === 0) { - $errorCode = preg_last_error(); + if ($result === false || $result === 0) { + $errorCode = preg_last_error(); - throw new Exception("The pattern {$pattern} is not valid for value {$value}. Failed with error code {$errorCode}."); + throw new Exception("The pattern {$pattern} is not valid for value {$value}. Failed with error code {$errorCode}."); + } } return [ - 'value' => $value, + 'value' => $values, 'regex' => $pattern, 'pact:matcher:type' => 'regex', ]; @@ -159,13 +161,15 @@ public function term(?string $value, string $pattern): array /** * Alias for the term matcher. * + * @param string|string[]|null $values + * * @return array * * @throws Exception */ - public function regex(?string $value, string $pattern): array + public function regex(string|array|null $values, string $pattern): array { - return $this->term($value, $pattern); + return $this->term($values, $pattern); } /** diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 2f9c11d1..b0d2aa7b 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -208,6 +208,22 @@ public function testRegex() $this->assertEquals($expected, $actual); } + /** + * @throws Exception + */ + public function testRegexMultiValues() + { + $expected = [ + 'value' => [1, 23], + 'regex' => '\d+', + 'pact:matcher:type' => 'regex', + ]; + + $actual = $this->matcher->regex([1, 23], '\d+'); + + $this->assertEquals($expected, $actual); + } + /** * @throws Exception */ From c19dca8fbeb1da3624bb2f7c8ca352f5087a9ec5 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 14:13:44 +0700 Subject: [PATCH 104/298] chore: Showcase different behaviors of PHP when receiving multiple values of query parameter --- .../src/Service/HttpClientService.php | 2 +- .../consumer/tests/Service/MatchersTest.php | 17 +++++++++++-- .../matchersConsumer-matchersProvider.json | 24 ++++++++++++++++++- example/matchers/provider/public/index.php | 1 + 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/example/matchers/consumer/src/Service/HttpClientService.php b/example/matchers/consumer/src/Service/HttpClientService.php index 21376139..efbf8338 100644 --- a/example/matchers/consumer/src/Service/HttpClientService.php +++ b/example/matchers/consumer/src/Service/HttpClientService.php @@ -20,7 +20,7 @@ public function getMatchers(): array { $response = $this->httpClient->get("{$this->baseUri}/matchers", [ 'headers' => ['Accept' => 'application/json'], - 'query' => 'ignore=statusCode&pages=2&pages=3', + 'query' => 'ignore=statusCode&pages=2&pages=3&locales[]=fr-BE&locales[]=ru-RU', ]); return \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 8c50a27b..7997a3ae 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -27,8 +27,11 @@ public function testGetMatchers() ->setPath($this->matcher->regex('/matchers', '^\/matchers$')) ->setQuery([ 'ignore' => 'statusCode', - 'pages' => [ - json_encode($this->matcher->regex([1], '\d+')), + 'pages' => [ // Consumer send multiple values, but provider receive single (last) value + json_encode($this->matcher->regex([1, 22], '\d+')), + ], + 'locales[]' => [ // Consumer send multiple values, provider receive all values + json_encode($this->matcher->regex(['en-US', 'en-AU'], '^[a-z]{2}-[A-Z]{2}$')), ], ]) ->addHeader('Accept', 'application/json'); @@ -90,6 +93,11 @@ public function testGetMatchers() ['vehicle 1' => 'car'], [$this->matcher->regex(null, 'car|bike|motorbike')] ), + 'query' => [ + 'ignore' => 'statusCode', + 'pages' => '22', + 'locales' => ['en-US', 'en-AU'], + ], ]); $config = new MockServerConfig(); @@ -164,6 +172,11 @@ public function testGetMatchers() 'eachValue' => [ 'vehicle 1' => 'car', ], + 'query' => [ + 'ignore' => 'statusCode', + 'pages' => '22', + 'locales' => ['en-US', 'en-AU'], + ], ], $matchersResult); } } diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index c9b8a6f4..e4756600 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -37,6 +37,15 @@ "regex": "\\d+" } ] + }, + "$['locales[]']": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[a-z]{2}-[A-Z]{2}$" + } + ] } } }, @@ -46,8 +55,13 @@ "ignore": [ "statusCode" ], + "locales[]": [ + "en-US", + "en-AU" + ], "pages": [ - "1" + "1", + "22" ] } }, @@ -112,6 +126,14 @@ ], "nullValue": null, "number": 123, + "query": { + "ignore": "statusCode", + "locales": [ + "en-US", + "en-AU" + ], + "pages": "22" + }, "regex": "500 miles", "semver": "10.0.0-alpha4", "time": "23:59::58", diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 104bd199..323f4532 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -73,6 +73,7 @@ 'item 1' => 'bike', 'item 2' => 'motorbike', ], + 'query' => $request->getQueryParams(), ])); return $response->withHeader('Content-Type', 'application/json'); From 8d557068e66774cda1270404e44065123eabfdc2 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 6 Nov 2023 22:23:19 +0700 Subject: [PATCH 105/298] revert: Values matcher accept keys again --- .../consumer/tests/Service/MatchersTest.php | 20 +++++++++++++ .../matchersConsumer-matchersProvider.json | 28 ++++++++++++++++++- example/matchers/provider/public/index.php | 16 +++++++++-- src/PhpPact/Consumer/Matcher/Matcher.php | 2 +- .../PhpPact/Consumer/Matcher/MatcherTest.php | 4 +-- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 7997a3ae..1eb560a0 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -70,6 +70,11 @@ public function testGetMatchers() 'datetime' => $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2000-10-31T01:30:00'), 'likeString' => $this->matcher->string('some string'), 'equal' => $this->matcher->equal('exact this value'), + 'equalArray' => $this->matcher->equal([ + 'a', + 'bb', + 'ccc', + ]), 'includes' => $this->matcher->includes('lazy dog'), 'number' => $this->matcher->number(123), 'arrayContaining' => $this->matcher->arrayContaining([ @@ -80,6 +85,11 @@ public function testGetMatchers() 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), 'values' => $this->matcher->values([ + 'a', + 'bb', + 'ccc', + ]), + 'valuesWithKeys' => $this->matcher->values([ 'a' => 'a', 'b' => 'bb', 'c' => 'ccc', @@ -151,6 +161,11 @@ public function testGetMatchers() 'datetime' => '2000-10-31T01:30:00', 'likeString' => 'some string', 'equal' => 'exact this value', + 'equalArray' => [ + 'a', + 'bb', + 'ccc', + ], 'includes' => 'lazy dog', 'number' => 123, 'arrayContaining' => [ @@ -165,6 +180,11 @@ public function testGetMatchers() 'bb', 'ccc', ], + 'valuesWithKeys' => [ + 'a' => 'a', + 'b' => 'bb', + 'c' => 'ccc', + ], 'contentType' => 'text/html', 'eachKey' => [ 'page 3' => 'example text', diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index e4756600..4495facd 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -106,6 +106,11 @@ }, "email": "hello@pact.io", "equal": "exact this value", + "equalArray": [ + "a", + "bb", + "ccc" + ], "hexadecimal": "F7A16", "includes": "lazy dog", "integer": 9, @@ -144,7 +149,12 @@ "a", "bb", "ccc" - ] + ], + "valuesWithKeys": { + "a": "a", + "b": "bb", + "c": "ccc" + } }, "contentType": "application/json", "encoded": false @@ -360,6 +370,14 @@ } ] }, + "$.equalArray": { + "combine": "AND", + "matchers": [ + { + "match": "equality" + } + ] + }, "$.hexadecimal": { "combine": "AND", "matchers": [ @@ -536,6 +554,14 @@ "match": "values" } ] + }, + "$.valuesWithKeys": { + "combine": "AND", + "matchers": [ + { + "match": "values" + } + ] } }, "header": {} diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 323f4532..1876e555 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -40,6 +40,11 @@ 'datetime' => '1997-07-16T19:20:30', 'likeString' => 'another string', 'equal' => 'exact this value', + 'equalArray' => [ + 'a', + 'bb', + 'ccc', + ], 'includes' => 'The quick brown fox jumps over the lazy dog', 'number' => 112.3, 'arrayContaining' => [ @@ -48,10 +53,17 @@ ], 'notEmpty' => [111], 'semver' => '0.27.1-beta2', - 'values' => [ + 'values' => [ // Missing ending values are OK, additional values are NOT OK 'a', 'bb', - 'ccc', + //'ccc', + //'dddd', + ], + 'valuesWithKeys' => [ // Missing keys are OK, additional keys are NOT OK + //'a' => 'a', + 'b' => 'bb', + //'c' => 'ccc', + //'d' => 'dddd', ], 'contentType' => << array_values($values), + 'value' => $values, 'pact:matcher:type' => 'values', ]; } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index b0d2aa7b..ff8dda22 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -881,8 +881,8 @@ public function testValuesWithKeys() $expected = [ 'pact:matcher:type' => 'values', 'value' => [ - 'item 1', - 'item 2' + 'key 1' => 'item 1', + 'key 2' => 'item 2' ], ]; $actual = $this->matcher->values([ From 00f33c18aadddaaf105478cc6c822791e8bfedec Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 16 Nov 2023 08:49:01 +0700 Subject: [PATCH 106/298] deps: Update ffi library --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 9183bff3..e890126b 100644 --- a/composer.json +++ b/composer.json @@ -82,12 +82,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.9", + "version": "0.4.10", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.9", + "version": "0.4.10", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From 021d5c049c38ee1f4b90705ef3aa7979e499a53a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 11 Nov 2023 19:52:44 +0700 Subject: [PATCH 107/298] feat: Support inheritance for body --- .../Registry/Interaction/Body/AbstractBodyRegistry.php | 9 ++++----- .../Interaction/Body/MessageContentsRegistry.php | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php index 2bb5d920..5f66fbca 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -23,10 +23,10 @@ public function __construct( public function withBody(Text|Binary|Multipart $body): void { - $success = match ($body::class) { - Binary::class => $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), - Text::class => $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), - Multipart::class => array_reduce( + $success = match (true) { + $body instanceof Binary => $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), + $body instanceof Text => $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), + $body instanceof Multipart => array_reduce( $body->getParts(), function (bool $success, Part $part) use ($body) { $result = $this->client->call('pactffi_with_multipart_file_v2', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); @@ -38,7 +38,6 @@ function (bool $success, Part $part) use ($body) { }, true ), - default => false, }; if (!$success) { throw new InteractionBodyNotAddedException(); diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php index f352eb88..7aae0fc8 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php @@ -23,11 +23,10 @@ public function __construct( public function withBody(Text|Binary|Multipart $body): void { - $success = match ($body::class) { - Binary::class => $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), - Text::class => $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), - Multipart::class => throw new BodyNotSupportedException('Message does not support multipart'), - default => false, + $success = match (true) { + $body instanceof Binary => $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), + $body instanceof Text => $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), + $body instanceof Multipart => throw new BodyNotSupportedException('Message does not support multipart'), }; if (!$success) { throw new MessageContentsNotAddedException(); From 71a3c06d84070773ba1a027417101085b8ddee8d Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 21 Nov 2023 10:09:17 +0700 Subject: [PATCH 108/298] feat: Allow capture verifier output --- .../ProviderVerifier/Model/VerifierLoggerInterface.php | 8 ++++++++ src/PhpPact/Standalone/ProviderVerifier/Verifier.php | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/VerifierLoggerInterface.php diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierLoggerInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierLoggerInterface.php new file mode 100644 index 00000000..5009fa2c --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierLoggerInterface.php @@ -0,0 +1,8 @@ +client = new Client(); $this @@ -207,6 +208,9 @@ public function addBroker(BrokerInterface $broker): self public function verify(): bool { $error = $this->client->call('pactffi_verifier_execute', $this->handle); + if ($this->logger) { + $this->logger->log($this->client->call('pactffi_verifier_json', $this->handle)); + } $this->client->call('pactffi_verifier_shutdown', $this->handle); return !$error; From 83dfdb8339f9acd5a06f38987248d3f82bc330d5 Mon Sep 17 00:00:00 2001 From: tienvx Date: Tue, 21 Nov 2023 15:32:52 +0700 Subject: [PATCH 109/298] feat: Allow capture mock server mismatches --- src/PhpPact/Consumer/Service/MockServer.php | 27 +++++++++++++++---- src/PhpPact/Service/LoggerInterface.php | 8 ++++++ .../Model/VerifierLoggerInterface.php | 8 ------ .../Standalone/ProviderVerifier/Verifier.php | 4 +-- 4 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 src/PhpPact/Service/LoggerInterface.php delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/VerifierLoggerInterface.php diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index 44aa9179..4d0a7386 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -2,11 +2,13 @@ namespace PhpPact\Consumer\Service; +use FFI; use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; +use PhpPact\Service\LoggerInterface; use PhpPact\Standalone\MockService\MockServerConfigInterface; class MockServer implements MockServerInterface @@ -14,7 +16,8 @@ class MockServer implements MockServerInterface public function __construct( private ClientInterface $client, private PactRegistryInterface $pactRegistry, - private MockServerConfigInterface $config + private MockServerConfigInterface $config, + private ?LoggerInterface $logger = null ) { } @@ -37,17 +40,19 @@ public function start(): void public function verify(): bool { - $matched = $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); - try { + $matched = $this->isMatched(); + if ($matched) { $this->writePact(); + } elseif ($this->logger) { + $this->logger->log($this->getMismatches()); } + + return $matched; } finally { $this->cleanUp(); } - - return $matched; } protected function getTransport(): string @@ -78,4 +83,16 @@ private function cleanUp(): void $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); $this->pactRegistry->deletePact(); } + + private function isMatched(): bool + { + return $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); + } + + private function getMismatches(): string + { + $cData = $this->client->call('pactffi_mock_server_mismatches', $this->config->getPort()); + + return FFI::string($cData); + } } diff --git a/src/PhpPact/Service/LoggerInterface.php b/src/PhpPact/Service/LoggerInterface.php new file mode 100644 index 00000000..9e2e9511 --- /dev/null +++ b/src/PhpPact/Service/LoggerInterface.php @@ -0,0 +1,8 @@ +client = new Client(); $this From 5e61140d52f8dc15c7d7f46de7eeb3fa94931193 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 28 Nov 2023 08:23:40 +0700 Subject: [PATCH 110/298] deps: Update ffi library to 0.4.11 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e890126b..a518e3b8 100644 --- a/composer.json +++ b/composer.json @@ -82,12 +82,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.10", + "version": "0.4.11", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.10", + "version": "0.4.11", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From e5db014b1ba07791e9f18c229f99c175e9f00c72 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 28 Oct 2023 09:07:58 +0700 Subject: [PATCH 111/298] chore: Add status code matcher example --- .../src/Service/HttpClientService.php | 10 +++---- .../consumer/tests/Service/MatchersTest.php | 14 +++++----- .../matchersConsumer-matchersProvider.json | 23 ++++++++++------ example/matchers/provider/public/index.php | 4 ++- src/PhpPact/Consumer/Matcher/Matcher.php | 26 +++++++++++++++++-- .../Model/Interaction/StatusTrait.php | 15 ++++++++--- .../Interaction/Part/ResponseRegistry.php | 4 +-- .../Part/ResponseRegistryInterface.php | 2 +- .../PhpPact/Consumer/Matcher/MatcherTest.php | 7 +++-- 9 files changed, 74 insertions(+), 31 deletions(-) diff --git a/example/matchers/consumer/src/Service/HttpClientService.php b/example/matchers/consumer/src/Service/HttpClientService.php index efbf8338..8b857165 100644 --- a/example/matchers/consumer/src/Service/HttpClientService.php +++ b/example/matchers/consumer/src/Service/HttpClientService.php @@ -3,6 +3,7 @@ namespace MatchersConsumer\Service; use GuzzleHttp\Client; +use Psr\Http\Message\ResponseInterface; class HttpClientService { @@ -16,13 +17,12 @@ public function __construct(string $baseUri) $this->baseUri = $baseUri; } - public function getMatchers(): array + public function sendRequest(): ResponseInterface { - $response = $this->httpClient->get("{$this->baseUri}/matchers", [ + return $this->httpClient->get("{$this->baseUri}/matchers", [ 'headers' => ['Accept' => 'application/json'], - 'query' => 'ignore=statusCode&pages=2&pages=3&locales[]=fr-BE&locales[]=ru-RU', + 'query' => 'pages=2&pages=3&locales[]=fr-BE&locales[]=ru-RU', + 'http_errors' => false, ]); - - return \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); } } diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 1eb560a0..b915525f 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -4,6 +4,7 @@ use MatchersConsumer\Service\HttpClientService; use PhpPact\Consumer\InteractionBuilder; +use PhpPact\Consumer\Matcher\HttpStatus; use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; @@ -26,7 +27,6 @@ public function testGetMatchers() ->setMethod('GET') ->setPath($this->matcher->regex('/matchers', '^\/matchers$')) ->setQuery([ - 'ignore' => 'statusCode', 'pages' => [ // Consumer send multiple values, but provider receive single (last) value json_encode($this->matcher->regex([1, 22], '\d+')), ], @@ -38,7 +38,7 @@ public function testGetMatchers() $response = new ProviderResponse(); $response - ->setStatus(200) + ->setStatus($this->matcher->statusCode(HttpStatus::SERVER_ERROR, 512)) ->addHeader('Content-Type', 'application/json') ->setBody([ 'like' => $this->matcher->like(['key' => 'value']), @@ -104,7 +104,6 @@ public function testGetMatchers() [$this->matcher->regex(null, 'car|bike|motorbike')] ), 'query' => [ - 'ignore' => 'statusCode', 'pages' => '22', 'locales' => ['en-US', 'en-AU'], ], @@ -127,10 +126,14 @@ public function testGetMatchers() ->willRespondWith($response); $service = new HttpClientService($config->getBaseUri()); - $matchersResult = $service->getMatchers(); + $response = $service->sendRequest(); $verifyResult = $builder->verify(); + $statusCode = $response->getStatusCode(); + $body = \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); + $this->assertTrue($verifyResult); + $this->assertSame(512, $statusCode); $this->assertEquals([ 'like' => ['key' => 'value'], 'likeNull' => null, @@ -193,10 +196,9 @@ public function testGetMatchers() 'vehicle 1' => 'car', ], 'query' => [ - 'ignore' => 'statusCode', 'pages' => '22', 'locales' => ['en-US', 'en-AU'], ], - ], $matchersResult); + ], $body); } } diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index 4495facd..b7ac9422 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -52,9 +52,6 @@ "method": "GET", "path": "/matchers", "query": { - "ignore": [ - "statusCode" - ], "locales[]": [ "en-US", "en-AU" @@ -132,7 +129,6 @@ "nullValue": null, "number": 123, "query": { - "ignore": "statusCode", "locales": [ "en-US", "en-AU" @@ -564,9 +560,20 @@ ] } }, - "header": {} + "header": {}, + "status": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "statusCode", + "status": "serverError" + } + ] + } + } }, - "status": 200 + "status": 512 }, "transport": "http", "type": "Synchronous/HTTP" @@ -574,9 +581,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.10", + "ffi": "0.4.11", "mockserver": "1.2.4", - "models": "1.1.11" + "models": "1.1.12" }, "pactSpecification": { "version": "4.0" diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 1876e555..b6ece2d1 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -88,7 +88,9 @@ 'query' => $request->getQueryParams(), ])); - return $response->withHeader('Content-Type', 'application/json'); + return $response + ->withHeader('Content-Type', 'application/json') + ->withStatus(503); }); $app->post('/pact-change-state', function (Request $request, Response $response) { diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 7e13a999..0bcbe3ea 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -625,14 +625,36 @@ public function semver(string $value): array * * @return array */ - public function statusCode(string $status): array + public function statusCode(string $status, ?int $value = null): array { if (!in_array($status, HttpStatus::all())) { throw new Exception(sprintf("Status '%s' is not supported. Supported status are: %s", $status, implode(', ', HttpStatus::all()))); } + if (null === $value) { + [$min, $max] = match($status) { + HttpStatus::INFORMATION => [100, 199], + HttpStatus::SUCCESS => [200, 299], + HttpStatus::REDIRECT => [300, 399], + HttpStatus::CLIENT_ERROR => [400, 499], + HttpStatus::SERVER_ERROR => [500, 599], + HttpStatus::NON_ERROR => [100, 399], + HttpStatus::ERROR => [400, 599], + default => [100, 199], // Can't happen, just to make PHPStan happy + }; + + return [ + 'pact:generator:type' => 'RandomInt', + 'min' => $min, + 'max' => $max, + 'status' => $status, + 'pact:matcher:type' => 'statusCode', + ]; + } + return [ - 'status' => $status, + 'value' => $value, + 'status' => $status, 'pact:matcher:type' => 'statusCode', ]; } diff --git a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php index 28514806..b6180f7b 100644 --- a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php @@ -2,18 +2,25 @@ namespace PhpPact\Consumer\Model\Interaction; +use JsonException; + trait StatusTrait { - private int $status = 200; + private string $status = '200'; - public function getStatus(): int + public function getStatus(): string { return $this->status; } - public function setStatus(int $status): self + /** + * @param int|array $status + * + * @throws JsonException + */ + public function setStatus(int|array $status): self { - $this->status = $status; + $this->status = is_array($status) ? json_encode($status, JSON_THROW_ON_ERROR) : (string) $status; return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php index b74c0078..e5c0d4d3 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php @@ -19,9 +19,9 @@ public function __construct( parent::__construct($client, $interactionRegistry, $responseBodyRegistry ?? new ResponseBodyRegistry($client, $interactionRegistry)); } - public function withResponse(int $status): self + public function withResponse(string $status): self { - $this->client->call('pactffi_response_status', $this->getInteractionId(), $status); + $this->client->call('pactffi_response_status_v2', $this->getInteractionId(), $status); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php index 05f56e52..fdcf0258 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php @@ -4,5 +4,5 @@ interface ResponseRegistryInterface extends PartRegistryInterface { - public function withResponse(int $status): self; + public function withResponse(string $status): self; } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index ff8dda22..23aeb0fe 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -851,8 +851,11 @@ public function testInvalidStatusCode() public function testValidStatusCode() { $expected = [ - 'status' => 'success', - 'pact:matcher:type' => 'statusCode', + 'pact:generator:type' => 'RandomInt', + 'min' => 200, + 'max' => 299, + 'status' => 'success', + 'pact:matcher:type' => 'statusCode', ]; $actual = $this->matcher->statusCode(HttpStatus::SUCCESS); From de37e12f34e12c1aec578f7908d3ab983abf6a5c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 25 Nov 2023 20:28:39 +0700 Subject: [PATCH 112/298] chore: Add generators example --- composer.json | 5 +- example/generators/consumer/phpunit.xml | 11 + .../src/Service/HttpClientService.php | 28 ++ .../consumer/tests/Service/GeneratorsTest.php | 105 +++++++ ...generatorsConsumer-generatorsProvider.json | 260 ++++++++++++++++++ example/generators/provider/phpunit.xml | 11 + example/generators/provider/public/index.php | 44 +++ .../provider/tests/PactVerifyTest.php | 47 ++++ phpunit.xml | 6 + 9 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 example/generators/consumer/phpunit.xml create mode 100644 example/generators/consumer/src/Service/HttpClientService.php create mode 100644 example/generators/consumer/tests/Service/GeneratorsTest.php create mode 100644 example/generators/pacts/generatorsConsumer-generatorsProvider.json create mode 100644 example/generators/provider/phpunit.xml create mode 100644 example/generators/provider/public/index.php create mode 100644 example/generators/provider/tests/PactVerifyTest.php diff --git a/composer.json b/composer.json index a518e3b8..1c1df957 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,10 @@ "XmlProvider\\Tests\\": "example/xml/provider/tests", "MatchersConsumer\\": "example/matchers/consumer/src", "MatchersConsumer\\Tests\\": "example/matchers/consumer/tests", - "MatchersProvider\\Tests\\": "example/matchers/provider/tests" + "MatchersProvider\\Tests\\": "example/matchers/provider/tests", + "GeneratorsConsumer\\": "example/generators/consumer/src", + "GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests", + "GeneratorsProvider\\Tests\\": "example/generators/provider/tests" } }, "scripts": { diff --git a/example/generators/consumer/phpunit.xml b/example/generators/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/generators/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/generators/consumer/src/Service/HttpClientService.php b/example/generators/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..bebbdf37 --- /dev/null +++ b/example/generators/consumer/src/Service/HttpClientService.php @@ -0,0 +1,28 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function sendRequest(): ResponseInterface + { + return $this->httpClient->get("{$this->baseUri}/generators", [ + 'headers' => ['Accept' => 'application/json'], + 'json' => ['id' => 112], + 'http_errors' => false, + ]); + } +} diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php new file mode 100644 index 00000000..5295689e --- /dev/null +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -0,0 +1,105 @@ +matcher = new Matcher(); + } + + public function testGetMatchers() + { + $request = new ConsumerRequest(); + $request + ->setMethod('GET') + ->setPath('/generators') + ->addHeader('Accept', 'application/json') + ->setBody([ + 'id' => $this->matcher->fromProviderState($this->matcher->integer(), '${id}') + ]); + + $response = new ProviderResponse(); + $response + ->setStatus($this->matcher->statusCode(HttpStatus::CLIENT_ERROR)) + ->addHeader('Content-Type', 'application/json') + ->setBody([ + 'regex' => $this->matcher->regex(null, $regexWithoutAnchors = '\d+ (miles|kilometers)'), + 'boolean' => $this->matcher->booleanV3(null), + 'integer' => $this->matcher->integerV3(null), + 'decimal' => $this->matcher->decimalV3(null), + 'hexadecimal' => $this->matcher->hexadecimal(null), + 'uuid' => $this->matcher->uuid(null), + 'date' => $this->matcher->date('yyyy-MM-dd', null), + 'time' => $this->matcher->time('HH:mm::ss', null), + 'datetime' => $this->matcher->datetime("YYYY-MM-D'T'HH:mm:ss", null), + 'string' => $this->matcher->string(null), + 'number' => $this->matcher->number(null), + 'requestId' => 222, + ]); + + $config = new MockServerConfig(); + $config + ->setConsumer('generatorsConsumer') + ->setProvider('generatorsProvider') + ->setPactDir(__DIR__.'/../../../pacts') + ->setPactSpecificationVersion('4.0.0'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('Get Generators') + ->uponReceiving('A get request to /generators') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $response = $service->sendRequest(); + $verifyResult = $builder->verify(); + + $statusCode = $response->getStatusCode(); + $body = \json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); + + $this->assertTrue($verifyResult); + $this->assertThat( + $statusCode, + $this->logicalAnd( + $this->greaterThanOrEqual(400), + $this->lessThanOrEqual(499) + ) + ); + $this->assertRegExp('/^' . $regexWithoutAnchors . '$/', $body['regex']); + $this->assertIsBool($body['boolean']); + $this->assertIsInt($body['integer']); + $this->assertIsFloat($body['decimal'] + 0); + $this->assertRegExp('/' . Matcher::HEX_FORMAT . '/', $body['hexadecimal']); + $this->assertRegExp('/' . Matcher::UUID_V4_FORMAT . '/', $body['uuid']); + $this->assertTrue($this->validateDateTime($body['date'], 'Y-m-d')); + $this->assertTrue($this->validateDateTime($body['time'], 'H:i::s')); + $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-z\TH:i:s")); + $this->assertIsString($body['string']); + $this->assertIsNumeric($body['number']); + $this->assertSame(222, $body['requestId']); + } + + private function validateDateTime(string $datetime, string $format): bool + { + $value = DateTime::createFromFormat($format, $datetime); + + return $value && $value->format($format) === $datetime; + } +} diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json new file mode 100644 index 00000000..8ab6f942 --- /dev/null +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -0,0 +1,260 @@ +{ + "consumer": { + "name": "generatorsConsumer" + }, + "interactions": [ + { + "description": "A get request to /generators", + "pending": false, + "providerStates": [ + { + "name": "Get Generators" + } + ], + "request": { + "body": { + "content": { + "id": 13 + }, + "contentType": "application/json", + "encoded": false + }, + "generators": { + "body": { + "$.id": { + "expression": "${id}", + "type": "ProviderState" + } + } + }, + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json" + ] + }, + "matchingRules": { + "body": { + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, + "header": {} + }, + "method": "GET", + "path": "/generators" + }, + "response": { + "body": { + "content": { + "boolean": null, + "date": null, + "datetime": null, + "decimal": null, + "hexadecimal": null, + "integer": null, + "number": null, + "regex": null, + "requestId": 222, + "string": "some string", + "time": null, + "uuid": null + }, + "contentType": "application/json", + "encoded": false + }, + "generators": { + "body": { + "$.boolean": { + "type": "RandomBoolean" + }, + "$.date": { + "format": "yyyy-MM-dd", + "type": "Date" + }, + "$.datetime": { + "format": "YYYY-MM-D'T'HH:mm:ss", + "type": "DateTime" + }, + "$.decimal": { + "digits": 10, + "type": "RandomDecimal" + }, + "$.hexadecimal": { + "digits": 10, + "type": "RandomHexadecimal" + }, + "$.integer": { + "max": 10, + "min": 0, + "type": "RandomInt" + }, + "$.number": { + "max": 10, + "min": 0, + "type": "RandomInt" + }, + "$.regex": { + "regex": "\\d+ (miles|kilometers)", + "type": "Regex" + }, + "$.string": { + "size": 10, + "type": "RandomString" + }, + "$.time": { + "format": "HH:mm::ss", + "type": "Time" + }, + "$.uuid": { + "type": "Uuid" + } + }, + "status": { + "max": 499, + "min": 400, + "type": "RandomInt" + } + }, + "headers": { + "Content-Type": [ + "application/json" + ] + }, + "matchingRules": { + "body": { + "$.boolean": { + "combine": "AND", + "matchers": [ + { + "match": "boolean" + } + ] + }, + "$.date": { + "combine": "AND", + "matchers": [ + { + "format": "yyyy-MM-dd", + "match": "date" + } + ] + }, + "$.datetime": { + "combine": "AND", + "matchers": [ + { + "format": "YYYY-MM-D'T'HH:mm:ss", + "match": "datetime" + } + ] + }, + "$.decimal": { + "combine": "AND", + "matchers": [ + { + "match": "decimal" + } + ] + }, + "$.hexadecimal": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-fA-F]+$" + } + ] + }, + "$.integer": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.number": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + }, + "$.regex": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "\\d+ (miles|kilometers)" + } + ] + }, + "$.string": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.time": { + "combine": "AND", + "matchers": [ + { + "format": "HH:mm::ss", + "match": "time" + } + ] + }, + "$.uuid": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" + } + ] + } + }, + "header": {}, + "status": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "statusCode", + "status": "clientError" + } + ] + } + } + }, + "status": 0 + }, + "transport": "http", + "type": "Synchronous/HTTP" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.11", + "mockserver": "1.2.4", + "models": "1.1.12" + }, + "pactSpecification": { + "version": "4.0" + } + }, + "provider": { + "name": "generatorsProvider" + } +} \ No newline at end of file diff --git a/example/generators/provider/phpunit.xml b/example/generators/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/generators/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/generators/provider/public/index.php b/example/generators/provider/public/index.php new file mode 100644 index 00000000..208d9a5f --- /dev/null +++ b/example/generators/provider/public/index.php @@ -0,0 +1,44 @@ +addBodyParsingMiddleware(); + +$app->get('/generators', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + $response->getBody()->write(\json_encode([ + 'regex' => '800 kilometers', + 'boolean' => true, + 'integer' => 11, + 'decimal' => 25.1, + 'hexadecimal' => '20AC', + 'uuid' => 'e9d2f3a5-6ecc-4bff-8935-84bb6141325a', + 'date' => '1997-12-11', + 'time' => '11:01::02', + 'datetime' => '1997-07-16T19:20:30', + 'string' => 'another string', + 'number' => 112.3, + 'requestId' => $body['id'], + ])); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withStatus(400); +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) { + $response->getBody()->write(\json_encode([ + 'id' => 222, + ])); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withStatus(200); +}); + +$app->run(); diff --git a/example/generators/provider/tests/PactVerifyTest.php b/example/generators/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..86f8ea05 --- /dev/null +++ b/example/generators/provider/tests/PactVerifyTest.php @@ -0,0 +1,47 @@ +process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process->start(); + } + + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('generatorsProvider') + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/generatorsConsumer-generatorsProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/phpunit.xml b/phpunit.xml index e9cca0ee..e300d5a8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -49,6 +49,12 @@ ./example/matchers/provider/tests + + ./example/generators/consumer/tests + + + ./example/generators/provider/tests + From d2d02fd00a9ba9f5227112c5c566bf7a3fab4584 Mon Sep 17 00:00:00 2001 From: tienvx Date: Thu, 16 Nov 2023 23:10:10 +0700 Subject: [PATCH 113/298] chore: Clean up and use matchers/generators for message example --- .../consumer/src/ExampleMessageConsumer.php | 17 ++----- example/message/consumer/src/receive.php | 2 +- .../tests/ExampleMessageConsumerTest.php | 38 ++++---------- .../messageConsumer-messageProvider.json | 40 ++++++++------- .../message/provider/src/ExampleMessage.php | 28 ++++++++++ .../provider/src/ExampleMessageProvider.php | 51 ------------------- .../message/provider/src/ExampleProvider.php | 45 +++++----------- example/message/provider/src/send.php | 8 +-- 8 files changed, 83 insertions(+), 146 deletions(-) create mode 100644 example/message/provider/src/ExampleMessage.php delete mode 100644 example/message/provider/src/ExampleMessageProvider.php diff --git a/example/message/consumer/src/ExampleMessageConsumer.php b/example/message/consumer/src/ExampleMessageConsumer.php index 77851aa6..4284d509 100644 --- a/example/message/consumer/src/ExampleMessageConsumer.php +++ b/example/message/consumer/src/ExampleMessageConsumer.php @@ -4,19 +4,12 @@ class ExampleMessageConsumer { - public function ProcessText(string $message): object + public function processMessage(string $message): void { $obj = \json_decode($message); - print ' [x] Processed ' . \print_r($obj->contents->text, true) . "\n"; - - return $obj; - } - - public function ProcessSong(string $message): object - { - $obj = \json_decode($message); - print ' [x] Processed ' . \print_r($obj->contents->song, true) . "\n"; - - return $obj; + print " [x] Processing \n"; + print ' [x] Text: ' . \print_r($obj->contents->text, true) . "\n"; + print ' [x] Number: ' . \print_r($obj->contents->number, true) . "\n"; + print " [x] Processed \n"; } } diff --git a/example/message/consumer/src/receive.php b/example/message/consumer/src/receive.php index 48996111..30848388 100644 --- a/example/message/consumer/src/receive.php +++ b/example/message/consumer/src/receive.php @@ -15,7 +15,7 @@ $callback = function ($msg) { // process that invokes the use of the message $processor = new MessageConsumer\ExampleMessageConsumer(); - $processor->ProcessText($msg->body); + $processor->processMessage($msg->body); $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); }; diff --git a/example/message/consumer/tests/ExampleMessageConsumerTest.php b/example/message/consumer/tests/ExampleMessageConsumerTest.php index de74f40e..cf2c27f4 100644 --- a/example/message/consumer/tests/ExampleMessageConsumerTest.php +++ b/example/message/consumer/tests/ExampleMessageConsumerTest.php @@ -6,6 +6,7 @@ use MessageConsumer\ExampleMessageConsumer; use PhpPact\Consumer\MessageBuilder; use PhpPact\Config\PactConfigInterface; +use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Standalone\PactMessage\PactMessageConfig; use PHPUnit\Framework\TestCase; use stdClass; @@ -13,6 +14,7 @@ class ExampleMessageConsumerTest extends TestCase { private static PactConfigInterface $config; + private Matcher $matcher; public static function setUpBeforeClass(): void { @@ -27,6 +29,11 @@ public static function setUpBeforeClass(): void } } + public function setUp(): void + { + $this->matcher = new Matcher(); + } + /** * @throws Exception */ @@ -36,6 +43,7 @@ public function testProcessText() $contents = new stdClass(); $contents->text = 'Hello Mary'; + $contents->number = $this->matcher->integerV3(); $metadata = ['queue' => 'wind cries', 'routing_key' => 'wind cries']; @@ -47,35 +55,7 @@ public function testProcessText() // established mechanism to this via callbacks $consumerMessage = new ExampleMessageConsumer(); - $callback = [$consumerMessage, 'ProcessText']; - $builder->setCallback($callback); - - $verifyResult = $builder->verify(); - - $this->assertTrue($verifyResult); - } - - /** - * @throws Exception - */ - public function testProcessSong() - { - $builder = new MessageBuilder(self::$config); - - $contents = new stdClass(); - $contents->song = 'And the wind whispers Mary'; - - $metadata = ['queue' => 'And the clowns have all gone to bed', 'routing_key' => 'And the clowns have all gone to bed']; - - $builder - ->given('You can hear happiness staggering on down the street') - ->expectsToReceive('footprints dressed in red') - ->withMetadata($metadata) - ->withContent($contents); - - // established mechanism to this via callbacks - $consumerMessage = new ExampleMessageConsumer(); - $callback = [$consumerMessage, 'ProcessSong']; + $callback = [$consumerMessage, 'processMessage']; $builder->setCallback($callback); $verifyResult = $builder->verify(); diff --git a/example/message/pacts/messageConsumer-messageProvider.json b/example/message/pacts/messageConsumer-messageProvider.json index 9b9cd512..4e29d2c6 100644 --- a/example/message/pacts/messageConsumer-messageProvider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -5,25 +5,31 @@ "messages": [ { "contents": { - "song": "And the wind whispers Mary" + "number": null, + "text": "Hello Mary" }, - "description": "footprints dressed in red", - "metadata": { - "contentType": "application/json", - "queue": "And the clowns have all gone to bed", - "routing_key": "And the clowns have all gone to bed" + "description": "an alligator named Mary exists", + "generators": { + "body": { + "$.number": { + "max": 10, + "min": 0, + "type": "RandomInt" + } + } }, - "providerStates": [ - { - "name": "You can hear happiness staggering on down the street" + "matchingRules": { + "body": { + "$.number": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + } } - ] - }, - { - "contents": { - "text": "Hello Mary" }, - "description": "an alligator named Mary exists", "metadata": { "contentType": "application/json", "queue": "wind cries", @@ -41,8 +47,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.9", - "models": "1.1.11" + "ffi": "0.4.10", + "models": "1.1.12" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/message/provider/src/ExampleMessage.php b/example/message/provider/src/ExampleMessage.php new file mode 100644 index 00000000..2a477b77 --- /dev/null +++ b/example/message/provider/src/ExampleMessage.php @@ -0,0 +1,28 @@ +metadata; + } + + public function getContents(): mixed + { + return $this->contents; + } + + public function __toString(): string + { + return json_encode([ + 'metadata' => $this->metadata, + 'contents' => $this->contents, + ]); + } +} diff --git a/example/message/provider/src/ExampleMessageProvider.php b/example/message/provider/src/ExampleMessageProvider.php deleted file mode 100644 index a8f8c60e..00000000 --- a/example/message/provider/src/ExampleMessageProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -metadata = $metadata; - } - - public function getMetadata(): array - { - return $this->metadata; - } - - public function setMetadata(array $metadata): self - { - $this->metadata = $metadata; - - return $this; - } - - public function getContents(): mixed - { - return $this->contents; - } - - public function setContents(mixed $contents): self - { - $this->contents = $contents; - - return $this; - } - - /** - * Build metadata and content for message - */ - public function Build(): string - { - $obj = new \stdClass(); - $obj->metadata = $this->metadata; - $obj->contents = $this->contents; - - return \json_encode($obj); - } -} diff --git a/example/message/provider/src/ExampleProvider.php b/example/message/provider/src/ExampleProvider.php index 2def3fa5..711653b0 100644 --- a/example/message/provider/src/ExampleProvider.php +++ b/example/message/provider/src/ExampleProvider.php @@ -2,47 +2,28 @@ namespace MessageProvider; -use MessageProvider\ExampleMessageProvider; - class ExampleProvider { - private array $messages; + private array $message = [ + 'metadata' => [ + 'queue' => 'wind cries', + 'routing_key' => 'wind cries', + ], + 'contents' => [ + 'text' => 'Hello Mary', + 'number' => 123, + ] + ]; private array $currentState = []; - public function __construct() - { - $this->messages = [ - 'an alligator named Mary exists' => [ - 'metadata' => [ - 'queue' => 'wind cries', - 'routing_key' => 'wind cries', - ], - 'contents' => [ - 'text' => 'Hello Mary', - ] - ], - 'footprints dressed in red' => [ - 'metadata' => [ - 'queue' => 'And the clowns have all gone to bed', - 'routing_key' => 'And the clowns have all gone to bed', - ], - 'contents' => [ - 'song' => 'And the wind whispers Mary', - ] - ], - ]; - } - - public function dispatchMessage(string $description, array $providerStates): ?ExampleMessageProvider + public function dispatchMessage(string $description, array $providerStates): ?ExampleMessage { - if (!isset($this->messages[$description])) { + if ($description !== 'an alligator named Mary exists') { return null; } - return (new ExampleMessageProvider()) - ->setMetadata($this->messages[$description]['metadata']) - ->setContents($this->messages[$description]['contents']); + return (new ExampleMessage($this->message['contents'], $this->message['metadata'])); } public function changeSate(string $action, string $state, array $params): void diff --git a/example/message/provider/src/send.php b/example/message/provider/src/send.php index 125db85e..0b3f7a66 100644 --- a/example/message/provider/src/send.php +++ b/example/message/provider/src/send.php @@ -1,24 +1,24 @@ 'myKey', 'routing_key' => 'myKey']); $content = new \stdClass(); $content->text = 'Hello Mary'; -$providerMessage->setContents($content); +$metadata = ['queue' => 'myKey', 'routing_key' => 'myKey']; +$providerMessage = new ExampleMessage($content, $metadata); $channel = $connection->channel(); $channel->queue_declare($providerMessage->getMetadata()['queue'], false, false, false, false); // transform message to AMQP -$msg = new AMQPMessage($providerMessage->Build()); +$msg = new AMQPMessage($providerMessage); // publish it $channel->basic_publish($msg, '', $providerMessage->getMetadata()['routing_key']); From fe53fc348f4974bef9978b5390b93167400a10e8 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 23 Nov 2023 02:27:05 +0700 Subject: [PATCH 114/298] feat: Support matchers and generators for message metadata --- .../tests/ExampleMessageConsumerTest.php | 2 +- .../messageConsumer-messageProvider.json | 20 +++++++++++++++++-- src/PhpPact/Consumer/Model/Message.php | 11 +++++++--- .../Registry/Interaction/MessageRegistry.php | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/example/message/consumer/tests/ExampleMessageConsumerTest.php b/example/message/consumer/tests/ExampleMessageConsumerTest.php index cf2c27f4..8a15f6b6 100644 --- a/example/message/consumer/tests/ExampleMessageConsumerTest.php +++ b/example/message/consumer/tests/ExampleMessageConsumerTest.php @@ -45,7 +45,7 @@ public function testProcessText() $contents->text = 'Hello Mary'; $contents->number = $this->matcher->integerV3(); - $metadata = ['queue' => 'wind cries', 'routing_key' => 'wind cries']; + $metadata = ['queue' => 'wind cries', 'routing_key' => $this->matcher->string()]; $builder ->given('a message', ['foo' => 'bar']) diff --git a/example/message/pacts/messageConsumer-messageProvider.json b/example/message/pacts/messageConsumer-messageProvider.json index 4e29d2c6..ea82586b 100644 --- a/example/message/pacts/messageConsumer-messageProvider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -16,6 +16,12 @@ "min": 0, "type": "RandomInt" } + }, + "metadata": { + "routing_key": { + "size": 10, + "type": "RandomString" + } } }, "matchingRules": { @@ -28,12 +34,22 @@ } ] } + }, + "metadata": { + "routing_key": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } } }, "metadata": { "contentType": "application/json", "queue": "wind cries", - "routing_key": "wind cries" + "routing_key": "some string" }, "providerStates": [ { @@ -47,7 +63,7 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.10", + "ffi": "0.4.11", "models": "1.1.12" }, "pactSpecification": { diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 90ba3356..7cdb8d84 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -45,7 +45,7 @@ public function getMetadata(): array } /** - * @param array $metadata + * @param array> $metadata */ public function setMetadata(array $metadata): self { @@ -57,9 +57,14 @@ public function setMetadata(array $metadata): self return $this; } - private function setMetadataValue(string $key, string $value): void + /** + * @param string|array $value + * + * @throws JsonException + */ + private function setMetadataValue(string $key, string|array $value): void { - $this->metadata[$key] = $value; + $this->metadata[$key] = is_string($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); } public function getContents(): Text|Binary|null diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index 607b9bdf..1a225dd6 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -79,7 +79,7 @@ private function given(array $providerStates): self private function withMetadata(array $metadata): self { foreach ($metadata as $key => $value) { - $this->client->call('pactffi_message_with_metadata', $this->id, (string) $key, (string) $value); + $this->client->call('pactffi_message_with_metadata_v2', $this->id, (string) $key, (string) $value); } return $this; From 9461ad80fdaa1d211fb31b1ea8adb1b6e432a962 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 29 Nov 2023 17:45:22 +0700 Subject: [PATCH 115/298] refactor!: Lazy load binary file --- .../tests/Service/HttpClientServiceTest.php | 6 ++-- .../pacts/binaryConsumer-binaryProvider.json | 7 +++-- .../Exception/BinaryFileNotExistException.php | 9 ++++++ .../Exception/BinaryFileReadException.php | 9 ++++++ src/PhpPact/Consumer/Model/Body/Binary.php | 28 +++++++++++++----- .../Interaction/Body/AbstractBodyRegistry.php | 29 ++++++++++--------- .../Body/MessageContentsRegistry.php | 18 ++++++++---- src/PhpPact/FFI/Model/StringData.php | 2 +- 8 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 src/PhpPact/Consumer/Exception/BinaryFileNotExistException.php create mode 100644 src/PhpPact/Consumer/Exception/BinaryFileReadException.php diff --git a/example/binary/consumer/tests/Service/HttpClientServiceTest.php b/example/binary/consumer/tests/Service/HttpClientServiceTest.php index c25e89bb..40b3e22e 100644 --- a/example/binary/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/binary/consumer/tests/Service/HttpClientServiceTest.php @@ -14,7 +14,7 @@ class HttpClientServiceTest extends TestCase { public function testGetImageContent() { - $imageContent = file_get_contents(__DIR__ . '/../_resource/image.jpg'); + $path = __DIR__ . '/../_resource/image.jpg'; $request = new ConsumerRequest(); $request @@ -26,7 +26,7 @@ public function testGetImageContent() $response ->setStatus(200) ->addHeader('Content-Type', 'image/jpeg') - ->setBody(new Binary($imageContent, in_array(php_uname('m'), ['AMD64', 'arm64', 'aarch64']) ? 'application/octet-stream' : 'image/jpeg')); + ->setBody(new Binary($path, in_array(php_uname('m'), ['AMD64', 'arm64', 'aarch64']) ? 'application/octet-stream' : 'image/jpeg')); $config = new MockServerConfig(); $config @@ -48,6 +48,6 @@ public function testGetImageContent() $verifyResult = $builder->verify(); $this->assertTrue($verifyResult); - $this->assertEquals($imageContent, $imageContentResult); + $this->assertEquals(file_get_contents($path), $imageContentResult); } } diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json index c6a957a1..a8511d97 100644 --- a/example/binary/pacts/binaryConsumer-binaryProvider.json +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -34,7 +34,8 @@ ] } }, - "header": {} + "header": {}, + "status": {} }, "status": 200 } @@ -42,9 +43,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.9", + "ffi": "0.4.11", "mockserver": "1.2.4", - "models": "1.1.11" + "models": "1.1.12" }, "pactSpecification": { "version": "3.0.0" diff --git a/src/PhpPact/Consumer/Exception/BinaryFileNotExistException.php b/src/PhpPact/Consumer/Exception/BinaryFileNotExistException.php new file mode 100644 index 00000000..4fcb49bd --- /dev/null +++ b/src/PhpPact/Consumer/Exception/BinaryFileNotExistException.php @@ -0,0 +1,9 @@ +setContents(StringData::createFrom($contents, false)); $this->setContentType($contentType); } - public function getContents(): StringData + public function getPath(): string { - return $this->contents; + return $this->path; } - public function setContents(StringData $contents): self + public function setPath(string $path): self { - $this->contents = $contents; + $this->path = $path; return $this; } + + public function createBinaryData(): StringData + { + if (!file_exists($this->getPath())) { + throw new BinaryFileNotExistException(sprintf('File %s does not exist', $this->getPath())); + } + $contents = file_get_contents($this->getPath()); + if (false === $contents) { + throw new BinaryFileReadException(sprintf('File %s can not be read', $this->getPath())); + } + + return StringData::createFrom($contents, false); + } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php index 5f66fbca..317bf173 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -8,7 +8,6 @@ use PhpPact\Consumer\Exception\PartNotAddedException; use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; -use PhpPact\Consumer\Model\Body\Part; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -23,23 +22,27 @@ public function __construct( public function withBody(Text|Binary|Multipart $body): void { - $success = match (true) { - $body instanceof Binary => $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), - $body instanceof Text => $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), - $body instanceof Multipart => array_reduce( - $body->getParts(), - function (bool $success, Part $part) use ($body) { + switch (true) { + case $body instanceof Binary: + $data = $body->createBinaryData(); + $success = $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); + break; + + case $body instanceof Text: + $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + break; + + case $body instanceof Multipart: + foreach ($body->getParts() as $part) { $result = $this->client->call('pactffi_with_multipart_file_v2', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); if ($result->failed instanceof CData) { throw new PartNotAddedException(FFI::string($result->failed)); } - - return true; - }, - true - ), + } + $success = true; + break; }; - if (!$success) { + if (!isset($success) || !$success) { throw new InteractionBodyNotAddedException(); } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php index 7aae0fc8..843ebe5e 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php @@ -23,12 +23,20 @@ public function __construct( public function withBody(Text|Binary|Multipart $body): void { - $success = match (true) { - $body instanceof Binary => $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()), - $body instanceof Text => $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()), - $body instanceof Multipart => throw new BodyNotSupportedException('Message does not support multipart'), + switch (true) { + case $body instanceof Binary: + $data = $body->createBinaryData(); + $success = $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); + break; + + case $body instanceof Text: + $success = $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + break; + + case $body instanceof Multipart: + throw new BodyNotSupportedException('Message does not support multipart'); }; - if (!$success) { + if (!isset($success) || !$success) { throw new MessageContentsNotAddedException(); } } diff --git a/src/PhpPact/FFI/Model/StringData.php b/src/PhpPact/FFI/Model/StringData.php index 82e8fe57..500d1542 100644 --- a/src/PhpPact/FFI/Model/StringData.php +++ b/src/PhpPact/FFI/Model/StringData.php @@ -27,7 +27,7 @@ public static function createFrom(string $value, bool $nullTerminated = true): s { $length = \strlen($value); $size = $length + ($nullTerminated ? 1 : 0); - $cData = FFI::new("uint8_t[{$size}]"); + $cData = FFI::new("uint8_t[{$size}]", false); FFI::memcpy($cData, $value, $length); return new self($cData, $size); From 4c8cc3b38356df16c6bdf68422f0de8351ffa386 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 29 Nov 2023 20:42:02 +0700 Subject: [PATCH 116/298] chore: Rename StringData to BinaryData --- src/PhpPact/Consumer/Model/Body/Binary.php | 6 +-- .../EmptyBinaryFileNotSupportedException.php | 9 ++++ src/PhpPact/FFI/Model/BinaryData.php | 49 +++++++++++++++++++ src/PhpPact/FFI/Model/StringData.php | 35 ------------- .../Consumer/Model/Body/BinaryTest.php | 44 +++++++++++++++++ tests/PhpPact/FFI/Model/BinaryDataTest.php | 31 ++++++++++++ tests/PhpPact/FFI/Model/StringDataTest.php | 43 ---------------- 7 files changed, 136 insertions(+), 81 deletions(-) create mode 100644 src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php create mode 100644 src/PhpPact/FFI/Model/BinaryData.php delete mode 100644 src/PhpPact/FFI/Model/StringData.php create mode 100644 tests/PhpPact/Consumer/Model/Body/BinaryTest.php create mode 100644 tests/PhpPact/FFI/Model/BinaryDataTest.php delete mode 100644 tests/PhpPact/FFI/Model/StringDataTest.php diff --git a/src/PhpPact/Consumer/Model/Body/Binary.php b/src/PhpPact/Consumer/Model/Body/Binary.php index 1c7b547b..44f8a914 100644 --- a/src/PhpPact/Consumer/Model/Body/Binary.php +++ b/src/PhpPact/Consumer/Model/Body/Binary.php @@ -4,7 +4,7 @@ use PhpPact\Consumer\Exception\BinaryFileNotExistException; use PhpPact\Consumer\Exception\BinaryFileReadException; -use PhpPact\FFI\Model\StringData; +use PhpPact\FFI\Model\BinaryData; class Binary { @@ -27,7 +27,7 @@ public function setPath(string $path): self return $this; } - public function createBinaryData(): StringData + public function createBinaryData(): BinaryData { if (!file_exists($this->getPath())) { throw new BinaryFileNotExistException(sprintf('File %s does not exist', $this->getPath())); @@ -37,6 +37,6 @@ public function createBinaryData(): StringData throw new BinaryFileReadException(sprintf('File %s can not be read', $this->getPath())); } - return StringData::createFrom($contents, false); + return BinaryData::createFrom($contents); } } diff --git a/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php b/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php new file mode 100644 index 00000000..602f9a4b --- /dev/null +++ b/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php @@ -0,0 +1,9 @@ +value; + } + + public function getSize(): int + { + return $this->size; + } + + public static function createFrom(string $contents): self + { + if (empty($contents)) { + throw new EmptyBinaryFileNotSupportedException(); + } + + $length = \strlen($contents); + $cData = FFI::new("uint8_t[{$length}]", false); + FFI::memcpy($cData, $contents, $length); + + return new self($cData, $length); + } + + public function __toString(): string + { + $result = ''; + for ($index = 0; $index < $this->size; $index++) { + $result .= chr($this->value[$index]); // @phpstan-ignore-line + } + + return $result; + } +} diff --git a/src/PhpPact/FFI/Model/StringData.php b/src/PhpPact/FFI/Model/StringData.php deleted file mode 100644 index 500d1542..00000000 --- a/src/PhpPact/FFI/Model/StringData.php +++ /dev/null @@ -1,35 +0,0 @@ -value; - } - - public function getSize(): int - { - return $this->size; - } - - public static function createFrom(string $value, bool $nullTerminated = true): self - { - $length = \strlen($value); - $size = $length + ($nullTerminated ? 1 : 0); - $cData = FFI::new("uint8_t[{$size}]", false); - FFI::memcpy($cData, $value, $length); - - return new self($cData, $size); - } -} diff --git a/tests/PhpPact/Consumer/Model/Body/BinaryTest.php b/tests/PhpPact/Consumer/Model/Body/BinaryTest.php new file mode 100644 index 00000000..e21f172b --- /dev/null +++ b/tests/PhpPact/Consumer/Model/Body/BinaryTest.php @@ -0,0 +1,44 @@ +assertSame('/path/to/file1.jpg', $body->getPath()); + $body->setPath('/other/path/to/file2.jpg'); + $this->assertSame('/other/path/to/file2.jpg', $body->getPath()); + } + + public function testGetContentType(): void + { + $body = new Binary('/path/to/text.txt', 'plain/text'); + $this->assertSame('plain/text', $body->getContentType()); + $body->setContentType('text/csv'); + $this->assertSame('text/csv', $body->getContentType()); + } + + public function testCreateBinaryDataFromInvalidFilePath(): void + { + $path = __DIR__ . '/../../../../_resources/invalid.jpg'; + $body = new Binary($path, 'image/jpeg'); + $this->expectException(BinaryFileNotExistException::class); + $this->expectExceptionMessage("File $path does not exist"); + $body->createBinaryData(); + } + + public function testCreateBinaryData(): void + { + $path = __DIR__ . '/../../../../_resources/image.jpg'; + $body = new Binary($path, 'image/jpeg'); + $data = $body->createBinaryData(); + + $this->assertEquals(file_get_contents($path), (string) $data); + } +} diff --git a/tests/PhpPact/FFI/Model/BinaryDataTest.php b/tests/PhpPact/FFI/Model/BinaryDataTest.php new file mode 100644 index 00000000..f86e5559 --- /dev/null +++ b/tests/PhpPact/FFI/Model/BinaryDataTest.php @@ -0,0 +1,31 @@ +expectException(EmptyBinaryFileNotSupportedException::class); + BinaryData::createFrom(''); + } + + public function testCreateBinaryString(): void + { + $path = __DIR__ . '/../../../_resources/image.jpg'; + $contents = file_get_contents($path); + $length = \strlen($contents); + + $binaryData = BinaryData::createFrom($contents); + $cData = $binaryData->getValue(); + + $this->assertSame($length, FFI::sizeof($cData)); + $this->assertSame($length, $binaryData->getSize()); + $this->assertEquals($contents, (string) $binaryData); + } +} diff --git a/tests/PhpPact/FFI/Model/StringDataTest.php b/tests/PhpPact/FFI/Model/StringDataTest.php deleted file mode 100644 index 075399b6..00000000 --- a/tests/PhpPact/FFI/Model/StringDataTest.php +++ /dev/null @@ -1,43 +0,0 @@ -getValue(); - $size = \strlen($value) + 1; - - $this->assertSame($size, FFI::sizeof($cData)); - $this->assertSame($value, $this->cDataToString($cData, $size - 1)); // ignore null - } - - public function testCreateBinaryString() - { - $value = file_get_contents(__DIR__ . '/../../../_resources/image.jpg'); - $stringData = StringData::createFrom($value, false); - $cData = $stringData->getValue(); - $size = \strlen($value); - - $this->assertSame($size, FFI::sizeof($cData)); - $this->assertEquals($value, $this->cDataToString($cData, $size)); - } - - private function cDataToString(CData $cData, int $size): string - { - $result = ''; - for ($index = 0; $index < $size; $index++) { - $result .= chr($cData[$index]); // @phpstan-ignore-line - } - - return $result; - } -} From d7524eae2574b05ea01f6cbf9d4441520f959063 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 29 Nov 2023 21:04:59 +0700 Subject: [PATCH 117/298] chore: Use 'Constructor property promotion' PHP 8 feature to make code smaller --- src/PhpPact/Consumer/Model/Body/Text.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PhpPact/Consumer/Model/Body/Text.php b/src/PhpPact/Consumer/Model/Body/Text.php index 1d2e54f7..961671a3 100644 --- a/src/PhpPact/Consumer/Model/Body/Text.php +++ b/src/PhpPact/Consumer/Model/Body/Text.php @@ -6,11 +6,8 @@ class Text { use ContentTypeTrait; - private string $contents; - - public function __construct(string $contents, string $contentType) + public function __construct(private string $contents, string $contentType) { - $this->setContents($contents); $this->setContentType($contentType); } From 2d358b3038d47cacd6634b1d668678c62a557a2a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 30 Nov 2023 12:22:47 +0700 Subject: [PATCH 118/298] chore: Change default datetime format --- .../consumer/tests/Service/GeneratorsTest.php | 8 ++++---- .../pacts/generatorsConsumer-generatorsProvider.json | 8 ++++---- example/generators/provider/public/index.php | 2 +- .../matchers/consumer/tests/Service/MatchersTest.php | 4 ++-- .../pacts/matchersConsumer-matchersProvider.json | 4 ++-- example/matchers/provider/public/index.php | 2 +- src/PhpPact/Consumer/Matcher/Matcher.php | 4 ++-- tests/PhpPact/Consumer/Matcher/MatcherTest.php | 12 ++++++------ 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index 5295689e..d0366d98 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -44,8 +44,8 @@ public function testGetMatchers() 'hexadecimal' => $this->matcher->hexadecimal(null), 'uuid' => $this->matcher->uuid(null), 'date' => $this->matcher->date('yyyy-MM-dd', null), - 'time' => $this->matcher->time('HH:mm::ss', null), - 'datetime' => $this->matcher->datetime("YYYY-MM-D'T'HH:mm:ss", null), + 'time' => $this->matcher->time('HH:mm:ss', null), + 'datetime' => $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", null), 'string' => $this->matcher->string(null), 'number' => $this->matcher->number(null), 'requestId' => 222, @@ -89,8 +89,8 @@ public function testGetMatchers() $this->assertRegExp('/' . Matcher::HEX_FORMAT . '/', $body['hexadecimal']); $this->assertRegExp('/' . Matcher::UUID_V4_FORMAT . '/', $body['uuid']); $this->assertTrue($this->validateDateTime($body['date'], 'Y-m-d')); - $this->assertTrue($this->validateDateTime($body['time'], 'H:i::s')); - $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-z\TH:i:s")); + $this->assertTrue($this->validateDateTime($body['time'], 'H:i:s')); + $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-d\TH:i:s")); $this->assertIsString($body['string']); $this->assertIsNumeric($body['number']); $this->assertSame(222, $body['requestId']); diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json index 8ab6f942..a21a7efe 100644 --- a/example/generators/pacts/generatorsConsumer-generatorsProvider.json +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -80,7 +80,7 @@ "type": "Date" }, "$.datetime": { - "format": "YYYY-MM-D'T'HH:mm:ss", + "format": "yyyy-MM-dd'T'HH:mm:ss", "type": "DateTime" }, "$.decimal": { @@ -110,7 +110,7 @@ "type": "RandomString" }, "$.time": { - "format": "HH:mm::ss", + "format": "HH:mm:ss", "type": "Time" }, "$.uuid": { @@ -151,7 +151,7 @@ "combine": "AND", "matchers": [ { - "format": "YYYY-MM-D'T'HH:mm:ss", + "format": "yyyy-MM-dd'T'HH:mm:ss", "match": "datetime" } ] @@ -210,7 +210,7 @@ "combine": "AND", "matchers": [ { - "format": "HH:mm::ss", + "format": "HH:mm:ss", "match": "time" } ] diff --git a/example/generators/provider/public/index.php b/example/generators/provider/public/index.php index 208d9a5f..ceebce11 100644 --- a/example/generators/provider/public/index.php +++ b/example/generators/provider/public/index.php @@ -19,7 +19,7 @@ 'hexadecimal' => '20AC', 'uuid' => 'e9d2f3a5-6ecc-4bff-8935-84bb6141325a', 'date' => '1997-12-11', - 'time' => '11:01::02', + 'time' => '11:01:02', 'datetime' => '1997-07-16T19:20:30', 'string' => 'another string', 'number' => 112.3, diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index b915525f..301459ec 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -66,8 +66,8 @@ public function testGetMatchers() 'email' => $this->matcher->email(), 'nullValue' => $this->matcher->nullValue(), 'date' => $this->matcher->date('yyyy-MM-dd', '2015-05-16'), - 'time' => $this->matcher->time('HH:mm::ss', '23:59::58'), - 'datetime' => $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2000-10-31T01:30:00'), + 'time' => $this->matcher->time('HH:mm:ss', '23:59::58'), + 'datetime' => $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", '2000-10-31T01:30:00'), 'likeString' => $this->matcher->string('some string'), 'equal' => $this->matcher->equal('exact this value'), 'equalArray' => $this->matcher->equal([ diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index b7ac9422..cf7b2083 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -297,7 +297,7 @@ "combine": "AND", "matchers": [ { - "format": "YYYY-mm-DD'T'HH:mm:ss", + "format": "yyyy-MM-dd'T'HH:mm:ss", "match": "datetime" } ] @@ -511,7 +511,7 @@ "combine": "AND", "matchers": [ { - "format": "HH:mm::ss", + "format": "HH:mm:ss", "match": "time" } ] diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index b6ece2d1..509506b1 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -36,7 +36,7 @@ 'email' => 'pact@example.com', 'nullValue' => null, 'date' => '1997-12-11', - 'time' => '11:01::02', + 'time' => '11:01:02', 'datetime' => '1997-07-16T19:20:30', 'likeString' => 'another string', 'equal' => 'exact this value', diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 0bcbe3ea..c7853e14 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -466,7 +466,7 @@ public function date(string $format = 'yyyy-MM-dd', ?string $value = null): arra /** * @return array */ - public function time(string $format = 'HH:mm::ss', ?string $value = null): array + public function time(string $format = 'HH:mm:ss', ?string $value = null): array { if (null === $value) { return [ @@ -486,7 +486,7 @@ public function time(string $format = 'HH:mm::ss', ?string $value = null): array /** * @return array */ - public function datetime(string $format = "YYYY-mm-DD'T'HH:mm:ss", ?string $value = null): array + public function datetime(string $format = "yyyy-MM-dd'T'HH:mm:ss", ?string $value = null): array { if (null === $value) { return [ diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 23aeb0fe..217f73f5 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -661,9 +661,9 @@ public function testTime() $expected = [ 'value' => '21:45::31', 'pact:matcher:type' => 'time', - 'format' => 'HH:mm::ss', + 'format' => 'HH:mm:ss', ]; - $actual = $this->matcher->time('HH:mm::ss', '21:45::31'); + $actual = $this->matcher->time('HH:mm:ss', '21:45::31'); $this->assertEquals($expected, $actual); } @@ -673,7 +673,7 @@ public function testRandomTime() $expected = [ 'pact:generator:type' => 'Time', 'pact:matcher:type' => 'time', - 'format' => 'HH:mm::ss', + 'format' => 'HH:mm:ss', ]; $actual = $this->matcher->time(); @@ -685,9 +685,9 @@ public function testDateTime() $expected = [ 'value' => '2015-08-06T16:53:10', 'pact:matcher:type' => 'datetime', - 'format' => "YYYY-mm-DD'T'HH:mm:ss", + 'format' => "yyyy-MM-dd'T'HH:mm:ss", ]; - $actual = $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2015-08-06T16:53:10'); + $actual = $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", '2015-08-06T16:53:10'); $this->assertEquals($expected, $actual); } @@ -697,7 +697,7 @@ public function testRandomDateTime() $expected = [ 'pact:generator:type' => 'DateTime', 'pact:matcher:type' => 'datetime', - 'format' => "YYYY-mm-DD'T'HH:mm:ss", + 'format' => "yyyy-MM-dd'T'HH:mm:ss", ]; $actual = $this->matcher->datetime(); From 1db2dd3636ffbf111507e43c56a4c31488f6c759 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 14 Nov 2023 10:26:46 +0700 Subject: [PATCH 119/298] feat: Support sync message --- .../Registry/Interaction/MessageRegistry.php | 2 +- .../Driver/Interaction/SyncMessageDriver.php | 31 +++++++++++++++ .../SyncMessageDriverInterface.php | 12 ++++++ .../Driver/Pact/AbstractPluginPactDriver.php | 39 +++++++++++++++++++ .../InteractionContentNotAddedException.php | 22 +++++++++++ ...inNotSupportedBySpecificationException.php | 16 ++++++++ .../Factory/SyncMessageDriverFactory.php | 26 +++++++++++++ .../SyncMessageDriverFactoryInterface.php | 11 ++++++ .../Contents/AbstractContentsRegistry.php | 30 ++++++++++++++ .../Interaction/SyncMessageRegistry.php | 31 +++++++++++++++ .../SyncMessage/SyncMessageBuilder.php | 30 ++++++++++++++ 11 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php create mode 100644 src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php create mode 100644 src/PhpPact/SyncMessage/Driver/Pact/AbstractPluginPactDriver.php create mode 100644 src/PhpPact/SyncMessage/Exception/InteractionContentNotAddedException.php create mode 100644 src/PhpPact/SyncMessage/Exception/PluginNotSupportedBySpecificationException.php create mode 100644 src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php create mode 100644 src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryInterface.php create mode 100644 src/PhpPact/SyncMessage/Registry/Interaction/Contents/AbstractContentsRegistry.php create mode 100644 src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php create mode 100644 src/PhpPact/SyncMessage/SyncMessageBuilder.php diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index 1a225dd6..fa1f693b 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -61,7 +61,7 @@ private function expectsToReceive(string $description): self /** * @param ProviderState[] $providerStates */ - private function given(array $providerStates): self + protected function given(array $providerStates): self { foreach ($providerStates as $providerState) { $this->client->call('pactffi_message_given', $this->id, $providerState->getName()); diff --git a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php new file mode 100644 index 00000000..a0260adc --- /dev/null +++ b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php @@ -0,0 +1,31 @@ +mockServer->verify(); + } + + public function registerMessage(Message $message): void + { + $this->pactDriver->setUp(); + $this->messageRegistry->registerMessage($message); + + $this->mockServer->start(); + } +} diff --git a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php new file mode 100644 index 00000000..778dc591 --- /dev/null +++ b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php @@ -0,0 +1,12 @@ +client->call('pactffi_cleanup_plugins', $this->pactRegistry->getId()); + parent::cleanUp(); + } + + public function setUp(): void + { + parent::setUp(); + $this->usingPlugin(); + } + + abstract protected function getPluginName(): string; + + protected function getPluginVersion(): ?string + { + return null; + } + + private function usingPlugin(): self + { + if ($this->getSpecification() < $this->client->get('PactSpecification_V4')) { + throw new PluginNotSupportedBySpecificationException($this->config->getPactSpecificationVersion()); + } + + $this->client->call('pactffi_using_plugin', $this->pactRegistry->getId(), $this->getPluginName(), $this->getPluginVersion()); + + return $this; + } +} diff --git a/src/PhpPact/SyncMessage/Exception/InteractionContentNotAddedException.php b/src/PhpPact/SyncMessage/Exception/InteractionContentNotAddedException.php new file mode 100644 index 00000000..8a82cd56 --- /dev/null +++ b/src/PhpPact/SyncMessage/Exception/InteractionContentNotAddedException.php @@ -0,0 +1,22 @@ + 'A general panic was caught.', + 2 => 'The mock server has already been started.', + 3 => 'The interaction handle is invalid.', + 4 => 'The content type is not valid.', + 5 => 'The contents JSON is not valid JSON.', + 6 => 'The plugin returned an error.', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } +} diff --git a/src/PhpPact/SyncMessage/Exception/PluginNotSupportedBySpecificationException.php b/src/PhpPact/SyncMessage/Exception/PluginNotSupportedBySpecificationException.php new file mode 100644 index 00000000..611e44e0 --- /dev/null +++ b/src/PhpPact/SyncMessage/Exception/PluginNotSupportedBySpecificationException.php @@ -0,0 +1,16 @@ +client->call('pactffi_interaction_contents', $this->getInteractionId(), $this->getPart(), $contentType, $contents); + if ($error) { + throw new InteractionContentNotAddedException($error); + } + } + + abstract protected function getInteractionId(): int; + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php b/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php new file mode 100644 index 00000000..1f22d653 --- /dev/null +++ b/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php @@ -0,0 +1,31 @@ +id = $this->client->call('pactffi_new_sync_message_interaction', $this->pactRegistry->getId(), $description); + + return $this; + } + + /** + * @param ProviderState[] $providerStates + */ + protected function given(array $providerStates): self + { + foreach ($providerStates as $providerState) { + $this->client->call('pactffi_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + + return $this; + } +} diff --git a/src/PhpPact/SyncMessage/SyncMessageBuilder.php b/src/PhpPact/SyncMessage/SyncMessageBuilder.php new file mode 100644 index 00000000..aa5e5511 --- /dev/null +++ b/src/PhpPact/SyncMessage/SyncMessageBuilder.php @@ -0,0 +1,30 @@ +driver = ($driverFactory ?? new SyncMessageDriverFactory())->create($config); + } + + public function registerMessage(): void + { + $this->driver->registerMessage($this->message); + } + + public function verify(): bool + { + return $this->driver->verifyMessage(); + } +} From cdc9bc62e40fef868e6007234cc207c883f98fe7 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Dec 2023 21:56:07 +0700 Subject: [PATCH 120/298] refactor: Add RandomInt generator class --- .../Consumer/Matcher/Generators/RandomInt.php | 27 +++++++++++++++++++ .../Matcher/Model/GeneratorInterface.php | 9 +++++++ .../Matcher/Generators/RandomIntTest.php | 18 +++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/RandomInt.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RandomIntTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php b/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php new file mode 100644 index 00000000..df774c87 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php @@ -0,0 +1,27 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'min' => $this->min, + 'max' => $this->max, + 'pact:generator:type' => 'RandomInt', + ]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php b/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php new file mode 100644 index 00000000..7701bdb5 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php @@ -0,0 +1,9 @@ +assertSame( + '{"min":5,"max":15,"pact:generator:type":"RandomInt"}', + json_encode($int) + ); + } +} From a1b16125e8058292df57b4031cd0a61976853acd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 16:30:36 +0700 Subject: [PATCH 121/298] refactor: Add RandomDecimal generator class --- .../Matcher/Generators/RandomDecimal.php | 26 +++++++++++++++++++ .../Matcher/Generators/RandomDecimalTest.php | 18 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php b/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php new file mode 100644 index 00000000..62ed7a8a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php @@ -0,0 +1,26 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'digits' => $this->digits, + 'pact:generator:type' => 'RandomDecimal', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php new file mode 100644 index 00000000..22aac2a9 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"digits":12,"pact:generator:type":"RandomDecimal"}', + json_encode($decimal) + ); + } +} From 5dbe37414fe3a53eec935e036c6f2b6a0e714757 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 16:45:17 +0700 Subject: [PATCH 122/298] refactor: Add RandomHexadecimal generator class --- .../Matcher/Generators/RandomHexadecimal.php | 26 +++++++++++++++++++ .../Generators/RandomHexadecimalTest.php | 18 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php b/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php new file mode 100644 index 00000000..48ec6ed3 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php @@ -0,0 +1,26 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'digits' => $this->digits, + 'pact:generator:type' => 'RandomHexadecimal', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php new file mode 100644 index 00000000..be13ab06 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"digits":8,"pact:generator:type":"RandomHexadecimal"}', + json_encode($hexaDecimal) + ); + } +} From e32b21a778f492685ad656ec6d87e15057888938 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 17:04:26 +0700 Subject: [PATCH 123/298] refactor: Add RandomString generator class --- .../Matcher/Generators/RandomString.php | 26 +++++++++++++++++++ .../Matcher/Generators/RandomStringTest.php | 18 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/RandomString.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomString.php b/src/PhpPact/Consumer/Matcher/Generators/RandomString.php new file mode 100644 index 00000000..f8be32a5 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomString.php @@ -0,0 +1,26 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'size' => $this->size, + 'pact:generator:type' => 'RandomString', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php new file mode 100644 index 00000000..2adf26bf --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"size":11,"pact:generator:type":"RandomString"}', + json_encode($string) + ); + } +} From 4779e002864707121a907422535fec06f3a5cafa Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 17:33:54 +0700 Subject: [PATCH 124/298] refactor: Add Regex generator class --- .../Consumer/Matcher/Generators/Regex.php | 26 +++++++++++++++++++ .../Consumer/Matcher/Generators/RegexTest.php | 18 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/Regex.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/Regex.php b/src/PhpPact/Consumer/Matcher/Generators/Regex.php new file mode 100644 index 00000000..0625269a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/Regex.php @@ -0,0 +1,26 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'regex' => $this->regex, + 'pact:generator:type' => 'Regex', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php new file mode 100644 index 00000000..00332a02 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"regex":"[\\\\w\\\\d]+","pact:generator:type":"Regex"}', + json_encode($regex) + ); + } +} From ca97cc068d4919586f26f2e3bded036f883117a0 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:17:35 +0700 Subject: [PATCH 125/298] refactor: Add Uuid generator class --- .../Consumer/Exception/ConsumerException.php | 9 ++++ .../Exception/InvalidUuidFormatException.php | 7 +++ .../Matcher/Exception/MatcherException.php | 9 ++++ .../Consumer/Matcher/Generators/Uuid.php | 50 +++++++++++++++++++ src/PhpPact/Exception/BaseException.php | 9 ++++ .../Consumer/Matcher/Generators/UuidTest.php | 28 +++++++++++ 6 files changed, 112 insertions(+) create mode 100644 src/PhpPact/Consumer/Exception/ConsumerException.php create mode 100644 src/PhpPact/Consumer/Matcher/Exception/InvalidUuidFormatException.php create mode 100644 src/PhpPact/Consumer/Matcher/Exception/MatcherException.php create mode 100644 src/PhpPact/Consumer/Matcher/Generators/Uuid.php create mode 100644 src/PhpPact/Exception/BaseException.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/UuidTest.php diff --git a/src/PhpPact/Consumer/Exception/ConsumerException.php b/src/PhpPact/Consumer/Exception/ConsumerException.php new file mode 100644 index 00000000..7d6159c3 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/ConsumerException.php @@ -0,0 +1,9 @@ + + */ + public function jsonSerialize(): array + { + $data = ['pact:generator:type' => 'Uuid']; + + if ($this->format !== null) { + $data['format'] = $this->format; + } + + return $data; + } +} diff --git a/src/PhpPact/Exception/BaseException.php b/src/PhpPact/Exception/BaseException.php new file mode 100644 index 00000000..3403fc39 --- /dev/null +++ b/src/PhpPact/Exception/BaseException.php @@ -0,0 +1,9 @@ +expectException(InvalidUuidFormatException::class); + $this->expectExceptionMessage('Format invalid is not supported. Supported formats are: simple, lower-case-hyphenated, upper-case-hyphenated, URN'); + } + $uuid = new Uuid($format); + $this->assertSame($json, json_encode($uuid)); + } +} From b3c38897f31e05540a28742bf7e9b5689939a245 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:27:17 +0700 Subject: [PATCH 126/298] refactor: Add Date generator class --- .../Matcher/Generators/AbstractDateTime.php | 32 +++++++++++++++++++ .../Consumer/Matcher/Generators/Date.php | 23 +++++++++++++ .../Consumer/Matcher/Generators/DateTest.php | 21 ++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/AbstractDateTime.php create mode 100644 src/PhpPact/Consumer/Matcher/Generators/Date.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/DateTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/AbstractDateTime.php b/src/PhpPact/Consumer/Matcher/Generators/AbstractDateTime.php new file mode 100644 index 00000000..e7d8ba1c --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/AbstractDateTime.php @@ -0,0 +1,32 @@ + + */ + public function jsonSerialize(): array + { + $data = ['pact:generator:type' => $this->getType()]; + + if ($this->format !== null) { + $data['format'] = $this->format; + } + + if ($this->expression !== null) { + $data['expression'] = $this->expression; + } + + return $data; + } + + abstract protected function getType(): string; +} diff --git a/src/PhpPact/Consumer/Matcher/Generators/Date.php b/src/PhpPact/Consumer/Matcher/Generators/Date.php new file mode 100644 index 00000000..9caefa82 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/Date.php @@ -0,0 +1,23 @@ +assertSame($json, json_encode($date)); + } +} From 753de4348e992bfa8961349eff9f6146ba2007e1 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:36:04 +0700 Subject: [PATCH 127/298] refactor: Add Time generator class --- .../Consumer/Matcher/Generators/Time.php | 23 +++++++++++++++++++ .../Consumer/Matcher/Generators/TimeTest.php | 21 +++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/Time.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/TimeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/Time.php b/src/PhpPact/Consumer/Matcher/Generators/Time.php new file mode 100644 index 00000000..a535ae0a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/Time.php @@ -0,0 +1,23 @@ +assertSame($json, json_encode($time)); + } +} From f705b51736b0ec8c9e0ec7f566a259dfa99b294f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:37:57 +0700 Subject: [PATCH 128/298] refactor: Add DateTime generator class --- .../Consumer/Matcher/Generators/DateTime.php | 23 +++++++++++++++++++ .../Matcher/Generators/DateTimeTest.php | 21 +++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/DateTime.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/DateTimeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/DateTime.php b/src/PhpPact/Consumer/Matcher/Generators/DateTime.php new file mode 100644 index 00000000..8d682ed9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/DateTime.php @@ -0,0 +1,23 @@ +assertSame($json, json_encode($dateTime)); + } +} From 2f702337ab8112921073e2da41ae77731a71a7e6 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:41:28 +0700 Subject: [PATCH 129/298] refactor: Add RandomBoolean generator class --- .../Matcher/Generators/RandomBoolean.php | 21 +++++++++++++++++++ .../Matcher/Generators/RandomBooleanTest.php | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php b/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php new file mode 100644 index 00000000..872ab29f --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php @@ -0,0 +1,21 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'pact:generator:type' => 'RandomBoolean', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php new file mode 100644 index 00000000..c302f3c0 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"pact:generator:type":"RandomBoolean"}', + json_encode($boolean) + ); + } +} From 081274a02b4407be205d35bdb92d39aa052e97a0 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:42:59 +0700 Subject: [PATCH 130/298] refactor: Add ProviderState generator class --- .../Matcher/Generators/ProviderState.php | 28 +++++++++++++++++++ .../Matcher/Generators/ProviderStateTest.php | 18 ++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/ProviderState.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php b/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php new file mode 100644 index 00000000..88b2d675 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php @@ -0,0 +1,28 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'expression' => $this->expression, + 'pact:generator:type' => 'ProviderState', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php b/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php new file mode 100644 index 00000000..a6a80566 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"expression":"\/products\/${id}","pact:generator:type":"ProviderState"}', + json_encode($url) + ); + } +} From d12136a06448ce04c8ce8fd04f2fd271ee6ac1b3 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Dec 2023 18:44:37 +0700 Subject: [PATCH 131/298] refactor: Add MockServerURL generator class --- .../Matcher/Generators/MockServerURL.php | 30 +++++++++++++++++++ .../Matcher/Generators/MockServerURLTest.php | 18 +++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php create mode 100644 tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php diff --git a/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php b/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php new file mode 100644 index 00000000..e8a34577 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php @@ -0,0 +1,30 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'regex' => $this->regex, + 'example' => $this->example, + 'pact:generator:type' => 'MockServerURL', + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php b/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php new file mode 100644 index 00000000..4a7bb3b2 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"regex":".*(\/path)$","example":"http:\/\/localhost:1234\/path","pact:generator:type":"MockServerURL"}', + json_encode($url) + ); + } +} From 70e19d23fcc1a20eafe7f8396a383d40a8de7d5f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 13 Dec 2023 12:41:54 +0700 Subject: [PATCH 132/298] refactor: Add Equality matcher class --- .../Consumer/Matcher/Matchers/Equality.php | 31 +++++++++++++++++++ .../Matcher/Model/MatcherInterface.php | 10 ++++++ .../Matcher/Matchers/EqualityTest.php | 18 +++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Equality.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/EqualityTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php new file mode 100644 index 00000000..9c6e3a34 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php @@ -0,0 +1,31 @@ +|string|float|int|bool|null $value + */ + public function __construct(private object|array|string|float|int|bool|null $value) + { + } + + public function jsonSerialize(): object + { + return (object) [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value + ]; + } + + public function getType(): string + { + return 'equality'; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php new file mode 100644 index 00000000..df200149 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php @@ -0,0 +1,10 @@ +assertSame( + '{"pact:matcher:type":"equality","value":"exact this string"}', + json_encode($string) + ); + } +} From 7dfff85d47b5c709089611a0780b0b9bdf7f9c85 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 14 Dec 2023 17:27:10 +0700 Subject: [PATCH 133/298] refactor: Add Attributes DTO --- .../Exception/AttributeConflictException.php | 7 +++ .../Matcher/Generators/AbstractDateTime.php | 11 +--- .../Matcher/Generators/AbstractGenerator.php | 19 ++++++ .../Consumer/Matcher/Generators/Date.php | 2 +- .../Consumer/Matcher/Generators/DateTime.php | 2 +- .../Matcher/Generators/MockServerURL.php | 16 ++--- .../Matcher/Generators/ProviderState.php | 14 +++-- .../Matcher/Generators/RandomBoolean.php | 15 ++--- .../Matcher/Generators/RandomDecimal.php | 16 ++--- .../Matcher/Generators/RandomHexadecimal.php | 16 ++--- .../Consumer/Matcher/Generators/RandomInt.php | 18 +++--- .../Matcher/Generators/RandomString.php | 16 ++--- .../Consumer/Matcher/Generators/Regex.php | 14 +++-- .../Consumer/Matcher/Generators/Time.php | 2 +- .../Consumer/Matcher/Generators/Uuid.php | 20 +++---- .../Consumer/Matcher/Model/Attributes.php | 59 +++++++++++++++++++ .../Matcher/Model/GeneratorInterface.php | 7 ++- .../Consumer/Matcher/Generators/DateTest.php | 22 ++++--- .../Matcher/Generators/DateTimeTest.php | 22 ++++--- .../Matcher/Generators/MockServerURLTest.php | 23 ++++++-- .../Matcher/Generators/ProviderStateTest.php | 23 ++++++-- .../Matcher/Generators/RandomBooleanTest.php | 23 ++++++-- .../Matcher/Generators/RandomDecimalTest.php | 23 ++++++-- .../Generators/RandomHexadecimalTest.php | 23 ++++++-- .../Matcher/Generators/RandomIntTest.php | 23 ++++++-- .../Matcher/Generators/RandomStringTest.php | 23 ++++++-- .../Consumer/Matcher/Generators/RegexTest.php | 23 ++++++-- .../Consumer/Matcher/Generators/TimeTest.php | 22 ++++--- .../Consumer/Matcher/Generators/UuidTest.php | 26 +++++--- .../Consumer/Matcher/Model/AttributesTest.php | 49 +++++++++++++++ 30 files changed, 422 insertions(+), 157 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/AttributeConflictException.php create mode 100644 src/PhpPact/Consumer/Matcher/Generators/AbstractGenerator.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/Attributes.php create mode 100644 tests/PhpPact/Consumer/Matcher/Model/AttributesTest.php diff --git a/src/PhpPact/Consumer/Matcher/Exception/AttributeConflictException.php b/src/PhpPact/Consumer/Matcher/Exception/AttributeConflictException.php new file mode 100644 index 00000000..5ea3d126 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/AttributeConflictException.php @@ -0,0 +1,7 @@ + */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - $data = ['pact:generator:type' => $this->getType()]; - + $data = []; if ($this->format !== null) { $data['format'] = $this->format; } @@ -27,6 +24,4 @@ public function jsonSerialize(): array return $data; } - - abstract protected function getType(): string; } diff --git a/src/PhpPact/Consumer/Matcher/Generators/AbstractGenerator.php b/src/PhpPact/Consumer/Matcher/Generators/AbstractGenerator.php new file mode 100644 index 00000000..df1a956c --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Generators/AbstractGenerator.php @@ -0,0 +1,19 @@ +getAttributesData()); + } + + /** + * @return array + */ + abstract protected function getAttributesData(): array; +} diff --git a/src/PhpPact/Consumer/Matcher/Generators/Date.php b/src/PhpPact/Consumer/Matcher/Generators/Date.php index 9caefa82..942f3c76 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/Date.php +++ b/src/PhpPact/Consumer/Matcher/Generators/Date.php @@ -16,7 +16,7 @@ */ class Date extends AbstractDateTime { - protected function getType(): string + public function getType(): string { return 'Date'; } diff --git a/src/PhpPact/Consumer/Matcher/Generators/DateTime.php b/src/PhpPact/Consumer/Matcher/Generators/DateTime.php index 8d682ed9..7c71e2fa 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/DateTime.php +++ b/src/PhpPact/Consumer/Matcher/Generators/DateTime.php @@ -16,7 +16,7 @@ */ class DateTime extends AbstractDateTime { - protected function getType(): string + public function getType(): string { return 'DateTime'; } diff --git a/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php b/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php index e8a34577..14430870 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php +++ b/src/PhpPact/Consumer/Matcher/Generators/MockServerURL.php @@ -2,29 +2,31 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a URL with the mock server as the base URL. * * Example regex: .*(/path)$ * Example example: http://localhost:1234/path */ -class MockServerURL implements GeneratorInterface +class MockServerURL extends AbstractGenerator { public function __construct(private string $regex, private string $example) { } + public function getType(): string + { + return 'MockServerURL'; + } + /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'regex' => $this->regex, - 'example' => $this->example, - 'pact:generator:type' => 'MockServerURL', + 'regex' => $this->regex, + 'example' => $this->example, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php b/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php index 88b2d675..0c364421 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php +++ b/src/PhpPact/Consumer/Matcher/Generators/ProviderState.php @@ -2,27 +2,29 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a value that is looked up from the provider state context using the given expression * * Example expression: /products/${id} */ -class ProviderState implements GeneratorInterface +class ProviderState extends AbstractGenerator { public function __construct(private string $expression) { } + public function getType(): string + { + return 'ProviderState'; + } + /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'expression' => $this->expression, - 'pact:generator:type' => 'ProviderState', + 'expression' => $this->expression, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php b/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php index 872ab29f..7bbe480a 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomBoolean.php @@ -2,20 +2,21 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random boolean value */ -class RandomBoolean implements GeneratorInterface +class RandomBoolean extends AbstractGenerator { + public function getType(): string + { + return 'RandomBoolean'; + } + /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - return [ - 'pact:generator:type' => 'RandomBoolean', - ]; + return []; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php b/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php index 62ed7a8a..0965578e 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomDecimal.php @@ -2,25 +2,27 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random big decimal value with the provided number of digits */ -class RandomDecimal implements GeneratorInterface +class RandomDecimal extends AbstractGenerator { public function __construct(private int $digits = 10) { } + public function getType(): string + { + return 'RandomDecimal'; + } + /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'digits' => $this->digits, - 'pact:generator:type' => 'RandomDecimal', + 'digits' => $this->digits, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php b/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php index 48ec6ed3..8a4a990f 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomHexadecimal.php @@ -2,25 +2,27 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random hexadecimal value of the given number of digits */ -class RandomHexadecimal implements GeneratorInterface +class RandomHexadecimal extends AbstractGenerator { public function __construct(private int $digits = 10) { } + public function getType(): string + { + return 'RandomHexadecimal'; + } + /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'digits' => $this->digits, - 'pact:generator:type' => 'RandomHexadecimal', + 'digits' => $this->digits, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php b/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php index df774c87..2f4ea5e8 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomInt.php @@ -2,26 +2,28 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random integer between the min and max values (inclusive) */ -class RandomInt implements GeneratorInterface +class RandomInt extends AbstractGenerator { public function __construct(private int $min = 0, private int $max = 10) { } + public function getType(): string + { + return 'RandomInt'; + } + /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'min' => $this->min, - 'max' => $this->max, - 'pact:generator:type' => 'RandomInt', + 'min' => $this->min, + 'max' => $this->max, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/RandomString.php b/src/PhpPact/Consumer/Matcher/Generators/RandomString.php index f8be32a5..911f7363 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/RandomString.php +++ b/src/PhpPact/Consumer/Matcher/Generators/RandomString.php @@ -2,25 +2,27 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random alphanumeric string of the provided length */ -class RandomString implements GeneratorInterface +class RandomString extends AbstractGenerator { public function __construct(private int $size = 10) { } + public function getType(): string + { + return 'RandomString'; + } + /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'size' => $this->size, - 'pact:generator:type' => 'RandomString', + 'size' => $this->size, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/Regex.php b/src/PhpPact/Consumer/Matcher/Generators/Regex.php index 0625269a..c80c3b18 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/Regex.php +++ b/src/PhpPact/Consumer/Matcher/Generators/Regex.php @@ -2,25 +2,27 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; - /** * Generates a random string from the provided regular expression */ -class Regex implements GeneratorInterface +class Regex extends AbstractGenerator { public function __construct(private string $regex) { } + public function getType(): string + { + return 'Regex'; + } + /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'regex' => $this->regex, - 'pact:generator:type' => 'Regex', + 'regex' => $this->regex, ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Generators/Time.php b/src/PhpPact/Consumer/Matcher/Generators/Time.php index a535ae0a..2ce4e359 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/Time.php +++ b/src/PhpPact/Consumer/Matcher/Generators/Time.php @@ -16,7 +16,7 @@ */ class Time extends AbstractDateTime { - protected function getType(): string + public function getType(): string { return 'Time'; } diff --git a/src/PhpPact/Consumer/Matcher/Generators/Uuid.php b/src/PhpPact/Consumer/Matcher/Generators/Uuid.php index ad5ceadb..47217801 100644 --- a/src/PhpPact/Consumer/Matcher/Generators/Uuid.php +++ b/src/PhpPact/Consumer/Matcher/Generators/Uuid.php @@ -2,7 +2,6 @@ namespace PhpPact\Consumer\Matcher\Generators; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PhpPact\Consumer\Matcher\Exception\InvalidUuidFormatException; /** @@ -13,7 +12,7 @@ * - upper-case-hyphenated (e.g 936DA01F-9ABD-4D9D-80C7-02AF85C822A8) * - URN (e.g. urn:uuid:936da01f-9abd-4d9d-80c7-02af85c822a8) */ -class Uuid implements GeneratorInterface +class Uuid extends AbstractGenerator { public const SIMPLE_FORMAT = 'simple'; public const LOWER_CASE_HYPHENATED_FORMAT = 'lower-case-hyphenated'; @@ -34,17 +33,18 @@ public function __construct(private ?string $format = null) } } + public function getType(): string + { + return 'Uuid'; + } + /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - $data = ['pact:generator:type' => 'Uuid']; - - if ($this->format !== null) { - $data['format'] = $this->format; - } - - return $data; + return $this->format !== null ? [ + 'format' => $this->format, + ] : []; } } diff --git a/src/PhpPact/Consumer/Matcher/Model/Attributes.php b/src/PhpPact/Consumer/Matcher/Model/Attributes.php new file mode 100644 index 00000000..8da2fc7a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Model/Attributes.php @@ -0,0 +1,59 @@ + $data + */ + public function __construct(private GeneratorInterface|MatcherInterface $parent, private array $data = []) + { + } + + public function getParent(): GeneratorInterface|MatcherInterface + { + return $this->parent; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + public function has(string $key): bool + { + return isset($this->data[$key]); + } + + public function get(string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function merge(self $attributes): self + { + foreach ($this->data as $key => $value) { + if ($attributes->has($key) && $value !== $attributes->get($key)) { + throw new AttributeConflictException(sprintf("Attribute '%s' of %s '%s' and %s '%s' are conflict", $key, $this->getParentType(), $this->getParentName(), $attributes->getParentType(), $attributes->getParentName())); + } + } + + return new self($this->parent, $this->data + $attributes->getData()); + } + + private function getParentType(): string + { + return $this->parent instanceof GeneratorInterface ? 'generator' : 'matcher'; + } + + private function getParentName(): string + { + return $this->parent->getType(); + } +} diff --git a/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php b/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php index 7701bdb5..202bd07f 100644 --- a/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php +++ b/src/PhpPact/Consumer/Matcher/Model/GeneratorInterface.php @@ -2,8 +2,9 @@ namespace PhpPact\Consumer\Matcher\Model; -use JsonSerializable; - -interface GeneratorInterface extends JsonSerializable +interface GeneratorInterface { + public function getType(): string; + + public function getAttributes(): Attributes; } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/DateTest.php b/tests/PhpPact/Consumer/Matcher/Generators/DateTest.php index 4354a611..1b0b2dfa 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/DateTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/DateTest.php @@ -7,15 +7,23 @@ class DateTest extends TestCase { + public function testType(): void + { + $generator = new Date(); + $this->assertSame('Date', $generator->getType()); + } + /** - * @testWith [null, null, "{\"pact:generator:type\":\"Date\"}"] - * ["yyyy-MM-dd", null, "{\"pact:generator:type\":\"Date\",\"format\":\"yyyy-MM-dd\"}"] - * [null, "+1 day", "{\"pact:generator:type\":\"Date\",\"expression\":\"+1 day\"}"] - * ["yyyy-MM-dd", "+1 day", "{\"pact:generator:type\":\"Date\",\"format\":\"yyyy-MM-dd\",\"expression\":\"+1 day\"}"] + * @testWith [null, null, []] + * ["yyyy-MM-dd", null, {"format":"yyyy-MM-dd"}] + * [null, "+1 day", {"expression":"+1 day"}] + * ["yyyy-MM-dd", "+1 day", {"format":"yyyy-MM-dd","expression":"+1 day"}] */ - public function testSerialize(?string $format, ?string $expression, string $json): void + public function testAttributes(?string $format, ?string $expression, array $data): void { - $date = new Date($format, $expression); - $this->assertSame($json, json_encode($date)); + $generator = new Date($format, $expression); + $attributes = $generator->getAttributes(); + $this->assertSame($generator, $attributes->getParent()); + $this->assertSame($data, $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/DateTimeTest.php b/tests/PhpPact/Consumer/Matcher/Generators/DateTimeTest.php index 9ae7d95f..57add126 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/DateTimeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/DateTimeTest.php @@ -7,15 +7,23 @@ class DateTimeTest extends TestCase { + public function testType(): void + { + $generator = new DateTime(); + $this->assertSame('DateTime', $generator->getType()); + } + /** - * @testWith [null, null, "{\"pact:generator:type\":\"DateTime\"}"] - * ["yyyy-MM-dd'T'HH:mm:ss", null, "{\"pact:generator:type\":\"DateTime\",\"format\":\"yyyy-MM-dd'T'HH:mm:ss\"}"] - * [null, "+1 day", "{\"pact:generator:type\":\"DateTime\",\"expression\":\"+1 day\"}"] - * ["yyyy-MM-dd'T'HH:mm:ss", "+1 day", "{\"pact:generator:type\":\"DateTime\",\"format\":\"yyyy-MM-dd'T'HH:mm:ss\",\"expression\":\"+1 day\"}"] + * @testWith [null, null, []] + * ["yyyy-MM-dd'T'HH:mm:ss", null, {"format":"yyyy-MM-dd'T'HH:mm:ss"}] + * [null, "+1 day", {"expression":"+1 day"}] + * ["yyyy-MM-dd'T'HH:mm:ss", "+1 day", {"format":"yyyy-MM-dd'T'HH:mm:ss","expression":"+1 day"}] */ - public function testSerialize(?string $format, ?string $expression, string $json): void + public function testAttributes(?string $format, ?string $expression, array $data): void { - $dateTime = new DateTime($format, $expression); - $this->assertSame($json, json_encode($dateTime)); + $generator = new DateTime($format, $expression); + $attributes = $generator->getAttributes(); + $this->assertSame($generator, $attributes->getParent()); + $this->assertSame($data, $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php b/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php index 4a7bb3b2..de6dbfde 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/MockServerURLTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\MockServerURL; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class MockServerURLTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new MockServerURL('.*(/path)$', 'http://localhost:1234/path'); + } + + public function testType(): void + { + $this->assertSame('MockServerURL', $this->generator->getType()); + } + + public function testAttributes(): void { - $url = new MockServerURL('.*(/path)$', 'http://localhost:1234/path'); - $this->assertSame( - '{"regex":".*(\/path)$","example":"http:\/\/localhost:1234\/path","pact:generator:type":"MockServerURL"}', - json_encode($url) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['regex' => '.*(/path)$', 'example' => 'http://localhost:1234/path'], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php b/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php index a6a80566..eaec9144 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/ProviderStateTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\ProviderState; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class ProviderStateTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new ProviderState('/products/${id}'); + } + + public function testType(): void + { + $this->assertSame('ProviderState', $this->generator->getType()); + } + + public function testAttributes(): void { - $url = new ProviderState('/products/${id}'); - $this->assertSame( - '{"expression":"\/products\/${id}","pact:generator:type":"ProviderState"}', - json_encode($url) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['expression' => '/products/${id}'], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php index c302f3c0..5bd8e790 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomBooleanTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\RandomBoolean; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RandomBooleanTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new RandomBoolean(); + } + + public function testType(): void + { + $this->assertSame('RandomBoolean', $this->generator->getType()); + } + + public function testAttributes(): void { - $boolean = new RandomBoolean(); - $this->assertSame( - '{"pact:generator:type":"RandomBoolean"}', - json_encode($boolean) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame([], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php index 22aac2a9..3b0b354e 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomDecimalTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\RandomDecimal; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RandomDecimalTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new RandomDecimal(12); + } + + public function testType(): void + { + $this->assertSame('RandomDecimal', $this->generator->getType()); + } + + public function testAttributes(): void { - $decimal = new RandomDecimal(12); - $this->assertSame( - '{"digits":12,"pact:generator:type":"RandomDecimal"}', - json_encode($decimal) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['digits' => 12], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php index be13ab06..dd95abc9 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomHexadecimalTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RandomHexadecimalTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new RandomHexadecimal(8); + } + + public function testType(): void + { + $this->assertSame('RandomHexadecimal', $this->generator->getType()); + } + + public function testAttributes(): void { - $hexaDecimal = new RandomHexadecimal(8); - $this->assertSame( - '{"digits":8,"pact:generator:type":"RandomHexadecimal"}', - json_encode($hexaDecimal) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['digits' => 8], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomIntTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomIntTest.php index 70d3ff9d..ef8b8c6e 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RandomIntTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomIntTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\RandomInt; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RandomIntTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new RandomInt(5, 15); + } + + public function testType(): void + { + $this->assertSame('RandomInt', $this->generator->getType()); + } + + public function testAttributes(): void { - $int = new RandomInt(5, 15); - $this->assertSame( - '{"min":5,"max":15,"pact:generator:type":"RandomInt"}', - json_encode($int) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['min' => 5, 'max' => 15], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php index 2adf26bf..5afdb898 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RandomStringTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\RandomString; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RandomStringTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new RandomString(11); + } + + public function testType(): void + { + $this->assertSame('RandomString', $this->generator->getType()); + } + + public function testAttributes(): void { - $string = new RandomString(11); - $this->assertSame( - '{"size":11,"pact:generator:type":"RandomString"}', - json_encode($string) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['size' => 11], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php b/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php index 00332a02..35a1c158 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/RegexTest.php @@ -3,16 +3,27 @@ namespace PhpPactTest\Consumer\Matcher\Generators; use PhpPact\Consumer\Matcher\Generators\Regex; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; class RegexTest extends TestCase { - public function testSerialize(): void + private GeneratorInterface $generator; + + protected function setUp(): void + { + $this->generator = new Regex('[\w\d]+'); + } + + public function testType(): void + { + $this->assertSame('Regex', $this->generator->getType()); + } + + public function testAttributes(): void { - $regex = new Regex('[\w\d]+'); - $this->assertSame( - '{"regex":"[\\\\w\\\\d]+","pact:generator:type":"Regex"}', - json_encode($regex) - ); + $attributes = $this->generator->getAttributes(); + $this->assertSame($this->generator, $attributes->getParent()); + $this->assertSame(['regex' => '[\w\d]+'], $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/TimeTest.php b/tests/PhpPact/Consumer/Matcher/Generators/TimeTest.php index d42d4e74..f0a887d1 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/TimeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/TimeTest.php @@ -7,15 +7,23 @@ class TimeTest extends TestCase { + public function testType(): void + { + $generator = new Time(); + $this->assertSame('Time', $generator->getType()); + } + /** - * @testWith [null, null, "{\"pact:generator:type\":\"Time\"}"] - * ["HH:mm:ss", null, "{\"pact:generator:type\":\"Time\",\"format\":\"HH:mm:ss\"}"] - * [null, "+1 hour", "{\"pact:generator:type\":\"Time\",\"expression\":\"+1 hour\"}"] - * ["HH:mm:ss", "+1 hour", "{\"pact:generator:type\":\"Time\",\"format\":\"HH:mm:ss\",\"expression\":\"+1 hour\"}"] + * @testWith [null, null, []] + * ["HH:mm:ss", null, {"format":"HH:mm:ss"}] + * [null, "+1 hour", {"expression":"+1 hour"}] + * ["HH:mm:ss", "+1 hour", {"format":"HH:mm:ss","expression":"+1 hour"}] */ - public function testSerialize(?string $format, ?string $expression, string $json): void + public function testAttributes(?string $format, ?string $expression, array $data): void { - $time = new Time($format, $expression); - $this->assertSame($json, json_encode($time)); + $generator = new Time($format, $expression); + $attributes = $generator->getAttributes(); + $this->assertSame($generator, $attributes->getParent()); + $this->assertSame($data, $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Generators/UuidTest.php b/tests/PhpPact/Consumer/Matcher/Generators/UuidTest.php index c5e08adb..313d0a55 100644 --- a/tests/PhpPact/Consumer/Matcher/Generators/UuidTest.php +++ b/tests/PhpPact/Consumer/Matcher/Generators/UuidTest.php @@ -8,21 +8,29 @@ class UuidTest extends TestCase { + public function testType(): void + { + $generator = new Uuid(); + $this->assertSame('Uuid', $generator->getType()); + } + /** - * @testWith [null, "{\"pact:generator:type\":\"Uuid\"}"] - * ["simple", "{\"pact:generator:type\":\"Uuid\",\"format\":\"simple\"}"] - * ["lower-case-hyphenated", "{\"pact:generator:type\":\"Uuid\",\"format\":\"lower-case-hyphenated\"}"] - * ["upper-case-hyphenated", "{\"pact:generator:type\":\"Uuid\",\"format\":\"upper-case-hyphenated\"}"] - * ["URN", "{\"pact:generator:type\":\"Uuid\",\"format\":\"URN\"}"] + * @testWith [null, []] + * ["simple", {"format":"simple"}] + * ["lower-case-hyphenated", {"format":"lower-case-hyphenated"}] + * ["upper-case-hyphenated", {"format":"upper-case-hyphenated"}] + * ["URN", {"format":"URN"}] * ["invalid", null] */ - public function testSerialize(?string $format, ?string $json): void + public function testAttributes(?string $format, ?array $data): void { - if (!$json) { + if (null === $data) { $this->expectException(InvalidUuidFormatException::class); $this->expectExceptionMessage('Format invalid is not supported. Supported formats are: simple, lower-case-hyphenated, upper-case-hyphenated, URN'); } - $uuid = new Uuid($format); - $this->assertSame($json, json_encode($uuid)); + $generator = new Uuid($format); + $attributes = $generator->getAttributes(); + $this->assertSame($generator, $attributes->getParent()); + $this->assertSame($data, $attributes->getData()); } } diff --git a/tests/PhpPact/Consumer/Matcher/Model/AttributesTest.php b/tests/PhpPact/Consumer/Matcher/Model/AttributesTest.php new file mode 100644 index 00000000..1b8ce95c --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Model/AttributesTest.php @@ -0,0 +1,49 @@ +assertSame($generator, $subject->getParent()); + + //$subject = new Attributes($matcher = new NullValue()); + //$this->assertSame($matcher, $subject->getParent()); + } + + public function testData(): void + { + $subject = new Attributes(new RandomBoolean(), $data = ['key' => 'value']); + $this->assertSame($data, $subject->getData()); + $this->assertFalse($subject->has('new key')); + $this->assertNull($subject->get('new key')); + $this->assertTrue($subject->has('key')); + $this->assertSame('value', $subject->get('key')); + } + + public function testMergeConflict(): void + { + $attributes = new Attributes(new RandomBoolean(), ['key' => 'value 1']); + $this->expectException(AttributeConflictException::class); + //$this->expectExceptionMessage("Attribute 'key' of generator 'RandomBoolean' and matcher 'null' are conflict"); + $this->expectExceptionMessage("Attribute 'key' of generator 'RandomBoolean' and generator 'RandomBoolean' are conflict"); + $attributes->merge(new Attributes(new RandomBoolean(), ['key' => 'value 2'])); + } + + public function testMerge(): void + { + $parent = new RandomBoolean(); + $attributes = new Attributes($parent, ['key' => 'value', 'key 2' => 123]); + $merged = $attributes->merge(new Attributes(new RandomBoolean(), ['key' => 'value', 'key 3' => ['value 1', 'value 2']])); + $this->assertSame($parent, $merged->getParent()); + $this->assertSame(['key' => 'value', 'key 2' => 123, 'key 3' => ['value 1', 'value 2']], $merged->getData()); + } +} From eac0da52b92d77025e00e1e75ad356979910a78a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 13 Dec 2023 13:13:59 +0700 Subject: [PATCH 134/298] refactor: Add Regex matcher class --- .../Exception/GeneratorRequiredException.php | 7 ++ .../Exception/InvalidRegexException.php | 7 ++ .../Matchers/GeneratorAwareMatcher.php | 61 ++++++++++++++++ .../Consumer/Matcher/Matchers/Regex.php | 70 +++++++++++++++++++ .../Matcher/Model/GeneratorAwareInterface.php | 12 ++++ .../GeneratorAwareMatcherTestCase.php | 20 ++++++ .../Consumer/Matcher/Matchers/RegexTest.php | 34 +++++++++ 7 files changed, 211 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/GeneratorRequiredException.php create mode 100644 src/PhpPact/Consumer/Matcher/Exception/InvalidRegexException.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Regex.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/GeneratorAwareInterface.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php diff --git a/src/PhpPact/Consumer/Matcher/Exception/GeneratorRequiredException.php b/src/PhpPact/Consumer/Matcher/Exception/GeneratorRequiredException.php new file mode 100644 index 00000000..92f3273a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/GeneratorRequiredException.php @@ -0,0 +1,7 @@ +generator = $generator; + } + + public function getGenerator(): ?GeneratorInterface + { + return $this->generator; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $data = [ + 'pact:matcher:type' => $this->getType(), + ]; + + if (null === $this->getValue()) { + if (!$this->generator) { + throw new GeneratorRequiredException(sprintf("Generator is required for matcher '%s' when example value is not set", $this->getType())); + } + + return $data + ['pact:generator:type' => $this->generator->getType()] + $this->getMergedAttributes()->getData(); + } + + return $data + $this->getAttributes()->getData() + ['value' => $this->getValue()]; + } + + protected function getAttributes(): Attributes + { + return new Attributes($this, $this->getAttributesData()); + } + + protected function getMergedAttributes(): Attributes + { + return $this->getAttributes()->merge($this->generator->getAttributes()); + } + + /** + * @return array + */ + abstract protected function getAttributesData(): array; + + abstract protected function getValue(): mixed; +} diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Regex.php b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php new file mode 100644 index 00000000..2d5fa859 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php @@ -0,0 +1,70 @@ +setGenerator(new RegexGenerator($this->regex)); + } + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + if (null !== $this->values) { + $this->validateRegex(); + } + + return parent::jsonSerialize(); + } + + private function validateRegex(): void + { + foreach ((array) $this->values as $value) { + $result = preg_match("/$this->regex/", $value); + + if ($result === false || $result === 0) { + $errorCode = preg_last_error(); + + throw new InvalidRegexException("The pattern '{$this->regex}' is not valid for value '{$value}'. Failed with error code {$errorCode}."); + } + } + } + + public function getType(): string + { + return 'regex'; + } + + /** + * @return string|string[]|null + */ + protected function getValue(): string|array|null + { + return $this->values; + } + + /** + * @return array + */ + protected function getAttributesData(): array + { + return ['regex' => $this->regex]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Model/GeneratorAwareInterface.php b/src/PhpPact/Consumer/Matcher/Model/GeneratorAwareInterface.php new file mode 100644 index 00000000..5eff073b --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Model/GeneratorAwareInterface.php @@ -0,0 +1,12 @@ +expectException(GeneratorRequiredException::class); + $this->expectExceptionMessage(sprintf("Generator is required for matcher '%s' when example value is not set", $this->matcher->getType())); + $this->matcher->setGenerator(null); + json_encode($this->matcher); + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php new file mode 100644 index 00000000..6823e04a --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php @@ -0,0 +1,34 @@ +matcher = new Regex($this->regex); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"regex\",\"pact:generator:type\":\"Regex\",\"regex\":\"\\\\d+\"}"] + * ["number", null] + * [["integer"], null] + * ["12+", "{\"pact:matcher:type\":\"regex\",\"regex\":\"\\\\d+\",\"value\":\"12+\"}"] + * [["12.3", "456"], "{\"pact:matcher:type\":\"regex\",\"regex\":\"\\\\d+\",\"value\":[\"12.3\",\"456\"]}"] + */ + public function testSerialize(string|array|null $values, ?string $json): void + { + if (!$json && $values) { + $this->expectException(InvalidRegexException::class); + $value = is_array($values) ? $values[0] : $values; + $this->expectExceptionMessage("The pattern '{$this->regex}' is not valid for value '{$value}'. Failed with error code 0."); + } + $this->matcher = new Regex($this->regex, $values); + $this->assertSame($json, json_encode($this->matcher)); + } +} From d4b5e766bc33e8ef07e8d97665ea5b2b7b919fae Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 09:20:44 +0700 Subject: [PATCH 135/298] refactor: Revert object to array --- src/PhpPact/Consumer/Matcher/Matchers/Equality.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php index 9c6e3a34..bbf779ee 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php @@ -16,9 +16,12 @@ public function __construct(private object|array|string|float|int|bool|null $val { } - public function jsonSerialize(): object + /** + * @return array + */ + public function jsonSerialize(): array { - return (object) [ + return [ 'pact:matcher:type' => $this->getType(), 'value' => $this->value ]; From 99e1a8f7e814b4733105f10cad66fdb8efe47991 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 09:56:13 +0700 Subject: [PATCH 136/298] refactor: Add Date matcher class --- .../Matcher/Matchers/AbstractDateTime.php | 23 +++++++++++++++ .../Consumer/Matcher/Matchers/Date.php | 28 +++++++++++++++++++ .../Consumer/Matcher/Matchers/DateTest.php | 24 ++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Date.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php new file mode 100644 index 00000000..47c4ec28 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php @@ -0,0 +1,23 @@ + + */ + protected function getAttributesData(): array + { + return ['format' => $this->format]; + } + + protected function getValue(): ?string + { + return $this->value; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Date.php b/src/PhpPact/Consumer/Matcher/Matchers/Date.php new file mode 100644 index 00000000..67ca1e82 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Date.php @@ -0,0 +1,28 @@ +setGenerator(new DateGenerator($format)); + } + parent::__construct($format, $value); + } + + public function getType(): string + { + return 'date'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php new file mode 100644 index 00000000..95a4b0aa --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php @@ -0,0 +1,24 @@ +matcher = new Date(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"date\",\"pact:generator:type\":\"Date\",\"format\":\"yyyy-MM-dd\"}"] + * ["1995-02-04", "{\"pact:matcher:type\":\"date\",\"format\":\"yyyy-MM-dd\",\"value\":\"1995-02-04\"}"] + */ + public function testSerialize(?string $value, string $json): void + { + $format = 'yyyy-MM-dd'; + $this->matcher = new Date($format, $value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From ab5ccc799da40d503beb4e97cd06a6ebcdd97f7e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:11:34 +0700 Subject: [PATCH 137/298] refactor: Add Time matcher class --- .../Consumer/Matcher/Matchers/Time.php | 28 +++++++++++++++++++ .../Consumer/Matcher/Matchers/TimeTest.php | 24 ++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Time.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Time.php b/src/PhpPact/Consumer/Matcher/Matchers/Time.php new file mode 100644 index 00000000..ac3f9fad --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Time.php @@ -0,0 +1,28 @@ +setGenerator(new TimeGenerator($format)); + } + parent::__construct($format, $value); + } + + public function getType(): string + { + return 'time'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php new file mode 100644 index 00000000..b61647bb --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php @@ -0,0 +1,24 @@ +matcher = new Time(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"time\",\"pact:generator:type\":\"Time\",\"format\":\"HH:mm:ss\"}"] + * ["12:02::34", "{\"pact:matcher:type\":\"time\",\"format\":\"HH:mm:ss\",\"value\":\"12:02::34\"}"] + */ + public function testSerialize(?string $value, string $json): void + { + $format = 'HH:mm:ss'; + $this->matcher = new Time($format, $value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 3d3636a3624a038235439f0f98442a0a4a056c54 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:13:25 +0700 Subject: [PATCH 138/298] refactor: Add DateTime matcher class --- .../Consumer/Matcher/Matchers/DateTime.php | 28 +++++++++++++++++++ .../Matcher/Matchers/DateTimeTest.php | 24 ++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/DateTime.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/DateTime.php b/src/PhpPact/Consumer/Matcher/Matchers/DateTime.php new file mode 100644 index 00000000..5d2f8184 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/DateTime.php @@ -0,0 +1,28 @@ +setGenerator(new DateTimeGenerator($format)); + } + parent::__construct($format, $value); + } + + public function getType(): string + { + return 'datetime'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php new file mode 100644 index 00000000..1ffc4dd8 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php @@ -0,0 +1,24 @@ +matcher = new DateTime(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"datetime\",\"pact:generator:type\":\"DateTime\",\"format\":\"yyyy-MM-dd'T'HH:mm:ss\"}"] + * ["1995-02-04T22:45:00", "{\"pact:matcher:type\":\"datetime\",\"format\":\"yyyy-MM-dd'T'HH:mm:ss\",\"value\":\"1995-02-04T22:45:00\"}"] + */ + public function testSerialize(?string $value, string $json): void + { + $format = "yyyy-MM-dd'T'HH:mm:ss"; + $this->matcher = new DateTime($format, $value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 540960e16a429a20b8b88f86ab891c85e894b7c4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:22:55 +0700 Subject: [PATCH 139/298] refactor: Add Boolean matcher class --- .../Consumer/Matcher/Matchers/Boolean.php | 33 +++++++++++++++++++ .../Consumer/Matcher/Matchers/BooleanTest.php | 24 ++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Boolean.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php new file mode 100644 index 00000000..0965e378 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php @@ -0,0 +1,33 @@ +setGenerator(new RandomBoolean()); + } + } + + public function getType(): string + { + return 'boolean'; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): ?bool + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php new file mode 100644 index 00000000..7188b48c --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php @@ -0,0 +1,24 @@ +matcher = new Boolean(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"boolean\",\"pact:generator:type\":\"RandomBoolean\"}"] + * [true, "{\"pact:matcher:type\":\"boolean\",\"value\":true}"] + * [false, "{\"pact:matcher:type\":\"boolean\",\"value\":false}"] + */ + public function testSerialize(?bool $value, string $json): void + { + $this->matcher = new Boolean($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 3b3c899ebcbba11dfb1e3fe9efc884130cf2232c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:26:17 +0700 Subject: [PATCH 140/298] refactor: Add ContentType matcher class --- .../Consumer/Matcher/Matchers/ContentType.php | 31 +++++++++++++++++++ .../Matcher/Matchers/ContentTypeTest.php | 18 +++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/ContentType.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php new file mode 100644 index 00000000..052e6e41 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php @@ -0,0 +1,31 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'value' => $this->contentType, + 'pact:matcher:type' => $this->getType(), + ]; + } + + public function getType(): string + { + return 'contentType'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php new file mode 100644 index 00000000..4b837779 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"value":"text\/csv","pact:matcher:type":"contentType"}', + json_encode($contentType) + ); + } +} From 2dfda6312c69d1c19c804fb6b1bd30fe805375aa Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:28:33 +0700 Subject: [PATCH 141/298] refactor: Add Decimal matcher class --- .../Consumer/Matcher/Matchers/Decimal.php | 33 +++++++++++++++++++ .../Consumer/Matcher/Matchers/DecimalTest.php | 23 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Decimal.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php new file mode 100644 index 00000000..69d2e54a --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php @@ -0,0 +1,33 @@ +setGenerator(new RandomDecimal()); + } + } + + public function getType(): string + { + return 'decimal'; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): ?float + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php new file mode 100644 index 00000000..cb4b84d5 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php @@ -0,0 +1,23 @@ +matcher = new Decimal(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"decimal\",\"pact:generator:type\":\"RandomDecimal\",\"digits\":10}"] + * [1.23, "{\"pact:matcher:type\":\"decimal\",\"value\":1.23}"] + */ + public function testSerialize(?float $value, string $json): void + { + $this->matcher = new Decimal($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From e6dd3de5092f2e3d18be352c0797562d011c6fce Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:38:27 +0700 Subject: [PATCH 142/298] refactor: Add Includes matcher class --- .../Consumer/Matcher/Matchers/Includes.php | 31 +++++++++++++++++++ .../Matcher/Matchers/IncludesTest.php | 18 +++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Includes.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/IncludesTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Includes.php b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php new file mode 100644 index 00000000..7435d919 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php @@ -0,0 +1,31 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value, + ]; + } + + public function getType(): string + { + return 'include'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/IncludesTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/IncludesTest.php new file mode 100644 index 00000000..6310f3fe --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/IncludesTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"pact:matcher:type":"include","value":"contains this string"}', + json_encode($string) + ); + } +} From 312a649b2065e45e45709029f493e11406fa8d81 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:41:49 +0700 Subject: [PATCH 143/298] refactor: Add Integer matcher class --- .../Consumer/Matcher/Matchers/Integer.php | 33 +++++++++++++++++++ .../Consumer/Matcher/Matchers/IntegerTest.php | 23 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Integer.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Integer.php b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php new file mode 100644 index 00000000..bacc17a8 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php @@ -0,0 +1,33 @@ +setGenerator(new RandomInt()); + } + } + + public function getType(): string + { + return 'integer'; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): ?int + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php new file mode 100644 index 00000000..e845d4ce --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php @@ -0,0 +1,23 @@ +matcher = new Integer(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"integer\",\"pact:generator:type\":\"RandomInt\",\"min\":0,\"max\":10}"] + * [123, "{\"pact:matcher:type\":\"integer\",\"value\":123}"] + */ + public function testSerialize(?int $value, string $json): void + { + $this->matcher = new Integer($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 358fd2923b5577e56a6c78ffffd408f8f5e383b8 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:44:23 +0700 Subject: [PATCH 144/298] refactor: Add MaxType matcher class --- .../Consumer/Matcher/Matchers/MaxType.php | 38 +++++++++++++++++++ .../Consumer/Matcher/Matchers/MaxTypeTest.php | 21 ++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/MaxType.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/MaxTypeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php new file mode 100644 index 00000000..098c80b9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php @@ -0,0 +1,38 @@ +$values + */ + public function __construct( + private array $values, + private int $max, + ) { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'max' => $this->max, + 'value' => array_values($this->values), + ]; + } + + public function getType(): string + { + return 'type'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/MaxTypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/MaxTypeTest.php new file mode 100644 index 00000000..2c034c90 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/MaxTypeTest.php @@ -0,0 +1,21 @@ +assertSame( + '{"pact:matcher:type":"type","max":3,"value":["string value"]}', + json_encode($array) + ); + } +} From 02bdd1ab170b23b4c635e3291af600ceb55cdefc Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:46:47 +0700 Subject: [PATCH 145/298] refactor: Add MinMaxType matcher class --- .../Consumer/Matcher/Matchers/MinMaxType.php | 40 +++++++++++++++++++ .../Matcher/Matchers/MinMaxTypeTest.php | 22 ++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/MinMaxTypeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php new file mode 100644 index 00000000..35586b24 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php @@ -0,0 +1,40 @@ + $values + */ + public function __construct( + private array $values, + private int $min, + private int $max, + ) { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'min' => $this->min, + 'max' => $this->max, + 'value' => array_values($this->values), + ]; + } + + public function getType(): string + { + return 'type'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/MinMaxTypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/MinMaxTypeTest.php new file mode 100644 index 00000000..49271a75 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/MinMaxTypeTest.php @@ -0,0 +1,22 @@ +assertSame( + '{"pact:matcher:type":"type","min":2,"max":5,"value":[1.23,2.34]}', + json_encode($array) + ); + } +} From cde0e981a9080b74c60c220900ec73caa403c54d Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:48:48 +0700 Subject: [PATCH 146/298] refactor: Add MinType matcher class --- .../Consumer/Matcher/Matchers/MinType.php | 38 +++++++++++++++++++ .../Consumer/Matcher/Matchers/MinTypeTest.php | 23 +++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/MinType.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/MinTypeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php new file mode 100644 index 00000000..cc4673b9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php @@ -0,0 +1,38 @@ + $values + */ + public function __construct( + private array $values, + private int $min, + ) { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'min' => $this->min, + 'value' => array_values($this->values), + ]; + } + + public function getType(): string + { + return 'type'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/MinTypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/MinTypeTest.php new file mode 100644 index 00000000..dba58a43 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/MinTypeTest.php @@ -0,0 +1,23 @@ +assertSame( + '{"pact:matcher:type":"type","min":3,"value":[123,34,5]}', + json_encode($array) + ); + } +} From 9b22e54e042ba1f9037a95f711333131aa56b15b Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:50:26 +0700 Subject: [PATCH 147/298] refactor: Add NotEmpty matcher class --- .../Consumer/Matcher/Matchers/NotEmpty.php | 34 +++++++++++++++++++ .../Matcher/Matchers/NotEmptyTest.php | 18 ++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/NotEmptyTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php new file mode 100644 index 00000000..b87ad6d9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php @@ -0,0 +1,34 @@ +|string|float|int|bool $value + */ + public function __construct(private object|array|string|float|int|bool $value) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value, + ]; + } + + public function getType(): string + { + return 'notEmpty'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/NotEmptyTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/NotEmptyTest.php new file mode 100644 index 00000000..fd5d89f5 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/NotEmptyTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"pact:matcher:type":"notEmpty","value":["some text"]}', + json_encode($array) + ); + } +} From 90b2bfb79040f83eb1032ae71def48208b162292 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:51:52 +0700 Subject: [PATCH 148/298] refactor: Add NullValue matcher class --- .../Consumer/Matcher/Matchers/NullValue.php | 26 +++++++++++++++++++ .../Matcher/Matchers/NullValueTest.php | 18 +++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/NullValue.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/NullValueTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php new file mode 100644 index 00000000..5c00f7e7 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php @@ -0,0 +1,26 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + ]; + } + + public function getType(): string + { + return 'null'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/NullValueTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/NullValueTest.php new file mode 100644 index 00000000..4e7906d0 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/NullValueTest.php @@ -0,0 +1,18 @@ +assertSame( + '{"pact:matcher:type":"null"}', + json_encode($null) + ); + } +} From ddf9f4da0d4c6e20374b617e47956d7d6b01e242 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:54:02 +0700 Subject: [PATCH 149/298] refactor: Add Number matcher class --- .../Consumer/Matcher/Matchers/Number.php | 33 +++++++++++++++++++ .../Consumer/Matcher/Matchers/NumberTest.php | 24 ++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Number.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Number.php b/src/PhpPact/Consumer/Matcher/Matchers/Number.php new file mode 100644 index 00000000..d98b9694 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Number.php @@ -0,0 +1,33 @@ +setGenerator(new RandomInt()); + } + } + + public function getType(): string + { + return 'number'; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): int|float|null + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php new file mode 100644 index 00000000..92591aa8 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php @@ -0,0 +1,24 @@ +matcher = new Number(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"number\",\"pact:generator:type\":\"RandomInt\",\"min\":0,\"max\":10}"] + * [123, "{\"pact:matcher:type\":\"number\",\"value\":123}"] + * [12.3, "{\"pact:matcher:type\":\"number\",\"value\":12.3}"] + */ + public function testSerialize(int|float|null $value, string $json): void + { + $this->matcher = new Number($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 20a104eef7eaeecae2a7b4241abe55fd7dbb75e5 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:55:55 +0700 Subject: [PATCH 150/298] refactor: Add Semver matcher class --- .../Consumer/Matcher/Matchers/Semver.php | 33 +++++++++++++++++++ .../Consumer/Matcher/Matchers/SemverTest.php | 23 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Semver.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Semver.php b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php new file mode 100644 index 00000000..11c63673 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php @@ -0,0 +1,33 @@ +setGenerator(new Regex('\d+\.\d+\.\d+')); + } + } + + public function getType(): string + { + return 'semver'; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): ?string + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php new file mode 100644 index 00000000..19bff24a --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php @@ -0,0 +1,23 @@ +matcher = new Semver(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"semver\",\"pact:generator:type\":\"Regex\",\"regex\":\"\\\\d+\\\\.\\\\d+\\\\.\\\\d+\"}"] + * ["1.2.3", "{\"pact:matcher:type\":\"semver\",\"value\":\"1.2.3\"}"] + */ + public function testSerialize(?string $value, string $json): void + { + $this->matcher = new Semver($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 3da9618ed736f8321e39bb655e94a59958df308b Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:58:04 +0700 Subject: [PATCH 151/298] refactor: Add StatusCode matcher class --- .../Exception/InvalidHttpStatusException.php | 7 +++ .../Consumer/Matcher/Matchers/StatusCode.php | 53 +++++++++++++++++++ .../Matcher/Matchers/StatusCodeTest.php | 35 ++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/InvalidHttpStatusException.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Exception/InvalidHttpStatusException.php b/src/PhpPact/Consumer/Matcher/Exception/InvalidHttpStatusException.php new file mode 100644 index 00000000..6c250dc9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/InvalidHttpStatusException.php @@ -0,0 +1,7 @@ + [100, 199], + HttpStatus::SUCCESS => [200, 299], + HttpStatus::REDIRECT => [300, 399], + HttpStatus::CLIENT_ERROR => [400, 499], + HttpStatus::SERVER_ERROR => [500, 599], + HttpStatus::NON_ERROR => [100, 399], + HttpStatus::ERROR => [400, 599], + default => [100, 199], // Can't happen, just to make PHPStan happy + }; + + $this->setGenerator(new RandomInt($min, $max)); + } + } + + public function getType(): string + { + return 'statusCode'; + } + + /** + * @return array + */ + protected function getAttributesData(): array + { + return ['status' => $this->status]; + } + + protected function getValue(): ?int + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php new file mode 100644 index 00000000..5747a14f --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php @@ -0,0 +1,35 @@ +matcher = new StatusCode('info'); + } + + /** + * @testWith ["invalid", null, null] + * ["info", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"info\",\"min\":100,\"max\":199}"] + * ["success", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"success\",\"min\":200,\"max\":299}"] + * ["redirect", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"redirect\",\"min\":300,\"max\":399}"] + * ["clientError", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"clientError\",\"min\":400,\"max\":499}"] + * ["serverError", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"serverError\",\"min\":500,\"max\":599}"] + * ["nonError", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"nonError\",\"min\":100,\"max\":399}"] + * ["error", null, "{\"pact:matcher:type\":\"statusCode\",\"pact:generator:type\":\"RandomInt\",\"status\":\"error\",\"min\":400,\"max\":599}"] + * ["info", "123", "{\"pact:matcher:type\":\"statusCode\",\"status\":\"info\",\"value\":123}"] + */ + public function testSerialize(string $status, ?string $value, ?string $json): void + { + if (!$json) { + $this->expectException(InvalidHttpStatusException::class); + $this->expectExceptionMessage("Status 'invalid' is not supported. Supported status are: info, success, redirect, clientError, serverError, nonError, error"); + } + $this->matcher = new StatusCode($status, $value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From 7be55cc00420492a55ce3898a6375e99adb6a13c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:59:56 +0700 Subject: [PATCH 152/298] refactor: Add StringValue matcher class --- .../Consumer/Matcher/Matchers/StringValue.php | 50 +++++++++++++++++++ .../Matcher/Matchers/StringValueTest.php | 26 ++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/StringValue.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php new file mode 100644 index 00000000..6cbc4bc2 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php @@ -0,0 +1,50 @@ +setGenerator(new RandomString()); + } + } + + public function getType(): string + { + return 'type'; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $data = [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->getValue() ?? 'some string', + ]; + + if ($this->getGenerator()) { + return $data + ['pact:generator:type' => $this->getGenerator()->getType()] + $this->getMergedAttributes()->getData(); + } + + return $data; + } + + protected function getAttributesData(): array + { + return []; + } + + protected function getValue(): ?string + { + return $this->value; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php new file mode 100644 index 00000000..602bead2 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php @@ -0,0 +1,26 @@ +matcher = new StringValue(); + } + + /** + * @testWith [null, "{\"pact:matcher:type\":\"type\",\"value\":\"some string\",\"pact:generator:type\":\"RandomString\",\"size\":10}"] + * ["test", "{\"pact:matcher:type\":\"type\",\"value\":\"test\"}"] + */ + public function testSerialize(?string $value, string $json): void + { + $this->matcher = new StringValue($value); + $this->assertSame($json, json_encode($this->matcher)); + } +} From b276a146c0bd3ae17c652f182d79ee09d77aab50 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 11:01:20 +0700 Subject: [PATCH 153/298] refactor: Add Type matcher class --- .../Consumer/Matcher/Matchers/Type.php | 34 +++++++++++++++++++ .../Consumer/Matcher/Matchers/TypeTest.php | 19 +++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Type.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/TypeTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Type.php b/src/PhpPact/Consumer/Matcher/Matchers/Type.php new file mode 100644 index 00000000..912561fa --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Type.php @@ -0,0 +1,34 @@ +|string|float|int|bool|null $value + */ + public function __construct(private object|array|string|float|int|bool|null $value) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value, + ]; + } + + public function getType(): string + { + return 'type'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/TypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/TypeTest.php new file mode 100644 index 00000000..3501c291 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/TypeTest.php @@ -0,0 +1,19 @@ + 'value']; + $object = new Type($value); + $this->assertSame( + '{"pact:matcher:type":"type","value":{"key":"value"}}', + json_encode($object) + ); + } +} From ba11d7730319e291f309b645b345939ca2125214 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 11:03:55 +0700 Subject: [PATCH 154/298] refactor: Add Values matcher class --- .../Consumer/Matcher/Matchers/Values.php | 34 +++++++++++++++++++ .../Consumer/Matcher/Matchers/ValuesTest.php | 19 +++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/Values.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/ValuesTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Values.php b/src/PhpPact/Consumer/Matcher/Matchers/Values.php new file mode 100644 index 00000000..789c4643 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/Values.php @@ -0,0 +1,34 @@ + $values + */ + public function __construct(private array $values) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->values, + ]; + } + + public function getType(): string + { + return 'values'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/ValuesTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/ValuesTest.php new file mode 100644 index 00000000..7b5885c0 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/ValuesTest.php @@ -0,0 +1,19 @@ +assertSame($json, json_encode($array)); + } +} From 25ab9719d4d22507f27e117eaeb05570de830c97 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:20:12 +0700 Subject: [PATCH 155/298] refactor: Add ArrayContains matcher class --- .../Matcher/Matchers/ArrayContains.php | 34 +++++++++++++++++++ .../Matcher/Matchers/ArrayContainsTest.php | 24 +++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/ArrayContainsTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php new file mode 100644 index 00000000..da9368df --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php @@ -0,0 +1,34 @@ + $variants + */ + public function __construct(private array $variants) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'variants' => array_values($this->variants), + ]; + } + + public function getType(): string + { + return 'arrayContains'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/ArrayContainsTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/ArrayContainsTest.php new file mode 100644 index 00000000..d611b65b --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/ArrayContainsTest.php @@ -0,0 +1,24 @@ +assertSame( + '{"pact:matcher:type":"arrayContains","variants":[{"pact:matcher:type":"type","value":"string"},{"pact:matcher:type":"integer","pact:generator:type":"RandomInt","min":0,"max":10}]}', + json_encode($array) + ); + } +} From 82841706c12503f968a409a507c1c196ac37b26e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:31:09 +0700 Subject: [PATCH 156/298] refactor: Add EachKey matcher class --- .../Consumer/Matcher/Matchers/EachKey.php | 36 +++++++++++++++++++ .../Consumer/Matcher/Matchers/EachKeyTest.php | 31 ++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/EachKey.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php new file mode 100644 index 00000000..d0b67067 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php @@ -0,0 +1,36 @@ +|object $value + * @param MatcherInterface[] $rules + */ + public function __construct(private object|array $value, private array $rules) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value, + 'rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules), + ]; + } + + public function getType(): string + { + return 'eachKey'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php new file mode 100644 index 00000000..537d554a --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php @@ -0,0 +1,31 @@ + 123, + 'def' => 111, + 'ghi' => [ + 'test' => 'value', + ], + ]; + $rules = [ + new Type('string'), + new Regex('\w{3}'), + ]; + $eachKey = new EachKey($value, $rules); + $this->assertSame( + '{"pact:matcher:type":"eachKey","value":{"abc":123,"def":111,"ghi":{"test":"value"}},"rules":[{"pact:matcher:type":"type","value":"string"},{"pact:matcher:type":"regex","pact:generator:type":"Regex","regex":"\\\\w{3}"}]}', + json_encode($eachKey) + ); + } +} From 4f93b05e30465144921a6998181a5d22b3882f55 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Dec 2023 10:35:13 +0700 Subject: [PATCH 157/298] refactor: Add EachValue matcher class --- .../Consumer/Matcher/Matchers/EachValue.php | 36 +++++++++++++++++++ .../Matcher/Matchers/EachValueTest.php | 29 +++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/EachValue.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php new file mode 100644 index 00000000..1533fba1 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php @@ -0,0 +1,36 @@ +|object $value + * @param MatcherInterface[] $rules + */ + public function __construct(private object|array $value, private array $rules) + { + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'pact:matcher:type' => $this->getType(), + 'value' => $this->value, + 'rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules), + ]; + } + + public function getType(): string + { + return 'eachValue'; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php new file mode 100644 index 00000000..9356292b --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php @@ -0,0 +1,29 @@ +assertSame( + '{"pact:matcher:type":"eachValue","value":["ab1","cd2","ef9"],"rules":[{"pact:matcher:type":"type","value":"string"},{"pact:matcher:type":"regex","pact:generator:type":"Regex","regex":"\\\\w{2}\\\\d"}]}', + json_encode($eachValue) + ); + } +} From a655db34d7131c5f0a1dff7428e76606a9f53db4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 08:52:17 +0700 Subject: [PATCH 158/298] chore: Rename test method --- example/generators/consumer/tests/Service/GeneratorsTest.php | 2 +- example/matchers/consumer/tests/Service/MatchersTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index d0366d98..651a6043 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -21,7 +21,7 @@ public function setUp(): void $this->matcher = new Matcher(); } - public function testGetMatchers() + public function testGetGenerators(): void { $request = new ConsumerRequest(); $request diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 301459ec..40a89a33 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -20,7 +20,7 @@ public function setUp(): void $this->matcher = new Matcher(); } - public function testGetMatchers() + public function testGetMatchers(): void { $request = new ConsumerRequest(); $request From 3937a0d6f17e47ca154f8c351708461bbd075b2b Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 08:58:10 +0700 Subject: [PATCH 159/298] chore: Print received message's metadata --- example/message/consumer/src/ExampleMessageConsumer.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/example/message/consumer/src/ExampleMessageConsumer.php b/example/message/consumer/src/ExampleMessageConsumer.php index 4284d509..d913ff63 100644 --- a/example/message/consumer/src/ExampleMessageConsumer.php +++ b/example/message/consumer/src/ExampleMessageConsumer.php @@ -8,8 +8,12 @@ public function processMessage(string $message): void { $obj = \json_decode($message); print " [x] Processing \n"; - print ' [x] Text: ' . \print_r($obj->contents->text, true) . "\n"; - print ' [x] Number: ' . \print_r($obj->contents->number, true) . "\n"; + print " [x] Contents: \n"; + print ' [x] Text: ' . \print_r($obj->contents->text, true) . "\n"; + print ' [x] Number: ' . \print_r($obj->contents->number, true) . "\n"; + print " [x] Metadata: \n"; + print ' [x] Queue: ' . \print_r($obj->metadata->queue, true) . "\n"; + print ' [x] Routing Key: ' . \print_r($obj->metadata->routing_key, true) . "\n"; print " [x] Processed \n"; } } From 9a72f3c48d1771e5c1c0e72b9c031e6c1caec9b7 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 09:27:47 +0700 Subject: [PATCH 160/298] refactor: Remove unnecessary matchers --- src/PhpPact/Consumer/Matcher/Matcher.php | 48 ++----------------- .../PhpPact/Consumer/Matcher/MatcherTest.php | 42 +++------------- 2 files changed, 9 insertions(+), 81 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index c7853e14..0125b8ef 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -363,7 +363,7 @@ public function uuid(?string $value = null): array * * @throws Exception */ - public function ipv4Address(string $ip = '127.0.0.13'): array + public function ipv4Address(?string $ip = '127.0.0.13'): array { return $this->term($ip, self::IPV4_FORMAT); } @@ -373,7 +373,7 @@ public function ipv4Address(string $ip = '127.0.0.13'): array * * @throws Exception */ - public function ipv6Address(string $ip = '::ffff:192.0.2.128'): array + public function ipv6Address(?string $ip = '::ffff:192.0.2.128'): array { return $this->term($ip, self::IPV6_FORMAT); } @@ -383,53 +383,11 @@ public function ipv6Address(string $ip = '::ffff:192.0.2.128'): array * * @throws Exception */ - public function email(string $email = 'hello@pact.io'): array + public function email(?string $email = 'hello@pact.io'): array { return $this->term($email, self::EMAIL_FORMAT); } - /** - * @return array - * - * @throws Exception - */ - public function ipv4AddressV3(?string $ip = null): array - { - if (null === $ip) { - return $this->term(null, self::IPV4_FORMAT); - } - - return $this->ipv4Address($ip); - } - - /** - * @return array - * - * @throws Exception - */ - public function ipv6AddressV3(?string $ip = null): array - { - if (null === $ip) { - return $this->term(null, self::IPV6_FORMAT); - } - - return $this->ipv6Address($ip); - } - - /** - * @return array - * - * @throws Exception - */ - public function emailV3(?string $email = null): array - { - if (null === $email) { - return $this->term(null, self::EMAIL_FORMAT); - } - - return $this->email($email); - } - /** * Value that must be null. This will only match the JSON Null value. For other content types, it will * match if the attribute is missing. diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 217f73f5..eb205279 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -554,72 +554,42 @@ public function testEmail() /** * @throws Exception */ - public function testIpv4AddressV3() - { - $expected = $this->matcher->ipv4Address(); - $actual = $this->matcher->ipv4AddressV3('127.0.0.13'); - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testIpv6AddressV3() - { - $expected = $this->matcher->ipv6Address(); - $actual = $this->matcher->ipv6AddressV3('::ffff:192.0.2.128'); - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testEmailV3() - { - $expected = $this->matcher->email(); - $actual = $this->matcher->emailV3('hello@pact.io'); - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testRandomIpv4AddressV3() + public function testRandomIpv4Address() { $expected = [ 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', 'pact:matcher:type' => 'regex', 'pact:generator:type' => 'Regex', ]; - $actual = $this->matcher->ipv4AddressV3(); + $actual = $this->matcher->ipv4Address(null); $this->assertEquals($expected, $actual); } /** * @throws Exception */ - public function testRandomIpv6AddressV3() + public function testRandomIpv6Address() { $expected = [ 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', 'pact:matcher:type' => 'regex', 'pact:generator:type' => 'Regex', ]; - $actual = $this->matcher->ipv6AddressV3(); + $actual = $this->matcher->ipv6Address(null); $this->assertEquals($expected, $actual); } /** * @throws Exception */ - public function testRandomEmailV3() + public function testRandomEmail() { $expected = [ 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', 'pact:matcher:type' => 'regex', 'pact:generator:type' => 'Regex', ]; - $actual = $this->matcher->emailV3(); + $actual = $this->matcher->email(null); $this->assertEquals($expected, $actual); } From fb404e693a90eb459a7f28128400c0fdda80c80e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 07:59:59 +0700 Subject: [PATCH 161/298] refactor: Use matcher and generator classes --- .../consumer/tests/Service/GeneratorsTest.php | 2 +- ...generatorsConsumer-generatorsProvider.json | 4 +- .../MatcherNotSupportedException.php | 7 + src/PhpPact/Consumer/Matcher/Matcher.php | 526 +++-------- .../Model/Interaction/HeadersTrait.php | 18 +- .../Consumer/Model/Interaction/PathTrait.php | 7 +- .../Consumer/Model/Interaction/QueryTrait.php | 18 +- .../Model/Interaction/StatusTrait.php | 7 +- src/PhpPact/Consumer/Model/Message.php | 7 +- .../PhpPact/Consumer/Matcher/MatcherTest.php | 842 ++++-------------- .../Consumer/Model/ConsumerRequestTest.php | 2 +- 11 files changed, 342 insertions(+), 1098 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/MatcherNotSupportedException.php diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index 651a6043..d9c76ffe 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -29,7 +29,7 @@ public function testGetGenerators(): void ->setPath('/generators') ->addHeader('Accept', 'application/json') ->setBody([ - 'id' => $this->matcher->fromProviderState($this->matcher->integer(), '${id}') + 'id' => $this->matcher->fromProviderState($this->matcher->integerV3(), '${id}') ]); $response = new ProviderResponse(); diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json index a21a7efe..94be2039 100644 --- a/example/generators/pacts/generatorsConsumer-generatorsProvider.json +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -14,7 +14,7 @@ "request": { "body": { "content": { - "id": 13 + "id": null }, "contentType": "application/json", "encoded": false @@ -41,7 +41,7 @@ "combine": "AND", "matchers": [ { - "match": "type" + "match": "integer" } ] } diff --git a/src/PhpPact/Consumer/Matcher/Exception/MatcherNotSupportedException.php b/src/PhpPact/Consumer/Matcher/Exception/MatcherNotSupportedException.php new file mode 100644 index 00000000..eac908c2 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/MatcherNotSupportedException.php @@ -0,0 +1,7 @@ + */ - public function somethingLike(mixed $value): array + public function somethingLike(mixed $value): Type { return $this->like($value); } /** - * @param mixed $value example of what the expected data would be - * - * @throws Exception - * - * @return array + * This executes a type based match against the values, that is, they are equal if they are the same type. */ - public function like(mixed $value): array + public function like(mixed $value): Type { - return [ - 'value' => $value, - 'pact:matcher:type' => 'type', - ]; + return new Type($value); } /** * Expect an array of similar data as the value passed in. - * - * @return array */ - public function eachLike(mixed $value): array + public function eachLike(mixed $value): MinType { return $this->atLeastLike($value, 1); } @@ -64,28 +78,15 @@ public function eachLike(mixed $value): array /** * @param mixed $value example of what the expected data would be * @param int $min minimum number of objects to verify against - * - * @return array */ - public function atLeastLike(mixed $value, int $min): array + public function atLeastLike(mixed $value, int $min): MinType { - return [ - 'value' => array_fill(0, $min, $value), - 'pact:matcher:type' => 'type', - 'min' => $min, - ]; + return new MinType(array_fill(0, $min, $value), $min); } - /** - * @return array - */ - public function atMostLike(mixed $value, int $max): array + public function atMostLike(mixed $value, int $max): MaxType { - return [ - 'value' => [$value], - 'pact:matcher:type' => 'type', - 'max' => $max, - ]; + return new MaxType([$value], $max); } /** @@ -94,68 +95,38 @@ public function atMostLike(mixed $value, int $max): array * @param int $max maximum number of objects to verify against * @param int|null $count number of examples to generate, defaults to one * - * @return array + * @throws MatcherException */ - public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $count = null): array + public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $count = null): MinMaxType { $elements = $count ?? $min; if ($count !== null) { if ($count < $min) { - throw new Exception( + throw new MatcherException( "constrainedArrayLike has a minimum of {$min} but {$count} elements where requested." . ' Make sure the count is greater than or equal to the min.' ); } elseif ($count > $max) { - throw new Exception( + throw new MatcherException( "constrainedArrayLike has a maximum of {$max} but {$count} elements where requested." . ' Make sure the count is less than or equal to the max.' ); } } - return [ - 'min' => $min, - 'max' => $max, - 'pact:matcher:type' => 'type', - 'value' => array_fill(0, $elements, $value), - ]; + return new MinMaxType(array_fill(0, $elements, $value), $min, $max); } /** - * Validate that a value will match a regex pattern. + * Validate that values will match a regex pattern. * - * @param string|string[]|null $values example of what the expected data would be - * @param string $pattern valid Ruby regex pattern - * - * @return array + * @param string|string[]|null $values * - * @throws Exception + * @throws MatcherException */ - public function term(string|array|null $values, string $pattern): array - { - if (null === $values) { - return [ - 'regex' => $pattern, - 'pact:matcher:type' => 'regex', - 'pact:generator:type' => 'Regex', - ]; - } - - foreach ((array) $values as $value) { - $result = preg_match("/$pattern/", $value); - - if ($result === false || $result === 0) { - $errorCode = preg_last_error(); - - throw new Exception("The pattern {$pattern} is not valid for value {$value}. Failed with error code {$errorCode}."); - } - } - - return [ - 'value' => $values, - 'regex' => $pattern, - 'pact:matcher:type' => 'regex', - ]; + public function term(string|array|null $values, string $pattern): Regex + { + return new Regex($pattern, $values); } /** @@ -163,11 +134,9 @@ public function term(string|array|null $values, string $pattern): array * * @param string|string[]|null $values * - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function regex(string|array|null $values, string $pattern): array + public function regex(string|array|null $values, string $pattern): Regex { return $this->term($values, $pattern); } @@ -177,11 +146,9 @@ public function regex(string|array|null $values, string $pattern): array * * @param string $value valid ISO8601 date, example: 2010-01-01 * - * @throws Exception - * - * @return array + * @throws MatcherException */ - public function dateISO8601(string $value = '2013-02-01'): array + public function dateISO8601(string $value = '2013-02-01'): Regex { return $this->term($value, self::ISO8601_DATE_FORMAT); } @@ -191,11 +158,9 @@ public function dateISO8601(string $value = '2013-02-01'): array * * @param string $value * - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function timeISO8601(string $value = 'T22:44:30.652Z'): array + public function timeISO8601(string $value = 'T22:44:30.652Z'): Regex { return $this->term($value, self::ISO8601_TIME_FORMAT); } @@ -205,11 +170,9 @@ public function timeISO8601(string $value = 'T22:44:30.652Z'): array * * @param string $value * - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): array + public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): Regex { return $this->term($value, self::ISO8601_DATETIME_FORMAT); } @@ -219,11 +182,9 @@ public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): ar * * @param string $value * - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.123+01:00'): array + public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.123+01:00'): Regex { return $this->term($value, self::ISO8601_DATETIME_WITH_MILLIS_FORMAT); } @@ -233,157 +194,82 @@ public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.1 * * @param string $value * - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function timestampRFC3339(string $value = 'Mon, 31 Oct 2016 15:21:41 -0400'): array + public function timestampRFC3339(string $value = 'Mon, 31 Oct 2016 15:21:41 -0400'): Regex { return $this->term($value, self::RFC3339_TIMESTAMP_FORMAT); } - /** - * @return array - * - * @throws Exception - */ - public function boolean(): array + public function boolean(): Type { return $this->like(true); } - /** - * @return array - * - * @throws Exception - */ - public function integer(int $int = 13): array + public function integer(int $int = 13): Type { return $this->like($int); } - /** - * @return array - * - * @throws Exception - */ - public function decimal(float $float = 13.01): array + public function decimal(float $float = 13.01): Type { return $this->like($float); } - /** - * @return array - */ - public function booleanV3(?bool $value = null): array + public function booleanV3(?bool $value = null): Boolean { - if (null === $value) { - return [ - 'pact:generator:type' => 'RandomBoolean', - 'pact:matcher:type' => 'boolean', - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'boolean', - ]; + return new Boolean($value); } - /** - * @return array - */ - public function integerV3(?int $value = null): array + public function integerV3(?int $value = null): Integer { - if (null === $value) { - return [ - 'pact:generator:type' => 'RandomInt', - 'pact:matcher:type' => 'integer', - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'integer', - ]; + return new Integer($value); } - /** - * @return array - */ - public function decimalV3(?float $value = null): array + public function decimalV3(?float $value = null): Decimal { - if (null === $value) { - return [ - 'pact:generator:type' => 'RandomDecimal', - 'pact:matcher:type' => 'decimal', - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'decimal', - ]; + return new Decimal($value); } /** - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function hexadecimal(?string $value = null): array + public function hexadecimal(?string $value = null): Regex { + $matcher = new Regex(self::HEX_FORMAT, $value); + if (null === $value) { - return [ - 'pact:generator:type' => 'RandomHexadecimal', - ] + $this->term(null, self::HEX_FORMAT); + $matcher->setGenerator(new RandomHexadecimal()); } - return $this->term($value, self::HEX_FORMAT); + return $matcher; } /** - * @return array - * - * @throws Exception + * @throws MatcherException */ - public function uuid(?string $value = null): array + public function uuid(?string $value = null): Regex { + $matcher = new Regex(self::UUID_V4_FORMAT, $value); + if (null === $value) { - return [ - 'pact:generator:type' => 'Uuid', - ] + $this->term(null, self::UUID_V4_FORMAT); + $matcher->setGenerator(new Uuid()); } - return $this->term($value, self::UUID_V4_FORMAT); + return $matcher; } - /** - * @return array - * - * @throws Exception - */ - public function ipv4Address(?string $ip = '127.0.0.13'): array + public function ipv4Address(?string $ip = '127.0.0.13'): Regex { return $this->term($ip, self::IPV4_FORMAT); } - /** - * @return array - * - * @throws Exception - */ - public function ipv6Address(?string $ip = '::ffff:192.0.2.128'): array + public function ipv6Address(?string $ip = '::ffff:192.0.2.128'): Regex { return $this->term($ip, self::IPV6_FORMAT); } - /** - * @return array - * - * @throws Exception - */ - public function email(?string $email = 'hello@pact.io'): array + public function email(?string $email = 'hello@pact.io'): Regex { return $this->term($email, self::EMAIL_FORMAT); } @@ -391,149 +277,93 @@ public function email(?string $email = 'hello@pact.io'): array /** * Value that must be null. This will only match the JSON Null value. For other content types, it will * match if the attribute is missing. - * - * @return array */ - public function nullValue(): array + public function nullValue(): NullValue { - return [ - 'pact:matcher:type' => 'null', - ]; + return new NullValue(); } /** - * @return array + * Matches the string representation of a value against the date format. + * + * NOTE: Java's datetime format is used, not PHP's datetime format + * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns + * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function date(string $format = 'yyyy-MM-dd', ?string $value = null): array + public function date(string $format = 'yyyy-MM-dd', ?string $value = null): Date { - if (null === $value) { - return [ - 'pact:generator:type' => 'Date', - 'pact:matcher:type' => 'date', - 'format' => $format, - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'date', - 'format' => $format, - ]; + return new Date($format, $value); } /** - * @return array + * Matches the string representation of a value against the time format. + * + * NOTE: Java's datetime format is used, not PHP's datetime format + * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns + * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function time(string $format = 'HH:mm:ss', ?string $value = null): array + public function time(string $format = 'HH:mm:ss', ?string $value = null): Time { - if (null === $value) { - return [ - 'pact:generator:type' => 'Time', - 'pact:matcher:type' => 'time', - 'format' => $format, - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'time', - 'format' => $format, - ]; + return new Time($format, $value); } /** - * @return array + * Matches the string representation of a value against the datetime format. + * + * NOTE: Java's datetime format is used, not PHP's datetime format + * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns + * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function datetime(string $format = "yyyy-MM-dd'T'HH:mm:ss", ?string $value = null): array + public function datetime(string $format = "yyyy-MM-dd'T'HH:mm:ss", ?string $value = null): DateTime { - if (null === $value) { - return [ - 'pact:generator:type' => 'DateTime', - 'pact:matcher:type' => 'datetime', - 'format' => $format, - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'datetime', - 'format' => $format, - ]; + return new DateTime($format, $value); } - /** - * @return array - */ - public function string(?string $value = null): array + public function string(?string $value = null): StringValue { - if (null === $value) { - return [ - 'pact:generator:type' => 'RandomString', - ] + $this->like('some string'); // No matcher for string? - } - - return $this->like($value); // No matcher for string? + return new StringValue($value); } /** - * @param array $macher + * Generates a value that is looked up from the provider state context using the given expression * - * @return array + * @throws MatcherNotSupportedException */ - public function fromProviderState(array $macher, string $expression): array + public function fromProviderState(MatcherInterface $matcher, string $expression): MatcherInterface { - return $macher + [ - 'pact:generator:type' => 'ProviderState', - 'expression' => $expression, - ]; + if (!$matcher instanceof GeneratorAwareInterface) { + throw new MatcherNotSupportedException(sprintf("Matcher '%s' must be generator aware", $matcher->getType())); + } + + $matcher->setGenerator(new ProviderState($expression)); + + return $matcher; } /** * Value that must be equal to the example. This is mainly used to reset the matching rules which cascade. - * - * @return array */ - public function equal(mixed $value): array + public function equal(mixed $value): Equality { - return [ - 'pact:matcher:type' => 'equality', - 'value' => $value, - ]; + return new Equality($value); } /** * Value that must include the example value as a substring. - * - * @return array */ - public function includes(string $value): array + public function includes(string $value): Includes { - return [ - 'pact:matcher:type' => 'include', - 'value' => $value, - ]; + return new Includes($value); } /** * Value must be a number * * @param int|float|null $value Example value. If omitted a random integer value will be generated. - * - * @return array */ - public function number(int|float|null $value = null): array + public function number(int|float|null $value = null): Number { - if (null === $value) { - return [ - 'pact:generator:type' => 'RandomInt', - 'pact:matcher:type' => 'number', - ]; - } - - return [ - 'value' => $value, - 'pact:matcher:type' => 'number', - ]; + return new Number($value); } /** @@ -541,108 +371,52 @@ public function number(int|float|null $value = null): array * occurs once in the array. Variants may be objects containing matching rules. * * @param array $variants - * - * @return array */ - public function arrayContaining(array $variants): array + public function arrayContaining(array $variants): ArrayContains { - return [ - 'pact:matcher:type' => 'arrayContains', - 'variants' => array_values($variants), - ]; + return new ArrayContains($variants); } /** * Value must be present and not empty (not null or the empty string or empty array or empty object) - * - * @return array */ - public function notEmpty(mixed $value): array + public function notEmpty(mixed $value): NotEmpty { - return [ - 'value' => $value, - 'pact:matcher:type' => 'notEmpty', - ]; + return new NotEmpty($value); } /** * Value must be valid based on the semver specification - * - * @return array */ - public function semver(string $value): array + public function semver(string $value): Semver { - return [ - 'value' => $value, - 'pact:matcher:type' => 'semver', - ]; + return new Semver($value); } /** * Matches the response status code. - * - * @return array */ - public function statusCode(string $status, ?int $value = null): array + public function statusCode(string $status, ?int $value = null): StatusCode { - if (!in_array($status, HttpStatus::all())) { - throw new Exception(sprintf("Status '%s' is not supported. Supported status are: %s", $status, implode(', ', HttpStatus::all()))); - } - - if (null === $value) { - [$min, $max] = match($status) { - HttpStatus::INFORMATION => [100, 199], - HttpStatus::SUCCESS => [200, 299], - HttpStatus::REDIRECT => [300, 399], - HttpStatus::CLIENT_ERROR => [400, 499], - HttpStatus::SERVER_ERROR => [500, 599], - HttpStatus::NON_ERROR => [100, 399], - HttpStatus::ERROR => [400, 599], - default => [100, 199], // Can't happen, just to make PHPStan happy - }; - - return [ - 'pact:generator:type' => 'RandomInt', - 'min' => $min, - 'max' => $max, - 'status' => $status, - 'pact:matcher:type' => 'statusCode', - ]; - } - - return [ - 'value' => $value, - 'status' => $status, - 'pact:matcher:type' => 'statusCode', - ]; + return new StatusCode($status, $value); } /** * Match the values in a map, ignoring the keys * * @param array $values - * - * @return array */ - public function values(array $values): array + public function values(array $values): Values { - return [ - 'value' => $values, - 'pact:matcher:type' => 'values', - ]; + return new Values($values); } /** * Match binary data by its content type (magic file check) - * - * @return array */ - public function contentType(string $contentType): array + public function contentType(string $contentType): ContentType { - return [ - 'value' => $contentType, - 'pact:matcher:type' => 'contentType', - ]; + return new ContentType($contentType); } /** @@ -650,16 +424,10 @@ public function contentType(string $contentType): array * * @param array $values * @param array $rules - * - * @return array */ - public function eachKey(array $values, array $rules): array + public function eachKey(array $values, array $rules): EachKey { - return [ - 'rules' => $rules, - 'value' => $values, - 'pact:matcher:type' => 'eachKey', - ]; + return new EachKey($values, $rules); } /** @@ -667,15 +435,9 @@ public function eachKey(array $values, array $rules): array * * @param array $values * @param array $rules - * - * @return array */ - public function eachValue(array $values, array $rules): array + public function eachValue(array $values, array $rules): EachValue { - return [ - 'rules' => $rules, - 'value' => $values, - 'pact:matcher:type' => 'eachValue', - ]; + return new EachValue($values, $rules); } } diff --git a/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php index ebfa0486..9cc6f3ae 100644 --- a/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php @@ -2,6 +2,9 @@ namespace PhpPact\Consumer\Model\Interaction; +use JsonException; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; + trait HeadersTrait { /** @@ -31,13 +34,15 @@ public function setHeaders(array $headers): self } /** - * @param string[]|string $value + * @param MatcherInterface|MatcherInterface[]|string[]|string $value + * + * @throws JsonException */ - public function addHeader(string $header, array|string $value): self + public function addHeader(string $header, array|string|MatcherInterface $value): self { $this->headers[$header] = []; if (is_array($value)) { - array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); + array_walk($value, fn (string|MatcherInterface $value) => $this->addHeaderValue($header, $value)); } else { $this->addHeaderValue($header, $value); } @@ -45,8 +50,11 @@ public function addHeader(string $header, array|string $value): self return $this; } - private function addHeaderValue(string $header, string $value): void + /** + * @throws JsonException + */ + private function addHeaderValue(string $header, string|MatcherInterface $value): void { - $this->headers[$header][] = $value; + $this->headers[$header][] = is_string($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); } } diff --git a/src/PhpPact/Consumer/Model/Interaction/PathTrait.php b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php index 9eceebe2..52126078 100644 --- a/src/PhpPact/Consumer/Model/Interaction/PathTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php @@ -3,6 +3,7 @@ namespace PhpPact\Consumer\Model\Interaction; use JsonException; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; trait PathTrait { @@ -14,13 +15,11 @@ public function getPath(): string } /** - * @param string|array $path - * * @throws JsonException */ - public function setPath(array|string $path): self + public function setPath(MatcherInterface|string $path): self { - $this->path = is_array($path) ? json_encode($path, JSON_THROW_ON_ERROR) : $path; + $this->path = is_string($path) ? $path : json_encode($path, JSON_THROW_ON_ERROR); return $this; } diff --git a/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php index 5d907857..a1184855 100644 --- a/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php @@ -2,6 +2,9 @@ namespace PhpPact\Consumer\Model\Interaction; +use JsonException; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; + trait QueryTrait { /** @@ -31,13 +34,15 @@ public function setQuery(array $query): self } /** - * @param string|string[] $value + * @param MatcherInterface|MatcherInterface[]|string|string[] $value + * + * @throws JsonException */ - public function addQueryParameter(string $key, array|string $value): self + public function addQueryParameter(string $key, array|string|MatcherInterface $value): self { $this->query[$key] = []; if (is_array($value)) { - array_walk($value, fn (string $value) => $this->addQueryParameterValue($key, $value)); + array_walk($value, fn (string|MatcherInterface $value) => $this->addQueryParameterValue($key, $value)); } else { $this->addQueryParameterValue($key, $value); } @@ -45,8 +50,11 @@ public function addQueryParameter(string $key, array|string $value): self return $this; } - private function addQueryParameterValue(string $key, string $value): void + /** + * @throws JsonException + */ + private function addQueryParameterValue(string $key, string|MatcherInterface $value): void { - $this->query[$key][] = $value; + $this->query[$key][] = is_string($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); } } diff --git a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php index b6180f7b..53a95674 100644 --- a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php @@ -3,6 +3,7 @@ namespace PhpPact\Consumer\Model\Interaction; use JsonException; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; trait StatusTrait { @@ -14,13 +15,11 @@ public function getStatus(): string } /** - * @param int|array $status - * * @throws JsonException */ - public function setStatus(int|array $status): self + public function setStatus(int|MatcherInterface $status): self { - $this->status = is_array($status) ? json_encode($status, JSON_THROW_ON_ERROR) : (string) $status; + $this->status = is_int($status) ? (string) $status : json_encode($status, JSON_THROW_ON_ERROR); return $this; } diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 7cdb8d84..4ee117d8 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -4,6 +4,7 @@ use JsonException; use PhpPact\Consumer\Exception\BodyNotSupportedException; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; @@ -45,7 +46,7 @@ public function getMetadata(): array } /** - * @param array> $metadata + * @param array $metadata */ public function setMetadata(array $metadata): self { @@ -58,11 +59,9 @@ public function setMetadata(array $metadata): self } /** - * @param string|array $value - * * @throws JsonException */ - private function setMetadataValue(string $key, string|array $value): void + private function setMetadataValue(string $key, string|MatcherInterface $value): void { $this->metadata[$key] = is_string($value) ? $value : json_encode($value, JSON_THROW_ON_ERROR); } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index eb205279..b20e7d0d 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -2,9 +2,37 @@ namespace PhpPactTest\Consumer\Matcher; -use Exception; +use PhpPact\Consumer\Matcher\Exception\MatcherException; +use PhpPact\Consumer\Matcher\Exception\MatcherNotSupportedException; +use PhpPact\Consumer\Matcher\Generators\ProviderState; +use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; +use PhpPact\Consumer\Matcher\Generators\Uuid; use PhpPact\Consumer\Matcher\HttpStatus; use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Matcher\Matchers\ArrayContains; +use PhpPact\Consumer\Matcher\Matchers\Boolean; +use PhpPact\Consumer\Matcher\Matchers\ContentType; +use PhpPact\Consumer\Matcher\Matchers\Date; +use PhpPact\Consumer\Matcher\Matchers\DateTime; +use PhpPact\Consumer\Matcher\Matchers\Decimal; +use PhpPact\Consumer\Matcher\Matchers\EachKey; +use PhpPact\Consumer\Matcher\Matchers\EachValue; +use PhpPact\Consumer\Matcher\Matchers\Equality; +use PhpPact\Consumer\Matcher\Matchers\Includes; +use PhpPact\Consumer\Matcher\Matchers\Integer; +use PhpPact\Consumer\Matcher\Matchers\MaxType; +use PhpPact\Consumer\Matcher\Matchers\MinMaxType; +use PhpPact\Consumer\Matcher\Matchers\MinType; +use PhpPact\Consumer\Matcher\Matchers\NotEmpty; +use PhpPact\Consumer\Matcher\Matchers\NullValue; +use PhpPact\Consumer\Matcher\Matchers\Number; +use PhpPact\Consumer\Matcher\Matchers\Regex; +use PhpPact\Consumer\Matcher\Matchers\Semver; +use PhpPact\Consumer\Matcher\Matchers\StatusCode; +use PhpPact\Consumer\Matcher\Matchers\StringValue; +use PhpPact\Consumer\Matcher\Matchers\Time; +use PhpPact\Consumer\Matcher\Matchers\Type; +use PhpPact\Consumer\Matcher\Matchers\Values; use PHPUnit\Framework\TestCase; class MatcherTest extends TestCase @@ -16,249 +44,79 @@ protected function setUp(): void $this->matcher = new Matcher(); } - /** - * @throws Exception - */ - public function testLikeNull(): void + public function testSomethingLike(): void { - $json = \json_encode($this->matcher->like(null)); - - $this->assertEquals('{"value":null,"pact:matcher:type":"type"}', $json); + $this->assertInstanceOf(Type::class, $this->matcher->somethingLike(123)); } - /** - * @throws Exception - */ - public function testLike() + public function testLike(): void { - $json = \json_encode($this->matcher->like(12)); - - $this->assertEquals('{"value":12,"pact:matcher:type":"type"}', $json); + $this->assertInstanceOf(Type::class, $this->matcher->like('abc')); } - /** - * @dataProvider dataProviderForEachLikeTest - */ - public function testEachLike(object|array $value) + public function testEachLike(): void { - $expected = \json_encode([ - 'value' => [ - [ - 'value1' => [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ], - 'value2' => 2, - ], - ], - 'pact:matcher:type' => 'type', - 'min' => 1, - ]); - - $actual = \json_encode($this->matcher->eachLike($value)); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(MinType::class, $this->matcher->eachLike('test')); } - public function dataProviderForEachLikeTest() + public function testAtLeastLike(): void { - $value1Matcher = [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ]; - - $object = new \stdClass(); - $object->value1 = $value1Matcher; - $object->value2 = 2; - - $array = [ - 'value1' => $value1Matcher, - 'value2' => 2, - ]; - - return [ - [$object], - [$array], - ]; - } - - /** - * @dataProvider dataProviderForEachLikeTest - */ - public function testAtLeastLike(object|array $value) - { - $eachValueMatcher = [ - 'value1' => [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ], - 'value2' => 2, - ]; - $expected = \json_encode([ - 'value' => [ - $eachValueMatcher, - $eachValueMatcher, - ], - 'pact:matcher:type' => 'type', - 'min' => 2, - ]); - - $actual = \json_encode($this->matcher->atLeastLike($value, 2)); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(MinType::class, $this->matcher->atLeastLike('test', 2)); } - /** - * @dataProvider dataProviderForEachLikeTest - */ - public function testAtMostLike(object|array $value) + public function testAtMostLike(): void { - $expected = \json_encode([ - 'value' => [ - [ - 'value1' => [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ], - 'value2' => 2, - ], - ], - 'pact:matcher:type' => 'type', - 'max' => 2, - ]); - - $actual = \json_encode($this->matcher->atMostLike($value, 2)); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(MaxType::class, $this->matcher->atMostLike('test', 2)); } - /** - * @throws Exception - */ - public function testConstrainedArrayLikeCountLessThanMin() + public function testConstrainedArrayLikeCountLessThanMin(): void { - $this->expectException(Exception::class); + $this->expectException(MatcherException::class); $this->expectExceptionMessage('constrainedArrayLike has a minimum of 2 but 1 elements where requested.' . ' Make sure the count is greater than or equal to the min.'); $this->matcher->constrainedArrayLike('text', 2, 4, 1); } - /** - * @throws Exception - */ - public function testConstrainedArrayLikeCountLargerThanMax() + public function testConstrainedArrayLikeCountLargerThanMax(): void { - $this->expectException(Exception::class); + $this->expectException(MatcherException::class); $this->expectExceptionMessage('constrainedArrayLike has a maximum of 5 but 7 elements where requested.' . ' Make sure the count is less than or equal to the max.'); $this->matcher->constrainedArrayLike('text', 3, 5, 7); } - /** - * @dataProvider dataProviderForEachLikeTest - */ - public function testConstrainedArrayLike(object|array $value) - { - $eachValueMatcher = [ - 'value1' => [ - 'value' => 1, - 'pact:matcher:type' => 'type', - ], - 'value2' => 2, - ]; - $expected = \json_encode([ - 'min' => 2, - 'max' => 4, - 'pact:matcher:type' => 'type', - 'value' => [ - $eachValueMatcher, - $eachValueMatcher, - $eachValueMatcher, - ], - ]); - - $actual = \json_encode($this->matcher->constrainedArrayLike($value, 2, 4, 3)); - - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testRegexNoMatch() + public function testConstrainedArrayLike(): void { - $this->expectException(Exception::class); - $this->expectExceptionMessage('The pattern BadPattern is not valid for value SomeWord. Failed with error code 0.'); - $this->matcher->regex('SomeWord', 'BadPattern'); + $this->assertInstanceOf(MinMaxType::class, $this->matcher->constrainedArrayLike('test', 2, 4, 3)); } - /** - * @throws Exception - */ - public function testRegex() + public function testTerm(): void { - $expected = [ - 'value' => 'Games', - 'regex' => 'Games|Other', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->regex('Games', 'Games|Other'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->term('123', '\d+')); } - /** - * @throws Exception - */ - public function testRegexMultiValues() + public function testRegex(): void { - $expected = [ - 'value' => [1, 23], - 'regex' => '\d+', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->regex([1, 23], '\d+'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->regex('Games', 'Games|Other')); } - /** - * @throws Exception - */ - public function testDateISO8601() + public function testDateISO8601(): void { - $expected = [ - 'value' => '2010-01-17', - 'regex' => '^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->dateISO8601('2010-01-17'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->dateISO8601('2010-01-17')); } /** * @dataProvider dataProviderForTimeTest - * - * @throws Exception */ - public function testTimeISO8601($time) + public function testTimeISO8601(string $time): void { - $expected = [ - 'value' => $time, - 'regex' => '^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->timeISO8601($time); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->timeISO8601($time)); } - public function dataProviderForTimeTest() + /** + * @return string[] + */ + public function dataProviderForTimeTest(): array { return [ ['T22:44:30.652Z'], @@ -276,23 +134,16 @@ public function dataProviderForTimeTest() /** * @dataProvider dataProviderForDateTimeTest - * - * @throws Exception */ - public function testDateTimeISO8601($dateTime) + public function testDateTimeISO8601(string $dateTime): void { - $expected = [ - 'value' => $dateTime, - 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->dateTimeISO8601($dateTime); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->dateTimeISO8601($dateTime)); } - public function dataProviderForDateTimeTest() + /** + * @return string[] + */ + public function dataProviderForDateTimeTest(): array { return [ ['2015-08-06T16:53:10+01:00'], @@ -308,23 +159,16 @@ public function dataProviderForDateTimeTest() /** * @dataProvider dataProviderForDateTimeWithMillisTest - * - * @throws Exception */ - public function testDateTimeWithMillisISO8601($dateTime) + public function testDateTimeWithMillisISO8601(string $dateTime): void { - $expected = [ - 'value' => $dateTime, - 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->dateTimeWithMillisISO8601($dateTime); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->dateTimeWithMillisISO8601($dateTime)); } - public function dataProviderForDateTimeWithMillisTest() + /** + * @return string[] + */ + public function dataProviderForDateTimeWithMillisTest(): array { return [ ['2015-08-06T16:53:10.123+01:00'], @@ -338,546 +182,178 @@ public function dataProviderForDateTimeWithMillisTest() ]; } - /** - * @throws Exception - */ - public function testTimestampRFC3339() + public function testTimestampRFC3339(): void { - $expected = [ - 'value' => 'Mon, 31 Oct 2016 15:21:41 -0400', - 'regex' => '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$', - 'pact:matcher:type' => 'regex', - ]; - - $actual = $this->matcher->timestampRFC3339('Mon, 31 Oct 2016 15:21:41 -0400'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->timestampRFC3339('Mon, 31 Oct 2016 15:21:41 -0400')); } - /** - * @throws Exception - */ - public function testInteger() + public function testInteger(): void { - $json = \json_encode($this->matcher->integer()); - - $this->assertEquals('{"value":13,"pact:matcher:type":"type"}', $json); + $this->assertInstanceOf(Type::class, $this->matcher->integer()); } - /** - * @throws Exception - */ - public function testBoolean() + public function testBoolean(): void { - $json = \json_encode($this->matcher->boolean()); - - $this->assertEquals('{"value":true,"pact:matcher:type":"type"}', $json); + $this->assertInstanceOf(Type::class, $this->matcher->boolean()); } - /** - * @throws Exception - */ - public function testDecimal() + public function testDecimal(): void { - $json = \json_encode($this->matcher->decimal()); - - $this->assertEquals('{"value":13.01,"pact:matcher:type":"type"}', $json); + $this->assertInstanceOf(Type::class, $this->matcher->decimal()); } - public function testIntegerV3() + public function testIntegerV3(): void { - $expected = [ - 'value' => 13, - 'pact:matcher:type' => 'integer', - ]; - $actual = $this->matcher->integerV3(13); - - $this->assertEquals($expected, $actual); - } - - public function testRandomIntegerV3() - { - $expected = [ - 'pact:generator:type' => 'RandomInt', - 'pact:matcher:type' => 'integer', - ]; - $actual = $this->matcher->integerV3(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Integer::class, $this->matcher->integerV3(13)); } - public function testBooleanV3() + public function testBooleanV3(): void { - $expected = [ - 'value' => true, - 'pact:matcher:type' => 'boolean', - ]; - $actual = $this->matcher->booleanV3(true); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Boolean::class, $this->matcher->booleanV3(true)); } - public function testRandomBooleanV3() + public function testDecimalV3(): void { - $expected = [ - 'pact:generator:type' => 'RandomBoolean', - 'pact:matcher:type' => 'boolean', - ]; - $actual = $this->matcher->booleanV3(); - - $this->assertEquals($expected, $actual); - } - - public function testDecimalV3() - { - $expected = [ - 'value' => 13.01, - 'pact:matcher:type' => 'decimal', - ]; - $actual = $this->matcher->decimalV3(13.01); - - $this->assertEquals($expected, $actual); - } - - public function testRandomDecimalV3() - { - $expected = [ - 'pact:generator:type' => 'RandomDecimal', - 'pact:matcher:type' => 'decimal', - ]; - $actual = $this->matcher->decimalV3(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Decimal::class, $this->matcher->decimalV3(13.01)); } /** - * @throws Exception + * @testWith [null, true] + * ["3F", false] */ - public function testHexadecimal() + public function testHexadecimal(?string $value, bool $hasGenerator): void { - $expected = [ - 'value' => '3F', - 'regex' => '^[0-9a-fA-F]+$', - 'pact:matcher:type' => 'regex', - ]; - $actual = $this->matcher->hexadecimal('3F'); - - $this->assertEquals($expected, $actual); + $hexadecimal = $this->matcher->hexadecimal($value); + $this->assertInstanceOf(Regex::class, $hexadecimal); + if ($hasGenerator) { + $this->assertSame(RandomHexadecimal::class, get_class($hexadecimal->getGenerator())); + } else { + $this->assertNull($hexadecimal->getGenerator()); + } } /** - * @throws Exception + * @testWith [null, true] + * ["ce118b6e-d8e1-11e7-9296-cec278b6b50a", false] */ - public function testRandomHexadecimal() + public function testUuid(?string $value, bool $hasGenerator): void { - $expected = [ - 'regex' => '^[0-9a-fA-F]+$', - 'pact:matcher:type' => 'regex', - 'pact:generator:type' => 'RandomHexadecimal', - ]; - $actual = $this->matcher->hexadecimal(); - - $this->assertEquals($expected, $actual); + $uuid = $this->matcher->uuid($value); + $this->assertInstanceOf(Regex::class, $uuid); + if ($hasGenerator) { + $this->assertSame(Uuid::class, get_class($uuid->getGenerator())); + } else { + $this->assertNull($uuid->getGenerator()); + } } - /** - * @throws Exception - */ - public function testUuid() - { - $expected = [ - 'value' => 'ce118b6e-d8e1-11e7-9296-cec278b6b50a', - 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', - 'pact:matcher:type' => 'regex', - ]; - $actual = $this->matcher->uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a'); - - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testRandomUuid() - { - $expected = [ - 'pact:generator:type' => 'Uuid', - 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', - 'pact:matcher:type' => 'regex', - ]; - $actual = $this->matcher->uuid(); - - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testIpv4Address() - { - $expected = [ - 'value' => '127.0.0.13', - 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', - 'pact:matcher:type' => 'regex', - ]; - - $this->assertEquals($expected, $this->matcher->ipv4Address()); - } - - /** - * @throws Exception - */ - public function testIpv6Address() - { - $expected = [ - 'value' => '::ffff:192.0.2.128', - 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', - 'pact:matcher:type' => 'regex', - ]; - - $this->assertEquals($expected, $this->matcher->ipv6Address()); - } - - /** - * @throws Exception - */ - public function testEmail() - { - $expected = [ - 'value' => 'hello@pact.io', - 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', - 'pact:matcher:type' => 'regex', - ]; - $this->assertEquals($expected, $this->matcher->email()); - } - - /** - * @throws Exception - */ - public function testRandomIpv4Address() - { - $expected = [ - 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', - 'pact:matcher:type' => 'regex', - 'pact:generator:type' => 'Regex', - ]; - $actual = $this->matcher->ipv4Address(null); - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testRandomIpv6Address() - { - $expected = [ - 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', - 'pact:matcher:type' => 'regex', - 'pact:generator:type' => 'Regex', - ]; - $actual = $this->matcher->ipv6Address(null); - $this->assertEquals($expected, $actual); - } - - /** - * @throws Exception - */ - public function testRandomEmail() - { - $expected = [ - 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', - 'pact:matcher:type' => 'regex', - 'pact:generator:type' => 'Regex', - ]; - $actual = $this->matcher->email(null); - $this->assertEquals($expected, $actual); - } - - public function testNullValue() - { - $expected = [ - 'pact:matcher:type' => 'null', - ]; - $actual = $this->matcher->nullValue(); - $this->assertEquals($expected, $actual); - } - - public function testDate() + public function testIpv4Address(): void { - $expected = [ - 'value' => '2022-11-21', - 'pact:matcher:type' => 'date', - 'format' => 'yyyy-MM-dd', - ]; - $actual = $this->matcher->date('yyyy-MM-dd', '2022-11-21'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->ipv4Address()); } - public function testRandomDate() + public function testIpv6Address(): void { - $expected = [ - 'pact:generator:type' => 'Date', - 'pact:matcher:type' => 'date', - 'format' => 'yyyy-MM-dd', - ]; - $actual = $this->matcher->date(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->ipv6Address()); } - public function testTime() + public function testEmail(): void { - $expected = [ - 'value' => '21:45::31', - 'pact:matcher:type' => 'time', - 'format' => 'HH:mm:ss', - ]; - $actual = $this->matcher->time('HH:mm:ss', '21:45::31'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Regex::class, $this->matcher->email()); } - public function testRandomTime() + public function testNullValue(): void { - $expected = [ - 'pact:generator:type' => 'Time', - 'pact:matcher:type' => 'time', - 'format' => 'HH:mm:ss', - ]; - $actual = $this->matcher->time(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(NullValue::class, $this->matcher->nullValue()); } - public function testDateTime() + public function testDate(): void { - $expected = [ - 'value' => '2015-08-06T16:53:10', - 'pact:matcher:type' => 'datetime', - 'format' => "yyyy-MM-dd'T'HH:mm:ss", - ]; - $actual = $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", '2015-08-06T16:53:10'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Date::class, $this->matcher->date('yyyy-MM-dd', '2022-11-21')); } - public function testRandomDateTime() + public function testTime(): void { - $expected = [ - 'pact:generator:type' => 'DateTime', - 'pact:matcher:type' => 'datetime', - 'format' => "yyyy-MM-dd'T'HH:mm:ss", - ]; - $actual = $this->matcher->datetime(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Time::class, $this->matcher->time('HH:mm:ss', '21:45::31')); } - public function testString() + public function testDateTime(): void { - $expected = [ - 'pact:matcher:type' => 'type', - 'value' => 'test string', - ]; - $actual = $this->matcher->string('test string'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(DateTime::class, $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", '2015-08-06T16:53:10')); } - public function testRandomString() + public function testString(): void { - $expected = [ - 'pact:generator:type' => 'RandomString', - 'pact:matcher:type' => 'type', - 'value' => 'some string', - ]; - $actual = $this->matcher->string(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(StringValue::class, $this->matcher->string('test string')); } - public function testFromProviderState() + public function testFromProviderStateMatcherNotSupport(): void { - $expected = [ - 'regex' => Matcher::UUID_V4_FORMAT, - 'pact:matcher:type' => 'regex', - 'value' => 'f2392c53-6e55-48f7-8e08-18e4bf99c795', - 'pact:generator:type' => 'ProviderState', - 'expression' => '${id}', - ]; - $actual = $this->matcher->fromProviderState($this->matcher->uuid('f2392c53-6e55-48f7-8e08-18e4bf99c795'), '${id}'); - - $this->assertEquals($expected, $actual); + $this->expectException(MatcherNotSupportedException::class); + $this->expectExceptionMessage("Matcher 'type' must be generator aware"); + $this->matcher->fromProviderState(new Type('text'), '${text}'); } - public function testEqual() + public function testFromProviderState(): void { - $expected = [ - 'pact:matcher:type' => 'equality', - 'value' => 'test string', - ]; - $actual = $this->matcher->equal('test string'); - - $this->assertEquals($expected, $actual); + $uuid = $this->matcher->uuid(); + $this->assertSame(Uuid::class, get_class($uuid->getGenerator())); + $this->assertSame($uuid, $this->matcher->fromProviderState($uuid, '${id}')); + $this->assertSame(ProviderState::class, get_class($uuid->getGenerator())); } - public function testIncludes() + public function testEqual(): void { - $expected = [ - 'pact:matcher:type' => 'include', - 'value' => 'test string', - ]; - $actual = $this->matcher->includes('test string'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Equality::class, $this->matcher->equal('test string')); } - public function testNumber() + public function testIncludes(): void { - $expected = [ - 'value' => 13.01, - 'pact:matcher:type' => 'number', - ]; - $actual = $this->matcher->number(13.01); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Includes::class, $this->matcher->includes('test string')); } - public function testRandomNumber() + public function testNumber(): void { - $expected = [ - 'pact:generator:type' => 'RandomInt', - 'pact:matcher:type' => 'number', - ]; - $actual = $this->matcher->number(); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Number::class, $this->matcher->number(13.01)); } - public function testArrayContaining() + public function testArrayContaining(): void { - $expected = [ - 'pact:matcher:type' => 'arrayContains', - 'variants' => [ - 'item 1', - 'item 2' - ], - ]; - $actual = $this->matcher->arrayContaining([ + $this->assertInstanceOf(ArrayContains::class, $this->matcher->arrayContaining([ 'item 1', 'item 2' - ]); - - $this->assertEquals($expected, $actual); + ])); } - public function testArrayContainingWithKeys() + public function testNotEmpty(): void { - $expected = [ - 'pact:matcher:type' => 'arrayContains', - 'variants' => [ - 'item 1', - 'item 2' - ], - ]; - $actual = $this->matcher->arrayContaining([ - 'key 1' => 'item 1', - 'key 2' => 'item 2' - ]); - - $this->assertEquals($expected, $actual); - } - - public function testNotEmpty() - { - $expected = [ - 'value' => 'not empty string', - 'pact:matcher:type' => 'notEmpty', - ]; - $actual = $this->matcher->notEmpty('not empty string'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(NotEmpty::class, $this->matcher->notEmpty('not empty string')); } - public function testSemver() + public function testSemver(): void { - $expected = [ - 'value' => '1.2.3', - 'pact:matcher:type' => 'semver', - ]; - $actual = $this->matcher->semver('1.2.3'); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(Semver::class, $this->matcher->semver('1.2.3')); } - public function testInvalidStatusCode() + public function testValidStatusCode(): void { - $this->expectException(Exception::class); - $this->expectExceptionMessage("Status 'invalid' is not supported. Supported status are: info, success, redirect, clientError, serverError, nonError, error"); - $this->matcher->statusCode('invalid'); - } - - public function testValidStatusCode() - { - $expected = [ - 'pact:generator:type' => 'RandomInt', - 'min' => 200, - 'max' => 299, - 'status' => 'success', - 'pact:matcher:type' => 'statusCode', - ]; - $actual = $this->matcher->statusCode(HttpStatus::SUCCESS); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(StatusCode::class, $this->matcher->statusCode(HttpStatus::SUCCESS)); } - public function testValues() + public function testValues(): void { - $expected = [ - 'pact:matcher:type' => 'values', - 'value' => [ - 'item 1', - 'item 2' - ], - ]; - $actual = $this->matcher->values([ + $this->assertInstanceOf(Values::class, $this->matcher->values([ 'item 1', 'item 2' - ]); - - $this->assertEquals($expected, $actual); + ])); } - public function testValuesWithKeys() + public function testContentType(): void { - $expected = [ - 'pact:matcher:type' => 'values', - 'value' => [ - 'key 1' => 'item 1', - 'key 2' => 'item 2' - ], - ]; - $actual = $this->matcher->values([ - 'key 1' => 'item 1', - 'key 2' => 'item 2' - ]); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(ContentType::class, $this->matcher->contentType('image/jpeg')); } - public function testContentType() - { - $expected = [ - 'value' => 'image/jpeg', - 'pact:matcher:type' => 'contentType', - ]; - $actual = $this->matcher->contentType('image/jpeg'); - - $this->assertEquals($expected, $actual); - } - - public function testEachKey() + public function testEachKey(): void { $values = [ 'page 1' => 'Hello', @@ -886,17 +362,10 @@ public function testEachKey() $rules = [ $this->matcher->regex('page 3', '^page \d+$'), ]; - $expected = [ - 'rules' => $rules, - 'value' => $values, - 'pact:matcher:type' => 'eachKey', - ]; - $actual = $this->matcher->eachKey($values, $rules); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(EachKey::class, $this->matcher->eachKey($values, $rules)); } - public function testEachValue() + public function testEachValue(): void { $values = [ 'vehicle 1' => 'car', @@ -906,13 +375,6 @@ public function testEachValue() $rules = [ $this->matcher->regex('car', 'car|bike|motorbike'), ]; - $expected = [ - 'rules' => $rules, - 'value' => $values, - 'pact:matcher:type' => 'eachValue', - ]; - $actual = $this->matcher->eachValue($values, $rules); - - $this->assertEquals($expected, $actual); + $this->assertInstanceOf(EachValue::class, $this->matcher->eachValue($values, $rules)); } } diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 6ddcb9d7..0e2be97d 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -49,7 +49,7 @@ public function testSerializingWhenPathUsingMatcher() $this->assertEquals('PATCH', $model->getMethod()); $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['food' => ['milk']], $model->getQuery()); - $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); + $this->assertEquals('{"pact:matcher:type":"regex","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status"}', $model->getPath()); $body = $model->getBody(); $this->assertInstanceOf(Text::class, $body); From c9f1c3d41381b65a0ebc0adb33d2d4fe35ab4a65 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 13:01:42 +0700 Subject: [PATCH 162/298] test: Use assertJsonStringEqualsJsonString() for better comparing 2 json strings --- tests/PhpPact/Consumer/Model/ConsumerRequestTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 0e2be97d..9ef45bb7 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -49,7 +49,7 @@ public function testSerializingWhenPathUsingMatcher() $this->assertEquals('PATCH', $model->getMethod()); $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['food' => ['milk']], $model->getQuery()); - $this->assertEquals('{"pact:matcher:type":"regex","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status"}', $model->getPath()); + $this->assertJsonStringEqualsJsonString('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); $body = $model->getBody(); $this->assertInstanceOf(Text::class, $body); From da5aeff8c5dd580e95fa721db0ebcad4cbdf6795 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 18 Dec 2023 21:18:50 +0700 Subject: [PATCH 163/298] feat: Throw exception when both generator and example value are set --- .../GeneratorNotRequiredException.php | 7 +++ .../Matchers/GeneratorAwareMatcher.php | 3 + .../Consumer/Matcher/Matchers/BooleanTest.php | 14 +++-- .../Consumer/Matcher/Matchers/DateTest.php | 14 +++-- .../Matcher/Matchers/DateTimeTest.php | 14 +++-- .../Consumer/Matcher/Matchers/DecimalTest.php | 14 +++-- .../GeneratorAwareMatcherTestCase.php | 62 +++++++++++++++++-- .../Consumer/Matcher/Matchers/IntegerTest.php | 14 +++-- .../Consumer/Matcher/Matchers/NumberTest.php | 14 +++-- .../Consumer/Matcher/Matchers/RegexTest.php | 14 +++-- .../Consumer/Matcher/Matchers/SemverTest.php | 14 +++-- .../Matcher/Matchers/StatusCodeTest.php | 14 +++-- .../Consumer/Matcher/Matchers/TimeTest.php | 14 +++-- 13 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/GeneratorNotRequiredException.php diff --git a/src/PhpPact/Consumer/Matcher/Exception/GeneratorNotRequiredException.php b/src/PhpPact/Consumer/Matcher/Exception/GeneratorNotRequiredException.php new file mode 100644 index 00000000..c4be0ea9 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/GeneratorNotRequiredException.php @@ -0,0 +1,7 @@ + $this->generator->getType()] + $this->getMergedAttributes()->getData(); + } elseif ($this->generator) { + throw new GeneratorNotRequiredException(sprintf("Generator '%s' is not required for matcher '%s' when example value is set", $this->generator->getType(), $this->getType())); } return $data + $this->getAttributes()->getData() + ['value' => $this->getValue()]; diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php index 7188b48c..edca058e 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/BooleanTest.php @@ -3,12 +3,18 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Matchers\Boolean; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; class BooleanTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Boolean(); + return new Boolean(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Boolean(false); } /** @@ -18,7 +24,7 @@ protected function setUp(): void */ public function testSerialize(?bool $value, string $json): void { - $this->matcher = new Boolean($value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Boolean($value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php index 95a4b0aa..ea03cebc 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DateTest.php @@ -3,12 +3,18 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Matchers\Date; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; class DateTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Date(); + return new Date(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Date('yyyy-MM-dd', '2001-09-17'); } /** @@ -18,7 +24,7 @@ protected function setUp(): void public function testSerialize(?string $value, string $json): void { $format = 'yyyy-MM-dd'; - $this->matcher = new Date($format, $value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Date($format, $value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php index 1ffc4dd8..5c7f4f3e 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DateTimeTest.php @@ -3,12 +3,18 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Matchers\DateTime; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; class DateTimeTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new DateTime(); + return new DateTime(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new DateTime("yyyy-MM-dd HH:mm", '2011-07-13 16:41'); } /** @@ -18,7 +24,7 @@ protected function setUp(): void public function testSerialize(?string $value, string $json): void { $format = "yyyy-MM-dd'T'HH:mm:ss"; - $this->matcher = new DateTime($format, $value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new DateTime($format, $value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php index cb4b84d5..ce6330e4 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/DecimalTest.php @@ -3,12 +3,18 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Matchers\Decimal; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; class DecimalTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Decimal(); + return new Decimal(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Decimal(15.68); } /** @@ -17,7 +23,7 @@ protected function setUp(): void */ public function testSerialize(?float $value, string $json): void { - $this->matcher = new Decimal($value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Decimal($value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php b/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php index 3b5cab29..f8faa886 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php @@ -2,19 +2,69 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Exception\GeneratorNotRequiredException; use PhpPact\Consumer\Matcher\Exception\GeneratorRequiredException; +use PhpPact\Consumer\Matcher\Generators\Date; +use PhpPact\Consumer\Matcher\Generators\DateTime; +use PhpPact\Consumer\Matcher\Generators\MockServerURL; +use PhpPact\Consumer\Matcher\Generators\ProviderState; +use PhpPact\Consumer\Matcher\Generators\RandomBoolean; +use PhpPact\Consumer\Matcher\Generators\RandomDecimal; +use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; +use PhpPact\Consumer\Matcher\Generators\RandomInt; +use PhpPact\Consumer\Matcher\Generators\RandomString; +use PhpPact\Consumer\Matcher\Generators\Regex; +use PhpPact\Consumer\Matcher\Generators\Time; +use PhpPact\Consumer\Matcher\Generators\Uuid; use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; +use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PHPUnit\Framework\TestCase; abstract class GeneratorAwareMatcherTestCase extends TestCase { - protected GeneratorAwareMatcher $matcher; - - public function testMissingGenerator(): void + public function testGeneratorRequired(): void { + $matcher = $this->getMatcherWithoutExampleValue(); $this->expectException(GeneratorRequiredException::class); - $this->expectExceptionMessage(sprintf("Generator is required for matcher '%s' when example value is not set", $this->matcher->getType())); - $this->matcher->setGenerator(null); - json_encode($this->matcher); + $this->expectExceptionMessage(sprintf("Generator is required for matcher '%s' when example value is not set", $matcher->getType())); + $matcher->setGenerator(null); + json_encode($matcher); } + + /** + * @dataProvider generatorProvider + */ + public function testGeneratorNotRequired(GeneratorInterface $generator): void + { + $matcher = $this->getMatcherWithExampleValue(); + $this->expectException(GeneratorNotRequiredException::class); + $this->expectExceptionMessage(sprintf("Generator '%s' is not required for matcher '%s' when example value is set", $generator->getType(), $matcher->getType())); + $matcher->setGenerator($generator); + json_encode($matcher); + } + + /** + * @return GeneratorInterface[] + */ + public function generatorProvider(): array + { + return [ + [new Date()], + [new DateTime()], + [new MockServerURL('.*(/\d+)$', 'http://example.com/123')], + [new ProviderState('${key}')], + [new RandomBoolean()], + [new RandomDecimal()], + [new RandomHexadecimal()], + [new RandomInt()], + [new RandomString()], + [new Regex('\w')], + [new Time()], + [new Uuid()], + ]; + } + + abstract protected function getMatcherWithExampleValue(): GeneratorAwareMatcher; + + abstract protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher; } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php index e845d4ce..b36c74ba 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/IntegerTest.php @@ -2,13 +2,19 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\Integer; class IntegerTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Integer(); + return new Integer(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Integer(189); } /** @@ -17,7 +23,7 @@ protected function setUp(): void */ public function testSerialize(?int $value, string $json): void { - $this->matcher = new Integer($value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Integer($value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php index 92591aa8..0505623e 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/NumberTest.php @@ -2,13 +2,19 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\Number; class NumberTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Number(); + return new Number(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Number(56.73); } /** @@ -18,7 +24,7 @@ protected function setUp(): void */ public function testSerialize(int|float|null $value, string $json): void { - $this->matcher = new Number($value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Number($value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php index 6823e04a..e3eea53c 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/RegexTest.php @@ -3,15 +3,21 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Exception\InvalidRegexException; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\Regex; class RegexTest extends GeneratorAwareMatcherTestCase { private string $regex = '\d+'; - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Regex($this->regex); + return new Regex($this->regex); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Regex($this->regex, ['1', '23']); } /** @@ -28,7 +34,7 @@ public function testSerialize(string|array|null $values, ?string $json): void $value = is_array($values) ? $values[0] : $values; $this->expectExceptionMessage("The pattern '{$this->regex}' is not valid for value '{$value}'. Failed with error code 0."); } - $this->matcher = new Regex($this->regex, $values); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Regex($this->regex, $values); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php index 19bff24a..4178634d 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/SemverTest.php @@ -2,13 +2,19 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\Semver; class SemverTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Semver(); + return new Semver(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Semver('10.21.0-rc.1'); } /** @@ -17,7 +23,7 @@ protected function setUp(): void */ public function testSerialize(?string $value, string $json): void { - $this->matcher = new Semver($value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Semver($value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php index 5747a14f..74d6ef97 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/StatusCodeTest.php @@ -3,13 +3,19 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; use PhpPact\Consumer\Matcher\Exception\InvalidHttpStatusException; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\StatusCode; class StatusCodeTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new StatusCode('info'); + return new StatusCode('info'); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new StatusCode('error', 404); } /** @@ -29,7 +35,7 @@ public function testSerialize(string $status, ?string $value, ?string $json): vo $this->expectException(InvalidHttpStatusException::class); $this->expectExceptionMessage("Status 'invalid' is not supported. Supported status are: info, success, redirect, clientError, serverError, nonError, error"); } - $this->matcher = new StatusCode($status, $value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new StatusCode($status, $value); + $this->assertSame($json, json_encode($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php index b61647bb..3a6c7933 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/TimeTest.php @@ -2,13 +2,19 @@ namespace PhpPactTest\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Matchers\GeneratorAwareMatcher; use PhpPact\Consumer\Matcher\Matchers\Time; class TimeTest extends GeneratorAwareMatcherTestCase { - protected function setUp(): void + protected function getMatcherWithoutExampleValue(): GeneratorAwareMatcher { - $this->matcher = new Time(); + return new Time(); + } + + protected function getMatcherWithExampleValue(): GeneratorAwareMatcher + { + return new Time('HH:mm', '21:15'); } /** @@ -18,7 +24,7 @@ protected function setUp(): void public function testSerialize(?string $value, string $json): void { $format = 'HH:mm:ss'; - $this->matcher = new Time($format, $value); - $this->assertSame($json, json_encode($this->matcher)); + $matcher = new Time($format, $value); + $this->assertSame($json, json_encode($matcher)); } } From 3587763d7c7553e587ff8e32afccd72824c8e3d5 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 19 Dec 2023 09:49:40 +0700 Subject: [PATCH 164/298] feat: Add url matcher --- .../consumer/tests/Service/GeneratorsTest.php | 3 +++ .../generatorsConsumer-generatorsProvider.json | 15 +++++++++++++++ example/generators/provider/public/index.php | 1 + .../consumer/tests/Service/MatchersTest.php | 2 ++ .../pacts/matchersConsumer-matchersProvider.json | 10 ++++++++++ example/matchers/provider/public/index.php | 1 + src/PhpPact/Consumer/Matcher/Matcher.php | 15 +++++++++++++++ tests/PhpPact/Consumer/Matcher/MatcherTest.php | 16 ++++++++++++++++ 8 files changed, 63 insertions(+) diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index d9c76ffe..0bd2846d 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -48,6 +48,7 @@ public function testGetGenerators(): void 'datetime' => $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss", null), 'string' => $this->matcher->string(null), 'number' => $this->matcher->number(null), + 'url' => $this->matcher->url('http://localhost/users/1234/posts/latest', '.*(\\/users\\/\\d+\\/posts\\/latest)$'), 'requestId' => 222, ]); @@ -93,6 +94,8 @@ public function testGetGenerators(): void $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-d\TH:i:s")); $this->assertIsString($body['string']); $this->assertIsNumeric($body['number']); + $this->assertNotSame('http://localhost/users/1234/posts/latest', $body['url']); + $this->assertRegExp('/.*(\\/users\\/\\d+\\/posts\\/latest)$/', $body['url']); $this->assertSame(222, $body['requestId']); } diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json index 94be2039..12125983 100644 --- a/example/generators/pacts/generatorsConsumer-generatorsProvider.json +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -65,6 +65,7 @@ "requestId": 222, "string": "some string", "time": null, + "url": null, "uuid": null }, "contentType": "application/json", @@ -113,6 +114,11 @@ "format": "HH:mm:ss", "type": "Time" }, + "$.url": { + "example": "http://localhost/users/1234/posts/latest", + "regex": ".*(\\/users\\/\\d+\\/posts\\/latest)$", + "type": "MockServerURL" + }, "$.uuid": { "type": "Uuid" } @@ -215,6 +221,15 @@ } ] }, + "$.url": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*(\\/users\\/\\d+\\/posts\\/latest)$" + } + ] + }, "$.uuid": { "combine": "AND", "matchers": [ diff --git a/example/generators/provider/public/index.php b/example/generators/provider/public/index.php index ceebce11..f0185d04 100644 --- a/example/generators/provider/public/index.php +++ b/example/generators/provider/public/index.php @@ -23,6 +23,7 @@ 'datetime' => '1997-07-16T19:20:30', 'string' => 'another string', 'number' => 112.3, + 'url' => 'https://www.example.com/users/1234/posts/latest', 'requestId' => $body['id'], ])); diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 40a89a33..fb02ef63 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -103,6 +103,7 @@ public function testGetMatchers(): void ['vehicle 1' => 'car'], [$this->matcher->regex(null, 'car|bike|motorbike')] ), + 'url' => $this->matcher->url('http://localhost:8080/users/1234/posts/latest', '.*(\\/users\\/\\d+\\/posts\\/latest)$', false), 'query' => [ 'pages' => '22', 'locales' => ['en-US', 'en-AU'], @@ -195,6 +196,7 @@ public function testGetMatchers(): void 'eachValue' => [ 'vehicle 1' => 'car', ], + 'url' => 'http://localhost:8080/users/1234/posts/latest', 'query' => [ 'pages' => '22', 'locales' => ['en-US', 'en-AU'], diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index cf7b2083..a9f8dce8 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -140,6 +140,7 @@ "time": "23:59::58", "timeISO8601": "T22:44:30.652Z", "timestampRFC3339": "Mon, 31 Oct 2016 15:21:41 -0400", + "url": "http://localhost:8080/users/1234/posts/latest", "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb", "values": [ "a", @@ -534,6 +535,15 @@ } ] }, + "$.url": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*(\\/users\\/\\d+\\/posts\\/latest)$" + } + ] + }, "$.uuid": { "combine": "AND", "matchers": [ diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 509506b1..87fde298 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -85,6 +85,7 @@ 'item 1' => 'bike', 'item 2' => 'motorbike', ], + 'url' => 'https://www.example.com/users/1234/posts/latest', 'query' => $request->getQueryParams(), ])); diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index d7a8bbe5..e8ff518c 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -4,6 +4,7 @@ use PhpPact\Consumer\Matcher\Exception\MatcherException; use PhpPact\Consumer\Matcher\Exception\MatcherNotSupportedException; +use PhpPact\Consumer\Matcher\Generators\MockServerURL; use PhpPact\Consumer\Matcher\Generators\ProviderState; use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; use PhpPact\Consumer\Matcher\Generators\Uuid; @@ -440,4 +441,18 @@ public function eachValue(array $values, array $rules): EachValue { return new EachValue($values, $rules); } + + /** + * @throws MatcherException + */ + public function url(string $url, string $regex, bool $useMockServerBasePath = true): Regex + { + $matcher = new Regex($regex, $useMockServerBasePath ? null : $url); + + if ($useMockServerBasePath) { + $matcher->setGenerator(new MockServerURL($regex, $url)); + } + + return $matcher; + } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index b20e7d0d..76d6a370 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -4,6 +4,7 @@ use PhpPact\Consumer\Matcher\Exception\MatcherException; use PhpPact\Consumer\Matcher\Exception\MatcherNotSupportedException; +use PhpPact\Consumer\Matcher\Generators\MockServerURL; use PhpPact\Consumer\Matcher\Generators\ProviderState; use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; use PhpPact\Consumer\Matcher\Generators\Uuid; @@ -377,4 +378,19 @@ public function testEachValue(): void ]; $this->assertInstanceOf(EachValue::class, $this->matcher->eachValue($values, $rules)); } + + /** + * @testWith [true, true] + * [false, false] + */ + public function testUrl(bool $useMockServerBasePath, bool $hasGenerator): void + { + $url = $this->matcher->url('http://localhost:1234/path', '.*(/path)$', $useMockServerBasePath); + $this->assertInstanceOf(Regex::class, $url); + if ($hasGenerator) { + $this->assertSame(MockServerURL::class, get_class($url->getGenerator())); + } else { + $this->assertNull($url->getGenerator()); + } + } } From eee8305904d4a77abb3670c51a384ef88e57a07f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 19 Dec 2023 20:19:10 +0700 Subject: [PATCH 165/298] test(compatibility-suite): Add compatibility suite submodule --- .gitmodules | 3 +++ compatibility-suite/pact-compatibility-suite | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 compatibility-suite/pact-compatibility-suite diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..470a2e3f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "compatibility-suite/pact-compatibility-suite"] + path = compatibility-suite/pact-compatibility-suite + url = https://github.com/pact-foundation/pact-compatibility-suite.git diff --git a/compatibility-suite/pact-compatibility-suite b/compatibility-suite/pact-compatibility-suite new file mode 160000 index 00000000..db548451 --- /dev/null +++ b/compatibility-suite/pact-compatibility-suite @@ -0,0 +1 @@ +Subproject commit db548451c9a7515ba2adb11cb4c140fb1363f00b From 2947ae436c53f910cc0eeb38aaecc73f72643bfb Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 19 Dec 2023 20:38:00 +0700 Subject: [PATCH 166/298] chore: Remove old composer script --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 1c1df957..e70ecc1d 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,6 @@ } }, "scripts": { - "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", "static-code-analysis": "phpstan", "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", From 180eefc78d2b3342fea4342d80ead66ac5509bb6 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 20 Dec 2023 09:10:43 +0700 Subject: [PATCH 167/298] test(compatibility-suite): Add behat --- behat.yml | 0 composer.json | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 behat.yml diff --git a/behat.yml b/behat.yml new file mode 100644 index 00000000..e69de29b diff --git a/composer.json b/composer.json index 1c1df957..8d3be85a 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "phpstan/phpstan": "^1.9", "phpunit/phpunit": ">=8.5.23 <10", "guzzlehttp/guzzle": "^7.8", - "tienvx/pact-php-xml": "^0.1" + "tienvx/pact-php-xml": "^0.1", + "behat/behat": "^3.13" }, "autoload": { "psr-4": { @@ -44,6 +45,7 @@ "autoload-dev": { "psr-4": { "PhpPactTest\\": "tests/PhpPact", + "PhpPactTest\\CompatibilitySuite\\": "compatibility-suite/tests", "JsonConsumer\\": "example/json/consumer/src", "JsonConsumer\\Tests\\": "example/json/consumer/tests", "JsonProvider\\": "example/json/provider/src", @@ -80,7 +82,8 @@ "test": [ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", "phpunit --debug" - ] + ], + "check-compatibility": "behat" }, "extra": { "downloads": { From 9e8a2f8aa45b75c1822a6f26da4071b1af2d25c5 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 20 Dec 2023 10:51:22 +0700 Subject: [PATCH 168/298] test(compatibility-suite): Add SetUpContext --- .php-cs-fixer.php | 1 + compatibility-suite/pacts/.gitignore | 1 + compatibility-suite/tests/Constant/Path.php | 11 +++++++++ .../Context/Shared/Hook/SetUpContext.php | 23 +++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 compatibility-suite/pacts/.gitignore create mode 100644 compatibility-suite/tests/Constant/Path.php create mode 100644 compatibility-suite/tests/Context/Shared/Hook/SetUpContext.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index cf318188..43a2591d 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -4,6 +4,7 @@ ->in(__DIR__ . '/src') ->in(__DIR__ . '/tests') ->in(__DIR__ . '/example') + ->in(__DIR__ . '/compatibility-suite/tests') ->name('*.php'); $config = new PhpCsFixer\Config(); diff --git a/compatibility-suite/pacts/.gitignore b/compatibility-suite/pacts/.gitignore new file mode 100644 index 00000000..a6c57f5f --- /dev/null +++ b/compatibility-suite/pacts/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/compatibility-suite/tests/Constant/Path.php b/compatibility-suite/tests/Constant/Path.php new file mode 100644 index 00000000..4d84efdf --- /dev/null +++ b/compatibility-suite/tests/Constant/Path.php @@ -0,0 +1,11 @@ + Date: Wed, 20 Dec 2023 22:40:02 +0700 Subject: [PATCH 169/298] test(compatibility-suite): Add InteractionBuilder service --- .../Exception/CompatibilitySuiteException.php | 9 ++ .../Exception/FixtureNotFoundException.php | 7 + .../Exception/InvalidJsonFixtureException.php | 7 + compatibility-suite/tests/Model/Xml.php | 38 ++++++ .../tests/Service/FixtureLoader.php | 58 +++++++++ .../tests/Service/FixtureLoaderInterface.php | 16 +++ .../tests/Service/InteractionBuilder.php | 37 ++++++ .../Service/InteractionBuilderInterface.php | 10 ++ compatibility-suite/tests/Service/Parser.php | 122 ++++++++++++++++++ .../tests/Service/ParserInterface.php | 16 +++ .../tests/Service/RequestBuilder.php | 50 +++++++ .../tests/Service/RequestBuilderInterface.php | 10 ++ .../tests/Service/ResponseBuilder.php | 38 ++++++ .../Service/ResponseBuilderInterface.php | 10 ++ 14 files changed, 428 insertions(+) create mode 100644 compatibility-suite/tests/Exception/CompatibilitySuiteException.php create mode 100644 compatibility-suite/tests/Exception/FixtureNotFoundException.php create mode 100644 compatibility-suite/tests/Exception/InvalidJsonFixtureException.php create mode 100644 compatibility-suite/tests/Model/Xml.php create mode 100644 compatibility-suite/tests/Service/FixtureLoader.php create mode 100644 compatibility-suite/tests/Service/FixtureLoaderInterface.php create mode 100644 compatibility-suite/tests/Service/InteractionBuilder.php create mode 100644 compatibility-suite/tests/Service/InteractionBuilderInterface.php create mode 100644 compatibility-suite/tests/Service/Parser.php create mode 100644 compatibility-suite/tests/Service/ParserInterface.php create mode 100644 compatibility-suite/tests/Service/RequestBuilder.php create mode 100644 compatibility-suite/tests/Service/RequestBuilderInterface.php create mode 100644 compatibility-suite/tests/Service/ResponseBuilder.php create mode 100644 compatibility-suite/tests/Service/ResponseBuilderInterface.php diff --git a/compatibility-suite/tests/Exception/CompatibilitySuiteException.php b/compatibility-suite/tests/Exception/CompatibilitySuiteException.php new file mode 100644 index 00000000..8444efa8 --- /dev/null +++ b/compatibility-suite/tests/Exception/CompatibilitySuiteException.php @@ -0,0 +1,9 @@ + $this->xmlElementToArray($root), + ]); + } + + private function xmlElementToArray(SimpleXMLElement $element): array + { + $children = $element->children(); + if (0 !== $children->count()) { + $items = []; + foreach ($children as $child) { + $items[] = $this->xmlElementToArray($child); + } + + return [ + 'name' => $element->getName(), + 'children' => $items, + ]; + } else { + return [ + 'content' => (string) $element, + ]; + } + } +} diff --git a/compatibility-suite/tests/Service/FixtureLoader.php b/compatibility-suite/tests/Service/FixtureLoader.php new file mode 100644 index 00000000..66367ab5 --- /dev/null +++ b/compatibility-suite/tests/Service/FixtureLoader.php @@ -0,0 +1,58 @@ +getFilePath($fileName)); + } + + public function loadJson(string $fileName): mixed + { + try { + return json_decode($this->load($fileName), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new InvalidJsonFixtureException(sprintf("Could not load json fixture '%s': %s", $fileName, $exception->getMessage())); + } + } + + public function isBinary(string $fileName): bool + { + $ext = pathinfo($this->getFilePath($fileName), PATHINFO_EXTENSION); + + // TODO Find a better way + return in_array($ext, ['jpg', 'pdf']); + } + + public function determineContentType(string $fileName): string + { + if (str_ends_with($fileName, '.json')) { + return 'application/json'; + } elseif (str_ends_with($fileName, '.xml')) { + return 'application/xml'; + } elseif (str_ends_with($fileName, '.jpg')) { + return 'image/jpeg'; + } elseif (str_ends_with($fileName, '.pdf')) { + return 'application/pdf'; + } else { + return 'text/plain'; + } + } + + public function getFilePath(string $fileName): string + { + $filePath = Path::FIXTURES_PATH . '/' . $fileName; + if (!file_exists($filePath)) { + throw new FixtureNotFoundException(sprintf("Could not load fixture '%s'", $fileName)); + } + + return $filePath; + } +} diff --git a/compatibility-suite/tests/Service/FixtureLoaderInterface.php b/compatibility-suite/tests/Service/FixtureLoaderInterface.php new file mode 100644 index 00000000..9e4eae77 --- /dev/null +++ b/compatibility-suite/tests/Service/FixtureLoaderInterface.php @@ -0,0 +1,16 @@ +setDescription($data['description'] ?? 'Interaction ' . (int) $data['No']); + + $request = new ConsumerRequest(); + $this->requestBuilder->build($request, array_intersect_key($data, array_flip(['method', 'path', 'query', 'headers', 'body']))); + $interaction->setRequest($request); + + $response = new ProviderResponse(); + $this->responseBuilder->build($response, array_filter([ + 'status' => $data['response'] ?? null, + 'headers' => $data['response headers'] ?? null, + 'body' => $data['response body'] ?? null, + 'content-type' => $data['response content'] ?? null, + ])); + $interaction->setResponse($response); + + return $interaction; + } +} diff --git a/compatibility-suite/tests/Service/InteractionBuilderInterface.php b/compatibility-suite/tests/Service/InteractionBuilderInterface.php new file mode 100644 index 00000000..4e48840a --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionBuilderInterface.php @@ -0,0 +1,10 @@ + trim($value), explode(',', $value))); + } + + return $values; + }, + [] + ); + } + + public function parseBody(string $body, ?string $contentType = null): Text|Binary|Multipart|null + { + if (empty($body)) { + return null; + } + if (str_starts_with($body, 'JSON:')) { + return new Text(trim(substr($body, 5)), 'application/json'); + } + if (str_starts_with($body, 'XML:')) { + return new Text(trim(substr($body, 4)), 'application/xml'); + } + if (str_starts_with($body, 'file:')) { + $fileName = trim(substr($body, 5)); + $contents = $this->fixtureLoader->load($fileName); + if (str_ends_with($fileName, '-body.xml')) { + $body = simplexml_load_string($contents); + if (!$body) { + throw new Exception(sprintf("could not read fixture '%s'", $fileName)); + } + $contentType = (string) $body->contentType ?? 'text/plain'; + $contents = $body->contents ?? ''; + $lineEndings = (string) (iterator_to_array($contents->attributes())['eol'] ?? ''); + + if ($lineEndings === 'CRLF' && PHP_OS_FAMILY !== 'Windows') { + $contents = str_replace("\n", "\r\n", $contents); + } + + if ($this->specificationVersion !== '1.0.0' && $contentType === 'application/xml') { + return new Xml($contents, $contentType); + } else { + return new Text($contents, $contentType); + } + } else { + $contentType ??= $this->fixtureLoader->determineContentType($fileName); + $isBinary = $this->fixtureLoader->isBinary($fileName); + $filePath = $this->fixtureLoader->getFilePath($fileName); + + return $isBinary ? new Binary($filePath, $contentType) : new Text($contents, $contentType); + } + } + if ($body === 'EMPTY') { + $body = ''; + } + + return new Text($body, $contentType ?? 'text/plain'); + } + + public function parseQueryString(string $query): array + { + if (empty($query)) { + return []; + } + + return array_reduce( + explode('&', $query), + function (array $values, string $kv): array { + if (str_contains($kv, '=')) { + [$key, $value] = explode('=', $kv, 2); + $values[$key][] = $value; + } else { + $values[$kv][] = ''; + } + + return $values; + }, + [] + ); + } +} diff --git a/compatibility-suite/tests/Service/ParserInterface.php b/compatibility-suite/tests/Service/ParserInterface.php new file mode 100644 index 00000000..eb934a26 --- /dev/null +++ b/compatibility-suite/tests/Service/ParserInterface.php @@ -0,0 +1,16 @@ + $value) { + switch ($key) { + case 'method': + $request->setMethod($data['method']); + break; + + case 'path': + $request->setPath($data['path']); + break; + + case 'query': + $request->setQuery($this->parser->parseQueryString($data['query'])); + break; + + case 'headers': + $request->setHeaders($this->parser->parseHeaders($data['headers'])); + break; + + case 'raw headers': + $request->setHeaders($this->parser->parseHeaders($data['raw headers'], true)); + break; + + case 'body': + $request->setBody($this->parser->parseBody($data['body'], $request->getBody()?->getContentType())); + break; + + case 'content type': + $request->addHeader('Content-Type', $data['content type']); + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/RequestBuilderInterface.php b/compatibility-suite/tests/Service/RequestBuilderInterface.php new file mode 100644 index 00000000..0f9e30f3 --- /dev/null +++ b/compatibility-suite/tests/Service/RequestBuilderInterface.php @@ -0,0 +1,10 @@ + $value) { + switch ($key) { + case 'status': + $response->setStatus($data['status']); + break; + + case 'headers': + $response->setHeaders($this->parser->parseHeaders($data['headers'])); + break; + + case 'body': + $response->setBody($this->parser->parseBody($data['body'], $response->getBody()?->getContentType())); + break; + + case 'content-type': + $response->addHeader('Content-Type', $data['content-type']); + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/ResponseBuilderInterface.php b/compatibility-suite/tests/Service/ResponseBuilderInterface.php new file mode 100644 index 00000000..28f46c9f --- /dev/null +++ b/compatibility-suite/tests/Service/ResponseBuilderInterface.php @@ -0,0 +1,10 @@ + Date: Thu, 21 Dec 2023 07:50:40 +0700 Subject: [PATCH 170/298] test(compatibility-suite): Define interactions --- .../Context/Shared/InteractionsContext.php | 39 +++++ .../Shared/Transform/InteractionsContext.php | 48 ++++++ .../Exception/CompatibilitySuiteException.php | 2 +- .../Exception/FixtureNotFoundException.php | 2 +- .../IntegrationJsonFormatException.php | 7 + .../Exception/InvalidJsonFixtureException.php | 2 +- .../Exception/InvalidXmlFixtureException.php | 7 + .../MatchingRuleConditionException.php | 7 + .../UndefinedInteractionException.php | 7 + .../tests/Model/MatchingRule.php | 39 +++++ .../tests/Service/FixtureLoader.php | 4 +- .../tests/Service/InteractionsStorage.php | 43 ++++++ .../Service/InteractionsStorageInterface.php | 17 +++ .../tests/Service/MatchingRuleConverter.php | 134 +++++++++++++++++ .../MatchingRuleConverterInterface.php | 11 ++ .../tests/Service/MatchingRuleParser.php | 138 ++++++++++++++++++ .../Service/MatchingRuleParserInterface.php | 13 ++ .../tests/Service/MatchingRulesStorage.php | 21 +++ .../Service/MatchingRulesStorageInterface.php | 13 ++ compatibility-suite/tests/Service/Parser.php | 4 +- .../Service/RequestMatchingRuleBuilder.php | 76 ++++++++++ .../RequestMatchingRuleBuilderInterface.php | 10 ++ .../Service/ResponseMatchingRuleBuilder.php | 55 +++++++ .../ResponseMatchingRuleBuilderInterface.php | 10 ++ composer.json | 3 +- 25 files changed, 704 insertions(+), 8 deletions(-) create mode 100644 compatibility-suite/tests/Context/Shared/InteractionsContext.php create mode 100644 compatibility-suite/tests/Context/Shared/Transform/InteractionsContext.php create mode 100644 compatibility-suite/tests/Exception/IntegrationJsonFormatException.php create mode 100644 compatibility-suite/tests/Exception/InvalidXmlFixtureException.php create mode 100644 compatibility-suite/tests/Exception/MatchingRuleConditionException.php create mode 100644 compatibility-suite/tests/Exception/UndefinedInteractionException.php create mode 100644 compatibility-suite/tests/Model/MatchingRule.php create mode 100644 compatibility-suite/tests/Service/InteractionsStorage.php create mode 100644 compatibility-suite/tests/Service/InteractionsStorageInterface.php create mode 100644 compatibility-suite/tests/Service/MatchingRuleConverter.php create mode 100644 compatibility-suite/tests/Service/MatchingRuleConverterInterface.php create mode 100644 compatibility-suite/tests/Service/MatchingRuleParser.php create mode 100644 compatibility-suite/tests/Service/MatchingRuleParserInterface.php create mode 100644 compatibility-suite/tests/Service/MatchingRulesStorage.php create mode 100644 compatibility-suite/tests/Service/MatchingRulesStorageInterface.php create mode 100644 compatibility-suite/tests/Service/RequestMatchingRuleBuilder.php create mode 100644 compatibility-suite/tests/Service/RequestMatchingRuleBuilderInterface.php create mode 100644 compatibility-suite/tests/Service/ResponseMatchingRuleBuilder.php create mode 100644 compatibility-suite/tests/Service/ResponseMatchingRuleBuilderInterface.php diff --git a/compatibility-suite/tests/Context/Shared/InteractionsContext.php b/compatibility-suite/tests/Context/Shared/InteractionsContext.php new file mode 100644 index 00000000..be01ca39 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/InteractionsContext.php @@ -0,0 +1,39 @@ + $interaction) { + $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_DOMAIN, $id, $interaction); + $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_CLIENT_DOMAIN, $id, $interaction, true); + $this->storage->add(InteractionsStorageInterface::PROVIDER_DOMAIN, $id, $interaction, true); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id, $interaction); + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { + $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $file); + } + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id)) { + $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $file); + } + } + } +} diff --git a/compatibility-suite/tests/Context/Shared/Transform/InteractionsContext.php b/compatibility-suite/tests/Context/Shared/Transform/InteractionsContext.php new file mode 100644 index 00000000..8de5c6f2 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/Transform/InteractionsContext.php @@ -0,0 +1,48 @@ + + */ + public function getInteractions(TableNode $table): array + { + $interactions = []; + foreach ($table->getHash() as $data) { + $id = (int) $data['No']; + $interactions[$id] = $this->builder->build($data); + $this->storeMatchingRules($id, $data); + } + + return $interactions; + } + + private function storeMatchingRules(int $id, array $data): void + { + if (isset($data['matching rules'])) { + $this->matchingRulesStorage->add(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id, $data['matching rules']); + } + if (isset($data['response matching rules'])) { + $this->matchingRulesStorage->add(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id, $data['response matching rules']); + } + } +} diff --git a/compatibility-suite/tests/Exception/CompatibilitySuiteException.php b/compatibility-suite/tests/Exception/CompatibilitySuiteException.php index 8444efa8..405c95f4 100644 --- a/compatibility-suite/tests/Exception/CompatibilitySuiteException.php +++ b/compatibility-suite/tests/Exception/CompatibilitySuiteException.php @@ -1,6 +1,6 @@ matcher; + } + + public function getCategory(): string + { + return $this->category; + } + + public function getSubCategory(): string + { + return $this->subCategory; + } + + public function getMatcherAttributes(): array + { + return $this->matcherAttributes; + } + + public function getMatcherAttribute(string $attribute): mixed + { + return $this->matcherAttributes[$attribute] ?? null; + } +} diff --git a/compatibility-suite/tests/Service/FixtureLoader.php b/compatibility-suite/tests/Service/FixtureLoader.php index 66367ab5..571b4179 100644 --- a/compatibility-suite/tests/Service/FixtureLoader.php +++ b/compatibility-suite/tests/Service/FixtureLoader.php @@ -3,9 +3,9 @@ namespace PhpPactTest\CompatibilitySuite\Service; use JsonException; -use PhpPact\Consumer\Exception\FixtureNotFoundException; -use PhpPact\Consumer\Exception\InvalidJsonFixtureException; use PhpPactTest\CompatibilitySuite\Constant\Path; +use PhpPactTest\CompatibilitySuite\Exception\FixtureNotFoundException; +use PhpPactTest\CompatibilitySuite\Exception\InvalidJsonFixtureException; class FixtureLoader implements FixtureLoaderInterface { diff --git a/compatibility-suite/tests/Service/InteractionsStorage.php b/compatibility-suite/tests/Service/InteractionsStorage.php new file mode 100644 index 00000000..075d3145 --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsStorage.php @@ -0,0 +1,43 @@ + + */ + private array $interactions = []; + + public function add(string $domain, int $id, Interaction $interaction, bool $clone = false): void + { + $this->interactions[$domain][$id] = $clone ? $this->cloneInteraction($interaction) : $interaction; + } + + public function get(string $domain, int $id): Interaction + { + if (!isset($this->interactions[$domain][$id])) { + throw new UndefinedInteractionException(sprintf('Interaction %s is not defined in domain %s', $id, $domain)); + } + + return $this->interactions[$domain][$id]; + } + + private function cloneInteraction(Interaction $interaction): Interaction + { + $result = clone $interaction; + $result->setRequest(clone $interaction->getRequest()); + if ($interaction->getRequest()->getBody()) { + $result->getRequest()->setBody(clone $interaction->getRequest()->getBody()); + } + $result->setResponse(clone $interaction->getResponse()); + if ($interaction->getResponse()->getBody()) { + $result->getResponse()->setBody(clone $interaction->getResponse()->getBody()); + } + + return $result; + } +} diff --git a/compatibility-suite/tests/Service/InteractionsStorageInterface.php b/compatibility-suite/tests/Service/InteractionsStorageInterface.php new file mode 100644 index 00000000..74f3ba8f --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsStorageInterface.php @@ -0,0 +1,17 @@ +getMatcher()) { + case 'type': + $min = $rule->getMatcherAttribute('min'); + $max = $rule->getMatcherAttribute('max'); + if (null !== $min && null !== $max) { + return new MinMaxType($value, $min, $max); + } + if (null !== $min) { + return new MinType($value, $min); + } + if (null !== $max) { + return new MaxType($value, $max); + } + return new Type($value); + + case 'equality': + return new Equality($value); + + case 'include': + return new Includes($rule->getMatcherAttribute('value')); + + case 'number': + return new Number($this->getNumber($value)); + + case 'integer': + return new Integer($this->getNumber($value)); + + case 'decimal': + return new Decimal($this->getNumber($value)); + + case 'null': + return new NullValue(); + + case 'date': + return new Date($rule->getMatcherAttribute('format'), $value); + + case 'boolean': + return new Boolean($value); + + case 'contentType': + return new ContentType($rule->getMatcherAttribute('value')); + + case 'values': + return new Values($value); + + case 'notEmpty': + return new NotEmpty($value); + + case 'semver': + return new Semver($value); + + case 'eachKey': + return new EachKey($value, $rule->getMatcherAttribute('rules')); + + case 'eachValue': + return new EachValue($value, $rule->getMatcherAttribute('rules')); + + case 'arrayContains': + return new ArrayContains($rule->getMatcherAttribute('variants')); + + case 'regex': + $regex = $rule->getMatcherAttribute('regex'); + return new Regex($regex, $this->ignoreInvalidValue($regex, $value)); + + case 'statusCode': + return new StatusCode($rule->getMatcherAttribute('status')); + + default: + return null; + } + } + + private function getNumber(mixed $value): int|float|null + { + if (is_numeric($value)) { + $value = $value + 0; + } else { + $value = null; + } + + return $value; + } + + private function ignoreInvalidValue(string $regex, mixed $value): string|array|null + { + if (is_string($value)) { + if (!preg_match("/$regex/", $value)) { + $value = null; + } + } elseif (is_array($value)) { + foreach (array_keys($value) as $key) { + if (!preg_match("/$regex/", $value[$key])) { + $value[$key] = null; + } + } + } else { + $value = null; + } + + return $value; + } +} diff --git a/compatibility-suite/tests/Service/MatchingRuleConverterInterface.php b/compatibility-suite/tests/Service/MatchingRuleConverterInterface.php new file mode 100644 index 00000000..f992fa70 --- /dev/null +++ b/compatibility-suite/tests/Service/MatchingRuleConverterInterface.php @@ -0,0 +1,11 @@ +fixtureLoader->loadJson($fileName); + switch ($this->getSpecification($fileName)) { + case 'v2': + return $this->loadFromV2Map($map); + + case 'v3': + case 'v4': + return $this->loadFromV3Map($map); + + default: + return []; + } + } + + private function loadFromV2Map(array $map): array + { + $rules = []; + foreach ($map as $k => $v) { + if ($k === '$.body') { + $rules[] = new MatchingRule($v['match'], 'body', '$', $v); + } elseif (str_starts_with($k, '$.body')) { + $rules[] = new MatchingRule($v['match'], 'body', '$' . substr($k, 6), $v); + } elseif (str_starts_with($k, '$.headers')) { + $rules[] = new MatchingRule($v['match'], 'header', explode('.', $k, 3)[2], $v); + } else { + @[, $category, $subCategory] = explode('.', $k, 3); + $rules[] = new MatchingRule($v['match'], $category, $subCategory ?? '', $v); + } + } + + return $rules; + } + + private function loadFromV3Map(array $map): array + { + foreach ($map as $category => $subMap) { + switch ($category) { + case 'body': + return $this->getV3BodyMatchers($subMap); + + case 'status': + return $this->getV4StatusCodeMatchers($subMap); + + default: + break; + } + } + + return []; + } + + private function getV3BodyMatchers(array $map): array + { + $matchers = []; + foreach ($map as $subCategory => $subMap) { + if ($subMap['combine'] !== 'AND') { + throw new MatchingRuleConditionException("FFI call doesn't support OR matcher condition"); + } + foreach ($subMap['matchers'] as $matcher) { + switch ($matcher['match']) { + case 'eachKey': + case 'eachValue': + $matcher['rules'] = array_map(fn (array $rule) => $this->converter->convert(new MatchingRule($rule['match'], '', '', $rule), null), $matcher['rules']); + break; + + case 'arrayContains': + $items = []; + foreach ($matcher['variants'] as $variant) { + $value = []; + foreach ($variant['rules'] as $key => $rule) { + $key = str_replace('$.', '', $key); + if ($key === '*') { + // TODO It seems that IntegrationJson doesn't support '*'. Find a better way than hard coding like this. + $value['href'] = $this->converter->convert(new MatchingRule($rule['matchers'][0]['match'], '', '', $rule['matchers'][0]), 'http://api.x.io/orders/42/items'); + $value['title'] = $this->converter->convert(new MatchingRule($rule['matchers'][0]['match'], '', '', $rule['matchers'][0]), 'Delete Item'); + } else { + $regex = str_replace('\-', '-', $rule['matchers'][0]['regex']); + $value[$key] = $this->converter->convert(new MatchingRule($rule['matchers'][0]['match'], '', '', $rule['matchers'][0]), $regex); + } + } + $items[] = $value; + } + $matcher['variants'] = $items; + break; + + default: + break; + } + $matchers[] = new MatchingRule($matcher['match'], 'body', $subCategory, $matcher); + } + } + $this->sortMatchersByLevel($matchers); + + return $matchers; + } + + private function getV4StatusCodeMatchers(array $map): array + { + $matcher = $map['matchers'][0]; + + return [ + new MatchingRule($matcher['match'], 'status', '', $matcher), + ]; + } + + private function sortMatchersByLevel(array &$matchers): void + { + usort( + $matchers, + fn (MatchingRule $a, MatchingRule $b) => count(explode('.', $b->getSubCategory())) - count(explode('.', $a->getSubCategory())) + ); + } + + private function getSpecification(string $fileName): string + { + $basename = substr_replace($fileName, '', -5); + $parts = explode('-', $basename); + + return end($parts); + } +} diff --git a/compatibility-suite/tests/Service/MatchingRuleParserInterface.php b/compatibility-suite/tests/Service/MatchingRuleParserInterface.php new file mode 100644 index 00000000..9f89e0ee --- /dev/null +++ b/compatibility-suite/tests/Service/MatchingRuleParserInterface.php @@ -0,0 +1,13 @@ + + */ + public function parse(string $fileName): array; +} diff --git a/compatibility-suite/tests/Service/MatchingRulesStorage.php b/compatibility-suite/tests/Service/MatchingRulesStorage.php new file mode 100644 index 00000000..e2a4a92f --- /dev/null +++ b/compatibility-suite/tests/Service/MatchingRulesStorage.php @@ -0,0 +1,21 @@ + + */ + private array $files = []; + + public function add(string $domain, int $id, string $file): void + { + $this->files[$domain][$id] = $file; + } + + public function get(string $domain, int $id): ?string + { + return $this->files[$domain][$id] ?? null; + } +} diff --git a/compatibility-suite/tests/Service/MatchingRulesStorageInterface.php b/compatibility-suite/tests/Service/MatchingRulesStorageInterface.php new file mode 100644 index 00000000..32210d0c --- /dev/null +++ b/compatibility-suite/tests/Service/MatchingRulesStorageInterface.php @@ -0,0 +1,13 @@ +contentType ?? 'text/plain'; $contents = $body->contents ?? ''; diff --git a/compatibility-suite/tests/Service/RequestMatchingRuleBuilder.php b/compatibility-suite/tests/Service/RequestMatchingRuleBuilder.php new file mode 100644 index 00000000..19f89925 --- /dev/null +++ b/compatibility-suite/tests/Service/RequestMatchingRuleBuilder.php @@ -0,0 +1,76 @@ +parser->parse($file) as $rule) { + switch ($rule->getCategory()) { + case 'method': + // I don't think method support matching rule, at least in pact-php. + break; + + case 'path': + $matcher = $this->converter->convert($rule, $request->getPath()); + if ($matcher) { + $request->setPath($matcher); + } + break; + + case 'query': + if ($rule->getSubCategory()) { + $queryValues = $request->getQuery()[$rule->getSubCategory()]; + $matcher = $this->converter->convert($rule, $queryValues); + if ($matcher) { + $request->addQueryParameter($rule->getSubCategory(), $matcher); + } + } + break; + + case 'header': + if ($rule->getSubCategory()) { + $headerValues = array_change_key_case($request->getHeaders())[$rule->getSubCategory()]; + $matcher = $this->converter->convert($rule, $headerValues); + if ($matcher) { + $request->addHeader($rule->getSubCategory(), $matcher); + } + } + break; + + case 'body': + $body = $request->getBody(); + if ($body instanceof Text && $body->getContentType() === 'application/json') { + $jsonObject = new JsonObject($body->getContents(), true); + $value = $jsonObject->{$rule->getSubCategory()}; + if (str_contains($rule->getSubCategory(), '*')) { + $value = reset($value); // This is for handling '$.two.*.ids' and '$.*' + } + $matcher = $this->converter->convert($rule, $value); + if ($matcher) { + $jsonObject->{$rule->getSubCategory()} = $matcher; + $body->setContents($jsonObject); + } + } else { + throw new IntegrationJsonFormatException("Integration JSON format doesn't support non-JSON format"); + } + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/RequestMatchingRuleBuilderInterface.php b/compatibility-suite/tests/Service/RequestMatchingRuleBuilderInterface.php new file mode 100644 index 00000000..e2962f67 --- /dev/null +++ b/compatibility-suite/tests/Service/RequestMatchingRuleBuilderInterface.php @@ -0,0 +1,10 @@ +parser->parse($file) as $rule) { + switch ($rule->getCategory()) { + case 'status': + $response->setStatus($this->converter->convert($rule, $response->getStatus())); + break; + + case 'header': + if ($rule->getSubCategory()) { + $headerValues = array_change_key_case($response->getHeaders())[$rule->getSubCategory()]; + $matcher = $this->converter->convert($rule, $headerValues); + if ($matcher) { + $response->addHeader($rule->getSubCategory(), $matcher); + } + } + break; + + case 'body': + $body = $response->getBody(); + if ($body instanceof Text && $body->getContentType() === 'application/json') { + $jsonObject = new JsonObject($body->getContents(), true); + $matcher = $this->converter->convert($rule, $jsonObject->{$rule->getSubCategory()}); + if ($matcher) { + $jsonObject->{$rule->getSubCategory()} = $matcher; + $body->setContents($jsonObject); + } + } else { + throw new IntegrationJsonFormatException("Integration JSON format doesn't support non-JSON format"); + } + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/ResponseMatchingRuleBuilderInterface.php b/compatibility-suite/tests/Service/ResponseMatchingRuleBuilderInterface.php new file mode 100644 index 00000000..37ed1614 --- /dev/null +++ b/compatibility-suite/tests/Service/ResponseMatchingRuleBuilderInterface.php @@ -0,0 +1,10 @@ +=8.5.23 <10", "guzzlehttp/guzzle": "^7.8", "tienvx/pact-php-xml": "^0.1", - "behat/behat": "^3.13" + "behat/behat": "^3.13", + "galbar/jsonpath": "^3.0" }, "autoload": { "psr-4": { From b8a5234bf1a0fa6c4483f589dde3fa7352cf83bd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 21 Dec 2023 21:38:53 +0700 Subject: [PATCH 171/298] test(compatibility-suite): Implement V1 Consumer scenarios --- .../tests/Constant/Mismatch.php | 32 ++ .../Context/Shared/InteractionsContext.php | 35 ++- .../tests/Context/V1/Http/ConsumerContext.php | 295 ++++++++++++++++++ compatibility-suite/tests/Service/Client.php | 28 ++ .../tests/Service/ClientInterface.php | 12 + .../Service/InteractionsStorageInterface.php | 5 +- compatibility-suite/tests/Service/Server.php | 95 ++++++ .../tests/Service/ServerInterface.php | 21 ++ 8 files changed, 510 insertions(+), 13 deletions(-) create mode 100644 compatibility-suite/tests/Constant/Mismatch.php create mode 100644 compatibility-suite/tests/Context/V1/Http/ConsumerContext.php create mode 100644 compatibility-suite/tests/Service/Client.php create mode 100644 compatibility-suite/tests/Service/ClientInterface.php create mode 100644 compatibility-suite/tests/Service/Server.php create mode 100644 compatibility-suite/tests/Service/ServerInterface.php diff --git a/compatibility-suite/tests/Constant/Mismatch.php b/compatibility-suite/tests/Constant/Mismatch.php new file mode 100644 index 00000000..2b5ab806 --- /dev/null +++ b/compatibility-suite/tests/Constant/Mismatch.php @@ -0,0 +1,32 @@ + false, + 'PathMismatch' => false, + 'StatusMismatch' => 'Response status did not match', + 'QueryMismatch' => false, + 'HeaderMismatch' => 'Headers had differences', + 'BodyTypeMismatch' => 'Body type had differences', + 'BodyMismatch' => 'Body had differences', + 'MetadataMismatch' => 'Metadata had differences', + ]; + + public const VERIFIER_MISMATCH_ERROR_MAP = [ + 'One or more of the setup state change handlers has failed' => 'State change request failed', + ]; + + public const MOCK_SERVER_MISMATCH_TYPE_MAP = [ + 'method' => 'MethodMismatch', + 'path' => 'PathMismatch', + 'status' => 'StatusMismatch', + 'query' => 'QueryMismatch', + 'header' => 'HeaderMismatch', + 'body-content-type' => 'BodyTypeMismatch', + 'body' => 'BodyMismatch', + 'metadata' => 'MetadataMismatch', + ]; +} diff --git a/compatibility-suite/tests/Context/Shared/InteractionsContext.php b/compatibility-suite/tests/Context/Shared/InteractionsContext.php index be01ca39..d7d911dd 100644 --- a/compatibility-suite/tests/Context/Shared/InteractionsContext.php +++ b/compatibility-suite/tests/Context/Shared/InteractionsContext.php @@ -3,6 +3,7 @@ namespace PhpPactTest\CompatibilitySuite\Context\Shared; use Behat\Behat\Context\Context; +use PhpPact\Consumer\Model\Interaction; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\MatchingRulesStorageInterface; use PhpPactTest\CompatibilitySuite\Service\RequestMatchingRuleBuilderInterface; @@ -24,16 +25,30 @@ public function __construct( public function theFollowingHttpInteractionsHaveBeenDefined(array $interactions): void { foreach ($interactions as $id => $interaction) { - $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_DOMAIN, $id, $interaction); - $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_CLIENT_DOMAIN, $id, $interaction, true); - $this->storage->add(InteractionsStorageInterface::PROVIDER_DOMAIN, $id, $interaction, true); - $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id, $interaction); - if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { - $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $file); - } - if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id)) { - $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $file); - } + $this->storeInteractionWithoutMatchingRules($id, $interaction); + $this->storeInteractionWithMatchingRules($id, $interaction); + } + } + + private function storeInteractionWithoutMatchingRules(int $id, Interaction $interaction): void + { + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $id, $interaction, true); + } + + private function storeInteractionWithMatchingRules(int $id, Interaction $interaction): void + { + $this->buildMatchingRules($id, $interaction); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $id, $interaction, true); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id, $interaction, true); + } + + private function buildMatchingRules(int $id, Interaction $interaction): void + { + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { + $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $file); + } + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id)) { + $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $file); } } } diff --git a/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php new file mode 100644 index 00000000..a721d0df --- /dev/null +++ b/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php @@ -0,0 +1,295 @@ +server->register($id); + } + + /** + * @When request :id is made to the mock server + */ + public function requestIsMadeToTheMockServer(int $id): void + { + $this->client->sendRequestToServer($id); + } + + /** + * @Then a :code success response is returned + */ + public function aSuccessResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->client->getResponse()->getStatusCode()); + } + + /** + * @Then the payload will contain the :name JSON document + */ + public function thePayloadWillContainTheJsonDocument(string $name): void + { + Assert::assertJsonStringEqualsJsonString($this->fixtureLoader->load($name . '.json'), (string) $this->client->getResponse()->getBody()); + } + + /** + * @Then the content type will be set as :contentType + */ + public function theContentTypeWillBeSetAs(string $contentType): void + { + Assert::assertSame($contentType, $this->client->getResponse()->getHeaderLine('Content-Type')); + } + + /** + * @When the pact test is done + */ + public function thePactTestIsDone(): void + { + $this->server->verify(); + } + + /** + * @Then the mock server status will be OK + */ + public function theMockServerStatusWillBeOk(): void + { + Assert::assertTrue($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then the mock server will write out a Pact file for the interaction when done + */ + public function theMockServerWillWriteOutAPactFileForTheInteractionWhenDone(): void + { + Assert::assertTrue(file_exists($this->server->getPactPath())); + } + + /** + * @Then the pact file will contain {:num} interaction(s) + */ + public function thePactFileWillContainInteraction(int $num): void + { + $this->pact = json_decode(file_get_contents($this->server->getPactPath()), true); + Assert::assertEquals($num, count($this->pact['interactions'] ?? [])); + } + + /** + * @Then the {first} interaction request will be for a :method + */ + public function theFirstInteractionRequestWillBeForA(string $method): void + { + Assert::assertSame($method, $this->pact['interactions'][0]['request']['method'] ?? null); + } + + /** + * @Then the {first} interaction response will contain the :fixture document + */ + public function theFirstInteractionResponseWillContainTheDocument(string $fixture): void + { + Assert::assertEquals($this->fixtureLoader->loadJson($fixture), $this->pact['interactions'][0]['response']['body'] ?? null); + } + + /** + * @When the mock server is started with interactions :ids + */ + public function theMockServerIsStartedWithInteractions(string $ids): void + { + $ids = array_map(fn (string $id) => (int) trim($id), explode(',', $ids)); + $this->server->register(...$ids); + } + + /** + * @Then the mock server status will NOT be OK + */ + public function theMockServerStatusWillNotBeOk(): void + { + Assert::assertFalse($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then the mock server will NOT write out a Pact file for the interactions when done + */ + public function theMockServerWillNotWriteOutAPactFileForTheInteractionsWhenDone(): void + { + Assert::assertFileDoesNotExist($this->server->getPactPath()); + } + + /** + * @Then the mock server status will be an expected but not received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnExpectedButNotReceivedErrorForInteraction(int $id): void + { + $request = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getRequest(); + $mismatches = $this->getMismatches(); + Assert::assertCount(1, $mismatches); + $mismatch = current($mismatches); + Assert::assertSame('missing-request', $mismatch['type']); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + Assert::assertSame($request->getQuery(), $mismatch['request']['query']); + // TODO assert headers, body + } + + /** + * @Then a :code error response is returned + */ + public function aErrorResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->client->getResponse()->getStatusCode()); + } + + /** + * @Then the mock server status will be an unexpected :method request received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnUnexpectedRequestReceivedErrorForInteraction(string $method, int $id): void + { + $request = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getRequest(); + $mismatches = $this->getMismatches(); + Assert::assertCount(2, $mismatches); + $notFoundRequests = array_filter($mismatches, fn (array $mismatch) => $mismatch['type'] === 'request-not-found'); + $mismatch = current($notFoundRequests); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + // TODO assert query, headers, body + } + + /** + * @Then the {first} interaction request query parameters will be :query + */ + public function theFirstInteractionRequestQueryParametersWillBe(string $query) + { + Assert::assertEquals($query, $this->pact['interactions'][0]['request']['query']); + } + + /** + * @When request :id is made to the mock server with the following changes: + */ + public function requestIsMadeToTheMockServerWithTheFollowingChanges(int $id, TableNode $table) + { + $request = $this->storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $id)->getRequest(); + $this->requestBuilder->build($request, $table->getHash()[0]); + $this->requestIsMadeToTheMockServer($id); + } + + /** + * @Then the mock server status will be mismatches + */ + public function theMockServerStatusWillBeMismatches(): void + { + $mismatches = $this->getMismatches(); + Assert::assertNotEmpty($mismatches); + } + + /** + * @Then the mismatches will contain a :type mismatch with error :error + */ + public function theMismatchesWillContainAMismatchWithError(string $type, string $error): void + { + $mismatches = $this->getMismatches(); + $mismatch = current($mismatches); + Assert::assertSame('request-mismatch', $mismatch['type']); + $mismatches = array_filter( + $mismatch['mismatches'], + fn (array $mismatch) => $mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$type] + && str_contains($mismatch['mismatch'], $error) + ); + Assert::assertNotEmpty($mismatches); + } + + /** + * @Then the mock server will NOT write out a Pact file for the interaction when done + */ + public function theMockServerWillNotWriteOutAPactFileForTheInteractionWhenDone(): void + { + Assert::assertFileDoesNotExist($this->server->getPactPath()); + } + + /** + * @Then the mock server status will be an unexpected :method request received error for path :path + */ + public function theMockServerStatusWillBeAnUnexpectedRequestReceivedErrorForPath(string $method, string $path): void + { + $mismatches = $this->getMismatches(); + Assert::assertCount(2, $mismatches); + $notFoundRequests = array_filter($mismatches, fn (array $mismatch) => $mismatch['type'] === 'request-not-found'); + $mismatch = current($notFoundRequests); + Assert::assertSame($method, $mismatch['request']['method']); + Assert::assertSame($path, $mismatch['request']['path']); + } + + /** + * @Then the {first} interaction request will contain the header :header with value :value + */ + public function theFirstInteractionRequestWillContainTheHeaderWithValue(string $header, string $value): void + { + Assert::assertArrayHasKey($header, $this->pact['interactions'][0]['request']['headers']); + Assert::assertSame($value, $this->pact['interactions'][0]['request']['headers'][$header]); + } + + /** + * @Then the {first} interaction request content type will be :contentType + */ + public function theFirstInteractionRequestContentTypeWillBe(string $contentType): void + { + Assert::assertSame($contentType, $this->pact['interactions'][0]['request']['headers']['Content-Type']); + } + + /** + * @Then the {first} interaction request will contain the :fixture document + */ + public function theFirstInteractionRequestWillContainTheDocument(string $fixture): void + { + Assert::assertEquals($this->fixtureLoader->loadJson($fixture), $this->pact['interactions'][0]['request']['body'] ?? null); + } + + /** + * @Then the mismatches will contain a :type mismatch with path :path with error :error + */ + public function theMismatchesWillContainAMismatchWithPathWithError(string $type, string $path, string $error): void + { + $mismatches = $this->getMismatches(); + $mismatch = current($mismatches); + Assert::assertSame('request-mismatch', $mismatch['type']); + $mismatches = array_filter( + $mismatch['mismatches'], + fn (array $mismatch) => $mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$type] + && $mismatch['path'] === $path + && str_contains($mismatch['mismatch'], $error) + ); + Assert::assertNotEmpty($mismatches); + } + + private function getMismatches(): array + { + if ($this->server->getVerifyResult()->isSuccess()) { + return []; + } + + return json_decode($this->server->getVerifyResult()->getOutput(), true); + } +} diff --git a/compatibility-suite/tests/Service/Client.php b/compatibility-suite/tests/Service/Client.php new file mode 100644 index 00000000..ae1e2930 --- /dev/null +++ b/compatibility-suite/tests/Service/Client.php @@ -0,0 +1,28 @@ +storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $id)->getRequest(); + $this->response = $this->httpClient->sendRequest($request, $this->server->getBaseUri()); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/compatibility-suite/tests/Service/ClientInterface.php b/compatibility-suite/tests/Service/ClientInterface.php new file mode 100644 index 00000000..75a7523b --- /dev/null +++ b/compatibility-suite/tests/Service/ClientInterface.php @@ -0,0 +1,12 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $this->config = new MockServerConfig(); + $this->config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($specificationVersion) + ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); + + $this->logger = new Logger(); + + $client = new Client(); + $pactRegistry = new PactRegistry($client); + $this->pactDriver = new PactDriver($client, $this->config, $pactRegistry); + $this->mockServer = new MockServer($client, $pactRegistry, $this->config, $this->logger); + $this->interactionRegistry = new InteractionRegistry($client, $pactRegistry); + } + + public function register(int ...$ids): void + { + $interactions = array_map(fn (int $id) => $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id), $ids); + $this->pactDriver->setUp(); + foreach ($interactions as $interaction) { + $this->interactionRegistry->registerInteraction($interaction); + } + $this->mockServer->start(); + } + + public function getBaseUri(): UriInterface + { + return $this->config->getBaseUri(); + } + + public function verify(): void + { + $success = $this->mockServer->verify(); + $this->verifyResult = new VerifyResult($success, !$success ? $this->logger->getOutput() : ''); + } + + public function getVerifyResult(): VerifyResult + { + if (!isset($this->verifyResult)) { + $this->verify(); + } + + return $this->verifyResult; + } + + public function getPactPath(): string + { + return $this->pactPath; + } + + public function getPort(): int + { + return $this->config->getPort(); + } +} diff --git a/compatibility-suite/tests/Service/ServerInterface.php b/compatibility-suite/tests/Service/ServerInterface.php new file mode 100644 index 00000000..2015caf6 --- /dev/null +++ b/compatibility-suite/tests/Service/ServerInterface.php @@ -0,0 +1,21 @@ + Date: Fri, 22 Dec 2023 09:07:42 +0700 Subject: [PATCH 172/298] test(compatibility-suite): Implement V1 Provider scenarios --- .github/workflows/build.yml | 2 +- .github/workflows/compatibility-suite.yml | 24 +++ behat.yml | 3 + .../public/provider-states/.gitignore | 1 + .../public/provider-states/index.php | 68 ++++++++ .../suites/v1/http/consumer.yml | 23 +++ .../suites/v1/http/provider.yml | 32 ++++ .../Context/Shared/Hook/PactBrokerContext.php | 36 +++++ .../Shared/Hook/ProviderStateContext.php | 36 +++++ .../tests/Context/Shared/ProviderContext.php | 145 +++++++++++++++++ .../tests/Context/V1/Http/ProviderContext.php | 150 ++++++++++++++++++ compatibility-suite/tests/Model/Logger.php | 20 +++ .../tests/Model/VerifyResult.php | 20 +++ .../tests/Service/HttpClient.php | 52 ++++++ .../tests/Service/HttpClientInterface.php | 12 ++ .../tests/Service/PactBroker.php | 57 +++++++ .../tests/Service/PactBrokerInterface.php | 14 ++ .../tests/Service/PactWriter.php | 50 ++++++ .../tests/Service/PactWriterInterface.php | 12 ++ .../tests/Service/ProviderStateServer.php | 43 +++++ .../Service/ProviderStateServerInterface.php | 19 +++ .../tests/Service/ProviderVerifier.php | 64 ++++++++ .../Service/ProviderVerifierInterface.php | 18 +++ .../AbstractServiceContainer.php | 35 ++++ .../tests/ServiceContainer/V1.php | 53 +++++++ 25 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/compatibility-suite.yml create mode 100644 compatibility-suite/public/provider-states/.gitignore create mode 100644 compatibility-suite/public/provider-states/index.php create mode 100644 compatibility-suite/suites/v1/http/consumer.yml create mode 100644 compatibility-suite/suites/v1/http/provider.yml create mode 100644 compatibility-suite/tests/Context/Shared/Hook/PactBrokerContext.php create mode 100644 compatibility-suite/tests/Context/Shared/Hook/ProviderStateContext.php create mode 100644 compatibility-suite/tests/Context/Shared/ProviderContext.php create mode 100644 compatibility-suite/tests/Context/V1/Http/ProviderContext.php create mode 100644 compatibility-suite/tests/Model/Logger.php create mode 100644 compatibility-suite/tests/Model/VerifyResult.php create mode 100644 compatibility-suite/tests/Service/HttpClient.php create mode 100644 compatibility-suite/tests/Service/HttpClientInterface.php create mode 100644 compatibility-suite/tests/Service/PactBroker.php create mode 100644 compatibility-suite/tests/Service/PactBrokerInterface.php create mode 100644 compatibility-suite/tests/Service/PactWriter.php create mode 100644 compatibility-suite/tests/Service/PactWriterInterface.php create mode 100644 compatibility-suite/tests/Service/ProviderStateServer.php create mode 100644 compatibility-suite/tests/Service/ProviderStateServerInterface.php create mode 100644 compatibility-suite/tests/Service/ProviderVerifier.php create mode 100644 compatibility-suite/tests/Service/ProviderVerifierInterface.php create mode 100644 compatibility-suite/tests/ServiceContainer/AbstractServiceContainer.php create mode 100644 compatibility-suite/tests/ServiceContainer/V1.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2239e2c3..a7ebeac3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: pact-php +name: Pact-PHP Code Analysis & Test on: push: diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml new file mode 100644 index 00000000..366c7077 --- /dev/null +++ b/.github/workflows/compatibility-suite.yml @@ -0,0 +1,24 @@ +name: Pact-PHP Compatibility Suite + +on: [push, pull_request] + +env: + pact_do_not_track: true + +jobs: + v1: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + + - uses: ramsey/composer-install@v2 + + - name: Run Behat + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V1 diff --git a/behat.yml b/behat.yml index e69de29b..6ff7b709 100644 --- a/behat.yml +++ b/behat.yml @@ -0,0 +1,3 @@ +imports: + - 'compatibility-suite/suites/v1/http/consumer.yml' + - 'compatibility-suite/suites/v1/http/provider.yml' diff --git a/compatibility-suite/public/provider-states/.gitignore b/compatibility-suite/public/provider-states/.gitignore new file mode 100644 index 00000000..a6c57f5f --- /dev/null +++ b/compatibility-suite/public/provider-states/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/compatibility-suite/public/provider-states/index.php b/compatibility-suite/public/provider-states/index.php new file mode 100644 index 00000000..b7de797b --- /dev/null +++ b/compatibility-suite/public/provider-states/index.php @@ -0,0 +1,68 @@ +addBodyParsingMiddleware(); + +$path = __DIR__ . '/provider-states.json'; +$get = fn (): array => json_decode(file_get_contents($path), true); +$set = fn (array $providerStates) => file_put_contents($path, json_encode($providerStates)); + +if (!file_exists($path)) { + $set([]); +} + +$stateChangeHandler = function (Request $request, Response $response) use ($get, $set) { + $body = $request->getParsedBody(); + + $providerStates = $get(); + $providerStates[] = $body; + $set($providerStates); + + return $response; +}; + +$app->get('/has-action', function (Request $request, Response $response) use ($get) { + $action = $request->getQueryParams()['action']; + $hasAction = !empty(array_filter( + $get(), + fn (array $providerState) => $providerState['action'] === $action + )); + + $response->getBody()->write((string) $hasAction); + + return $response->withHeader('Content-Type', 'text/plain'); +}); + +$app->get('/has-state', function (Request $request, Response $response) use ($get) { + $params = $request->getQueryParams(); + $action = $params['action']; + $state = $params['state']; + unset($params['action'], $params['state']); + $hasState = !empty(array_filter( + $get(), + fn (array $providerState) => + $providerState['action'] === $action + && $providerState['state'] === $state + && $providerState['params'] == $params + )); + + $response->getBody()->write((string) $hasState); + + return $response->withHeader('Content-Type', 'text/plain'); +}); + +$app->post('/pact-change-state', $stateChangeHandler); + +$app->post('/failed-pact-change-state', function (Request $request, Response $response) use ($stateChangeHandler) { + $stateChangeHandler($request, $response); + + throw new \Exception('Cant do it'); +}); + +$app->run(); diff --git a/compatibility-suite/suites/v1/http/consumer.yml b/compatibility-suite/suites/v1/http/consumer.yml new file mode 100644 index 00000000..87dfeb31 --- /dev/null +++ b/compatibility-suite/suites/v1/http/consumer.yml @@ -0,0 +1,23 @@ +default: + suites: + v1_http_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V1/http_consumer.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ConsumerContext': + - '@server' + - '@request_builder' + - '@client' + - '@interactions_storage' + - '@fixture_loader' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V1 diff --git a/compatibility-suite/suites/v1/http/provider.yml b/compatibility-suite/suites/v1/http/provider.yml new file mode 100644 index 00000000..1e89c55f --- /dev/null +++ b/compatibility-suite/suites/v1/http/provider.yml @@ -0,0 +1,32 @@ +default: + suites: + v1_http_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V1/http_provider.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\PactBrokerContext': + - '@pact_broker' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\ProviderStateContext': + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ProviderContext': + - '@server' + - '@provider_verifier' + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ProviderContext': + - '@server' + - '@pact_writer' + - '@pact_broker' + - '@response_builder' + - '@interactions_storage' + - '@provider_verifier' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V1 diff --git a/compatibility-suite/tests/Context/Shared/Hook/PactBrokerContext.php b/compatibility-suite/tests/Context/Shared/Hook/PactBrokerContext.php new file mode 100644 index 00000000..13580025 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/Hook/PactBrokerContext.php @@ -0,0 +1,36 @@ +getScenario()->getTitle(), 'via a Pact broker')) { + $this->pactBroker->start(); + } + } + + /** + * @AfterScenario + */ + public function stopPactBroker(AfterScenarioScope $scope): void + { + if (str_contains($scope->getScenario()->getTitle(), 'via a Pact broker')) { + $this->pactBroker->stop(); + } + } +} diff --git a/compatibility-suite/tests/Context/Shared/Hook/ProviderStateContext.php b/compatibility-suite/tests/Context/Shared/Hook/ProviderStateContext.php new file mode 100644 index 00000000..c661a0d6 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/Hook/ProviderStateContext.php @@ -0,0 +1,36 @@ +getScenario()->getTitle())) { + $this->providerStateServer->start(); + } + } + + /** + * @AfterScenario + */ + public function stopProviderState(AfterScenarioScope $scope): void + { + if (preg_match('/^Verifying .* provider state/', $scope->getScenario()->getTitle())) { + $this->providerStateServer->stop(); + } + } +} diff --git a/compatibility-suite/tests/Context/Shared/ProviderContext.php b/compatibility-suite/tests/Context/Shared/ProviderContext.php new file mode 100644 index 00000000..c0440460 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/ProviderContext.php @@ -0,0 +1,145 @@ +providerVerifier->getConfig()->getProviderInfo()->setPort($this->server->getPort()); + $this->providerVerifier->verify(); + } + + /** + * @Then the verification will be successful + */ + public function theVerificationWillBeSuccessful(): void + { + Assert::assertTrue($this->providerVerifier->getVerifyResult()->isSuccess()); + } + + /** + * @Then the verification will NOT be successful + */ + public function theVerificationWillNotBeSuccessful(): void + { + Assert::assertFalse($this->providerVerifier->getVerifyResult()->isSuccess()); + } + + /** + * @Given a provider state callback is configured + */ + public function aProviderStateCallbackIsConfigured(): void + { + $port = $this->providerStateServer->getPort(); + $this->providerVerifier + ->getConfig() + ->getProviderState() + ->setStateChangeUrl(new Uri("http://localhost:$port/pact-change-state")) + ->setStateChangeTeardown(true); + ; + } + + /** + * @Then the provider state callback will be called before the verification is run + */ + public function theProviderStateCallbackWillBeCalledBeforeTheVerificationIsRun(): void + { + Assert::assertTrue($this->providerStateServer->hasAction(ProviderStateServerInterface::ACTION_SETUP)); + } + + /** + * @Then the provider state callback will receive a setup call with :state as the provider state parameter + */ + public function theProviderStateCallbackWillReceiveASetupCallWithAsTheProviderStateParameter(string $state): void + { + Assert::assertTrue($this->providerStateServer->hasState(ProviderStateServerInterface::ACTION_SETUP, $state)); + } + + /** + * @Then the provider state callback will be called after the verification is run + */ + public function theProviderStateCallbackWillBeCalledAfterTheVerificationIsRun(): void + { + Assert::assertTrue($this->providerStateServer->hasAction(ProviderStateServerInterface::ACTION_TEARDOWN)); + } + + /** + * @Then the provider state callback will receive a teardown call :state as the provider state parameter + */ + public function theProviderStateCallbackWillReceiveATeardownCallAsTheProviderStateParameter(string $state): void + { + Assert::assertTrue($this->providerStateServer->hasState(ProviderStateServerInterface::ACTION_TEARDOWN, $state)); + } + + /** + * @Given a provider state callback is configured, but will return a failure + */ + public function aProviderStateCallbackIsConfiguredButWillReturnAFailure(): void + { + $port = $this->providerStateServer->getPort(); + $this->providerVerifier + ->getConfig() + ->getProviderState() + ->setStateChangeUrl(new Uri("http://localhost:$port/failed-pact-change-state")) + ->setStateChangeTeardown(true); + ; + } + + /** + * @Then the provider state callback will NOT receive a teardown call + */ + public function theProviderStateCallbackWillNotReceiveATeardownCall(): void + { + Assert::assertFalse($this->providerStateServer->hasAction(ProviderStateServerInterface::ACTION_TEARDOWN)); + } + + /** + * @Then the verification results will contain a :error error + */ + public function theVerificationResultsWillContainAError(string $error): void + { + $output = json_decode($this->providerVerifier->getVerifyResult()->getOutput(), true); + $errors = array_reduce( + $output['errors'], + function (array $errors, array $error) { + switch ($error['mismatch']['type']) { + case 'error': + $errors[] = Mismatch::VERIFIER_MISMATCH_ERROR_MAP[$error['mismatch']['message']]; + break; + + case 'mismatches': + foreach ($error['mismatch']['mismatches'] as $mismatch) { + $errors[] = Mismatch::VERIFIER_MISMATCH_TYPE_MAP[$mismatch['type']]; + } + break; + + default: + break; + } + + return $errors; + }, + [] + ); + Assert::assertContains($error, $errors); + } +} diff --git a/compatibility-suite/tests/Context/V1/Http/ProviderContext.php b/compatibility-suite/tests/Context/V1/Http/ProviderContext.php new file mode 100644 index 00000000..89e3ad78 --- /dev/null +++ b/compatibility-suite/tests/Context/V1/Http/ProviderContext.php @@ -0,0 +1,150 @@ +server->register($id); + } + + /** + * @Given a Pact file for interaction :id is to be verified + */ + public function aPactFileForInteractionIsToBeVerified(int $id): void + { + $this->pactWriter->write($id, "c-$id"); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Given a provider is started that returns the responses from interactions :ids + */ + public function aProviderIsStartedThatReturnsTheResponsesFromInteractions(string $ids): void + { + $ids = array_map(fn (string $id) => (int) trim($id), explode(',', $ids)); + $this->server->register(...$ids); + } + + /** + * @Given a Pact file for interaction :id is to be verified from a Pact broker + */ + public function aPactFileForInteractionIsToBeVerifiedFromAPactBroker(int $id): void + { + $this->pactWriter->write($id, "c-$id"); + $this->pactBroker->publish($id); + $broker = new Broker(); + $broker->setUrl(new Uri('http:/localhost:9292')); + $this->providerVerifier->addSource($broker); + } + + /** + * @Then a verification result will NOT be published back + */ + public function aVerificationResultWillNotBePublishedBack(): void + { + Assert::assertSame(1, $this->pactBroker->getMatrix()['summary']['unknown']); + } + + /** + * @Given publishing of verification results is enabled + */ + public function publishingOfVerificationResultsIsEnabled(): void + { + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderVersion('1.2.3') + ; + $this->providerVerifier->getConfig()->setPublishOptions($publishOptions); + } + + /** + * @Then a successful verification result will be published back for interaction {:id} + */ + public function aSuccessfulVerificationResultWillBePublishedBackForInteraction(int $id): void + { + Assert::assertSame(1, $this->pactBroker->getMatrix()['summary']['success']); + } + + /** + * @Then a failed verification result will be published back for the interaction {:id} + */ + public function aFailedVerificationResultWillBePublishedBackForTheInteraction(int $id): void + { + Assert::assertSame(1, $this->pactBroker->getMatrix()['summary']['failed']); + } + + /** + * @Given a Pact file for interaction :id is to be verified with a provider state :state defined + */ + public function aPactFileForInteractionIsToBeVerifiedWithAProviderStateDefined(int $id, string $state): void + { + $this->pactWriter->write($id, "c-$id"); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['interactions'][0]['providerStates'][] = ['name' => $state]; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Then a warning will be displayed that there was no provider state callback configured for provider state :state + */ + public function aWarningWillBeDisplayedThatThereWasNoProviderStateCallbackConfiguredForProviderState(string $state): void + { + throw new PendingException("Unable to verify this, as I can't find a way to assert this message from verifier's log: 'pact_verifier::callback_executors: State Change ignored as there is no state change URL provided for interaction'"); + } + + /** + * @Given a request filter is configured to make the following changes: + */ + public function aRequestFilterIsConfiguredToMakeTheFollowingChanges(TableNode $table): void + { + throw new PendingException("Unable to set request filter callback from ffi"); + } + + /** + * @Then the request to the provider will contain the header :header + */ + public function theRequestToTheProviderWillContainTheHeader(string $header): void + { + throw new PendingException('Unable to set request filter callback from ffi, so no need to implement this step'); + } + + /** + * @Given a provider is started that returns the response from interaction :id, with the following changes: + */ + public function aProviderIsStartedThatReturnsTheResponseFromInteractionWithTheFollowingChanges(int $id, TableNode $table): void + { + $response = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getResponse(); + $this->responseBuilder->build($response, $table->getHash()[0]); + $this->server->register($id); + } +} diff --git a/compatibility-suite/tests/Model/Logger.php b/compatibility-suite/tests/Model/Logger.php new file mode 100644 index 00000000..df437bd5 --- /dev/null +++ b/compatibility-suite/tests/Model/Logger.php @@ -0,0 +1,20 @@ +output = $output; + } + + public function getOutput(): string + { + return $this->output; + } +} diff --git a/compatibility-suite/tests/Model/VerifyResult.php b/compatibility-suite/tests/Model/VerifyResult.php new file mode 100644 index 00000000..899c93f8 --- /dev/null +++ b/compatibility-suite/tests/Model/VerifyResult.php @@ -0,0 +1,20 @@ +success; + } + + public function getOutput(): string + { + return $this->output; + } +} diff --git a/compatibility-suite/tests/Service/HttpClient.php b/compatibility-suite/tests/Service/HttpClient.php new file mode 100644 index 00000000..4e23d64c --- /dev/null +++ b/compatibility-suite/tests/Service/HttpClient.php @@ -0,0 +1,52 @@ +client = new Client(); + } + + public function sendRequest(ConsumerRequest $request, UriInterface $uri): ResponseInterface + { + $options = []; + $options['query'] = $this->formatQueryString($request->getQuery()); + $options['headers'] = $request->getHeaders(); + $body = $request->getBody(); + if ($body instanceof Text || $body instanceof Binary) { + $options['body'] = match (true) { + $body instanceof Text => $body->getContents(), + $body instanceof Binary => file_get_contents($body->getPath()), + }; + $options['headers']['Content-Type'] = $body->getContentType(); + } + $options['http_errors'] = false; + + return $this->client->request($request->getMethod(), $uri->withPath($request->getPath()), $options); + } + + private function formatQueryString(array $query): string + { + $result = []; + + foreach($query as $key => $values) { + foreach ($values as $value) { + $result[] = urlencode($key) . '=' . urlencode($value); + } + } + + return implode('&', $result); + } +} diff --git a/compatibility-suite/tests/Service/HttpClientInterface.php b/compatibility-suite/tests/Service/HttpClientInterface.php new file mode 100644 index 00000000..b2c2057b --- /dev/null +++ b/compatibility-suite/tests/Service/HttpClientInterface.php @@ -0,0 +1,12 @@ +client = new Client(); + } + + public function publish(int $id): void + { + $this->consumer = "c-$id"; + $this->client->put("http://localhost:9292/pacts/provider/$this->provider/consumer/$this->consumer/version/1.0.0", [ + 'body' => file_get_contents(__DIR__."/../../pacts/$this->consumer-$this->provider.json"), + 'headers' => ['Content-Type' => 'application/json'], + ]); + } + + public function start(): void + { + exec('docker run --rm --publish 9292:9292 --detach --env PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 --name pact-broker pactfoundation/pact-broker:latest'); + while (true) { + try { + $response = $this->client->get('http://localhost:9292/diagnostic/status/heartbeat', ['http_errors' => false]); + if ($response->getStatusCode() !== 200) { + continue; + } + $status = json_decode($response->getBody(), true); + if ($status['ok']) { + break; + } + } catch (\Throwable) { + } finally { + sleep(1); + } + } + } + + public function stop(): void + { + exec('docker stop pact-broker'); + sleep(1); + } + + public function getMatrix(): array + { + return json_decode(file_get_contents("http://localhost:9292/matrix.json?q[][pacticipant]=$this->consumer&q[][pacticipant]=$this->provider"), true); + } +} diff --git a/compatibility-suite/tests/Service/PactBrokerInterface.php b/compatibility-suite/tests/Service/PactBrokerInterface.php new file mode 100644 index 00000000..674ed062 --- /dev/null +++ b/compatibility-suite/tests/Service/PactBrokerInterface.php @@ -0,0 +1,14 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $config = new MockServerConfig(); + $config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($this->specificationVersion) + ->setPactFileWriteMode($mode); + $client = new Client(); + $pactRegistry = new PactRegistry($client); + $pactDriver = new PactDriver($client, $config, $pactRegistry); + $interactionRegistry = new InteractionRegistry($client, $pactRegistry); + + $interaction = $this->storage->get(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id); + $pactDriver->setUp(); + $interactionRegistry->registerInteraction($interaction); + $pactDriver->writePact(); + $pactDriver->cleanUp(); + } + + public function getPactPath(): string + { + return $this->pactPath; + } +} diff --git a/compatibility-suite/tests/Service/PactWriterInterface.php b/compatibility-suite/tests/Service/PactWriterInterface.php new file mode 100644 index 00000000..90b0dbb4 --- /dev/null +++ b/compatibility-suite/tests/Service/PactWriterInterface.php @@ -0,0 +1,12 @@ +port); + \socket_getsockname($socket, $addr, $this->port); + \socket_close($socket); + $this->process = new ProviderProcess(Path::PUBLIC_PATH . '/provider-states/', $this->port); + $this->process->start(); + } + + public function stop(): void + { + $this->port = 0; + $this->process->stop(); + } + + public function getPort(): int + { + return $this->port; + } + + public function hasAction(string $action): bool + { + return file_get_contents("http://localhost:$this->port/has-action?action=" . urlencode($action)); + } + + public function hasState(string $action, string $state, array $params = []): bool + { + return file_get_contents("http://localhost:$this->port/has-state?action=" . urlencode($action) . '&state=' . urlencode($state) . ($params ? ('&' . http_build_query($params)) : '')); + } +} diff --git a/compatibility-suite/tests/Service/ProviderStateServerInterface.php b/compatibility-suite/tests/Service/ProviderStateServerInterface.php new file mode 100644 index 00000000..f2595c01 --- /dev/null +++ b/compatibility-suite/tests/Service/ProviderStateServerInterface.php @@ -0,0 +1,19 @@ +config = new VerifierConfig(); + $this->config + ->getProviderInfo() + ->setName('p') + ->setHost('localhost'); + } + + public function getConfig(): VerifierConfigInterface + { + return $this->config; + } + + public function verify(): void + { + $logger = new Logger(); + $verifier = new Verifier($this->config, $logger); + foreach ($this->sources as $source) { + if ($source instanceof Broker) { + $verifier->addBroker($source); + } else { + $verifier->addFile($source); + } + } + + $success = $verifier->verify(); + $this->verifyResult = new VerifyResult($success, $logger->getOutput()); + } + + public function addSource(string|Broker $source): void + { + if (in_array($source, $this->sources)) { + return; + } + if ($source instanceof Broker) { + $this->sources = array_filter($this->sources, fn (mixed $source) => !$source instanceof Broker); + } + $this->sources[] = $source; + } + + public function getVerifyResult(): VerifyResult + { + return $this->verifyResult; + } +} diff --git a/compatibility-suite/tests/Service/ProviderVerifierInterface.php b/compatibility-suite/tests/Service/ProviderVerifierInterface.php new file mode 100644 index 00000000..1f1f3e83 --- /dev/null +++ b/compatibility-suite/tests/Service/ProviderVerifierInterface.php @@ -0,0 +1,18 @@ +services[$id]); + } + + public function get(string $id) + { + if (!$this->has($id)) { + throw new ServiceNotFoundException( + sprintf('Service `%s` not found.', $id), + $id + ); + } + + return $this->services[$id]; + } + + protected function set(string $id, mixed $service): void + { + $this->services[$id] = $service; + } + + abstract protected function getSpecification(): string; +} diff --git a/compatibility-suite/tests/ServiceContainer/V1.php b/compatibility-suite/tests/ServiceContainer/V1.php new file mode 100644 index 00000000..01003ecf --- /dev/null +++ b/compatibility-suite/tests/ServiceContainer/V1.php @@ -0,0 +1,53 @@ +set('specification', $this->getSpecification()); + $this->set('interactions_storage', new InteractionsStorage()); + $this->set('provider_state_server', new ProviderStateServer()); + $this->set('matching_rule_converter', new MatchingRuleConverter()); + $this->set('matching_rules_storage', new MatchingRulesStorage()); + $this->set('http_client', new HttpClient()); + $this->set('provider_verifier', new ProviderVerifier()); + $this->set('fixture_loader', new FixtureLoader()); + $this->set('parser', new Parser($this->get('fixture_loader'), $this->getSpecification())); + $this->set('pact_broker', new PactBroker($this->getSpecification())); + $this->set('matching_rule_parser', new MatchingRuleParser($this->get('matching_rule_converter'), $this->get('fixture_loader'))); + $this->set('server', new Server($this->getSpecification(), $this->get('interactions_storage'))); + $this->set('request_builder', new RequestBuilder($this->get('parser'))); + $this->set('response_builder', new ResponseBuilder($this->get('parser'))); + $this->set('request_matching_rule_builder', new RequestMatchingRuleBuilder($this->get('matching_rule_parser'), $this->get('matching_rule_converter'))); + $this->set('response_matching_rule_builder', new ResponseMatchingRuleBuilder($this->get('matching_rule_parser'), $this->get('matching_rule_converter'))); + $this->set('interaction_builder', new InteractionBuilder($this->get('request_builder'), $this->get('response_builder'))); + $this->set('client', new Client($this->get('server'), $this->get('interactions_storage'), $this->get('http_client'))); + $this->set('pact_writer', new PactWriter($this->get('interactions_storage'), $this->getSpecification())); + } + + protected function getSpecification(): string + { + return '1.0.0'; + } +} From 6b21ab1a17512640ef37ce403221d6d2a23886a8 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 22 Dec 2023 15:30:48 +0700 Subject: [PATCH 173/298] test(compatibility-suite): Implement V2 scenarios --- .github/workflows/compatibility-suite.yml | 16 +++++++++ behat.yml | 2 ++ .../suites/v2/http/consumer.yml | 29 +++++++++++++++ .../suites/v2/http/provider.yml | 30 ++++++++++++++++ .../tests/Context/V2/Http/ConsumerContext.php | 36 +++++++++++++++++++ .../tests/ServiceContainer/V2.php | 11 ++++++ 6 files changed, 124 insertions(+) create mode 100644 compatibility-suite/suites/v2/http/consumer.yml create mode 100644 compatibility-suite/suites/v2/http/provider.yml create mode 100644 compatibility-suite/tests/Context/V2/Http/ConsumerContext.php create mode 100644 compatibility-suite/tests/ServiceContainer/V2.php diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index 366c7077..38c017ba 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -22,3 +22,19 @@ jobs: - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V1 + v2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + + - uses: ramsey/composer-install@v2 + + - name: Run Behat + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V2 diff --git a/behat.yml b/behat.yml index 6ff7b709..e2cb9df0 100644 --- a/behat.yml +++ b/behat.yml @@ -1,3 +1,5 @@ imports: - 'compatibility-suite/suites/v1/http/consumer.yml' - 'compatibility-suite/suites/v1/http/provider.yml' + - 'compatibility-suite/suites/v2/http/consumer.yml' + - 'compatibility-suite/suites/v2/http/provider.yml' diff --git a/compatibility-suite/suites/v2/http/consumer.yml b/compatibility-suite/suites/v2/http/consumer.yml new file mode 100644 index 00000000..e6872644 --- /dev/null +++ b/compatibility-suite/suites/v2/http/consumer.yml @@ -0,0 +1,29 @@ +default: + suites: + v2_http_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V2/http_consumer.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ConsumerContext': + - '@server' + - '@request_builder' + - '@client' + - '@interactions_storage' + - '@fixture_loader' + - 'PhpPactTest\CompatibilitySuite\Context\V2\Http\ConsumerContext': + - '@server' + - '@request_builder' + - '@request_matching_rule_builder' + - '@matching_rules_storage' + - '@interactions_storage' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V2 diff --git a/compatibility-suite/suites/v2/http/provider.yml b/compatibility-suite/suites/v2/http/provider.yml new file mode 100644 index 00000000..124d6b47 --- /dev/null +++ b/compatibility-suite/suites/v2/http/provider.yml @@ -0,0 +1,30 @@ +default: + suites: + v2_http_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V2/http_provider.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\ProviderStateContext': + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ProviderContext': + - '@server' + - '@provider_verifier' + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ProviderContext': + - '@server' + - '@pact_writer' + - '@pact_broker' + - '@response_builder' + - '@interactions_storage' + - '@provider_verifier' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V2 diff --git a/compatibility-suite/tests/Context/V2/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V2/Http/ConsumerContext.php new file mode 100644 index 00000000..ae9ee49f --- /dev/null +++ b/compatibility-suite/tests/Context/V2/Http/ConsumerContext.php @@ -0,0 +1,36 @@ +storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getRequest(); + $this->requestBuilder->build($request, $table->getHash()[0]); + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { + $this->requestMatchingRuleBuilder->build($request, $file); + } + $this->server->register($id); + } +} diff --git a/compatibility-suite/tests/ServiceContainer/V2.php b/compatibility-suite/tests/ServiceContainer/V2.php new file mode 100644 index 00000000..a4107cc7 --- /dev/null +++ b/compatibility-suite/tests/ServiceContainer/V2.php @@ -0,0 +1,11 @@ + Date: Sat, 23 Dec 2023 07:07:02 +0700 Subject: [PATCH 174/298] test(compatibility-suite): Implement V3 scenarios --- .github/workflows/compatibility-suite.yml | 16 + behat.yml | 6 + .../public/generators/.gitignore | 2 + .../public/generators/index.php | 33 ++ compatibility-suite/suites/v3/generators.yml | 28 ++ .../suites/v3/http/consumer.yml | 13 + .../suites/v3/http/provider.yml | 34 ++ .../suites/v3/matching-rules.yml | 18 + .../suites/v3/message/consumer.yml | 16 + .../suites/v3/message/provider.yml | 26 ++ .../Context/V3/BodyGeneratorsContext.php | 22 ++ .../tests/Context/V3/Http/ConsumerContext.php | 99 ++++++ .../tests/Context/V3/Http/ProviderContext.php | 57 ++++ .../Context/V3/Message/ConsumerContext.php | 307 ++++++++++++++++++ .../Context/V3/Message/ProviderContext.php | 166 ++++++++++ .../Context/V3/RequestGeneratorsContext.php | 123 +++++++ .../Context/V3/RequestMatchingContext.php | 169 ++++++++++ .../Context/V3/ResponseGeneratorsContext.php | 86 +++++ compatibility-suite/tests/Model/Generator.php | 34 ++ compatibility-suite/tests/Model/Message.php | 42 +++ .../tests/Service/BodyStorage.php | 18 + .../tests/Service/BodyStorageInterface.php | 10 + .../tests/Service/BodyValidator.php | 52 +++ .../tests/Service/BodyValidatorInterface.php | 10 + .../tests/Service/GeneratorConverter.php | 21 ++ .../Service/GeneratorConverterInterface.php | 11 + .../tests/Service/GeneratorParser.php | 52 +++ .../Service/GeneratorParserInterface.php | 13 + .../tests/Service/GeneratorServer.php | 63 ++++ .../Service/GeneratorServerInterface.php | 20 ++ .../tests/Service/MessageGeneratorBuilder.php | 44 +++ .../MessageGeneratorBuilderInterface.php | 10 + .../tests/Service/MessagePactWriter.php | 45 +++ .../Service/MessagePactWriterInterface.php | 12 + compatibility-suite/tests/Service/Parser.php | 29 ++ .../tests/Service/ParserInterface.php | 6 + .../tests/Service/RequestGeneratorBuilder.php | 58 ++++ .../RequestGeneratorBuilderInterface.php | 10 + .../Service/ResponseGeneratorBuilder.php | 48 +++ .../ResponseGeneratorBuilderInterface.php | 10 + .../tests/ServiceContainer/V3.php | 35 ++ composer.json | 3 +- 42 files changed, 1876 insertions(+), 1 deletion(-) create mode 100644 compatibility-suite/public/generators/.gitignore create mode 100644 compatibility-suite/public/generators/index.php create mode 100644 compatibility-suite/suites/v3/generators.yml create mode 100644 compatibility-suite/suites/v3/http/consumer.yml create mode 100644 compatibility-suite/suites/v3/http/provider.yml create mode 100644 compatibility-suite/suites/v3/matching-rules.yml create mode 100644 compatibility-suite/suites/v3/message/consumer.yml create mode 100644 compatibility-suite/suites/v3/message/provider.yml create mode 100644 compatibility-suite/tests/Context/V3/BodyGeneratorsContext.php create mode 100644 compatibility-suite/tests/Context/V3/Http/ConsumerContext.php create mode 100644 compatibility-suite/tests/Context/V3/Http/ProviderContext.php create mode 100644 compatibility-suite/tests/Context/V3/Message/ConsumerContext.php create mode 100644 compatibility-suite/tests/Context/V3/Message/ProviderContext.php create mode 100644 compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php create mode 100644 compatibility-suite/tests/Context/V3/RequestMatchingContext.php create mode 100644 compatibility-suite/tests/Context/V3/ResponseGeneratorsContext.php create mode 100644 compatibility-suite/tests/Model/Generator.php create mode 100644 compatibility-suite/tests/Model/Message.php create mode 100644 compatibility-suite/tests/Service/BodyStorage.php create mode 100644 compatibility-suite/tests/Service/BodyStorageInterface.php create mode 100644 compatibility-suite/tests/Service/BodyValidator.php create mode 100644 compatibility-suite/tests/Service/BodyValidatorInterface.php create mode 100644 compatibility-suite/tests/Service/GeneratorConverter.php create mode 100644 compatibility-suite/tests/Service/GeneratorConverterInterface.php create mode 100644 compatibility-suite/tests/Service/GeneratorParser.php create mode 100644 compatibility-suite/tests/Service/GeneratorParserInterface.php create mode 100644 compatibility-suite/tests/Service/GeneratorServer.php create mode 100644 compatibility-suite/tests/Service/GeneratorServerInterface.php create mode 100644 compatibility-suite/tests/Service/MessageGeneratorBuilder.php create mode 100644 compatibility-suite/tests/Service/MessageGeneratorBuilderInterface.php create mode 100644 compatibility-suite/tests/Service/MessagePactWriter.php create mode 100644 compatibility-suite/tests/Service/MessagePactWriterInterface.php create mode 100644 compatibility-suite/tests/Service/RequestGeneratorBuilder.php create mode 100644 compatibility-suite/tests/Service/RequestGeneratorBuilderInterface.php create mode 100644 compatibility-suite/tests/Service/ResponseGeneratorBuilder.php create mode 100644 compatibility-suite/tests/Service/ResponseGeneratorBuilderInterface.php create mode 100644 compatibility-suite/tests/ServiceContainer/V3.php diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index 38c017ba..4a44444b 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -38,3 +38,19 @@ jobs: - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V2 + v3: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + + - uses: ramsey/composer-install@v2 + + - name: Run Behat + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 diff --git a/behat.yml b/behat.yml index e2cb9df0..56830032 100644 --- a/behat.yml +++ b/behat.yml @@ -3,3 +3,9 @@ imports: - 'compatibility-suite/suites/v1/http/provider.yml' - 'compatibility-suite/suites/v2/http/consumer.yml' - 'compatibility-suite/suites/v2/http/provider.yml' + - 'compatibility-suite/suites/v3/http/consumer.yml' + - 'compatibility-suite/suites/v3/http/provider.yml' + - 'compatibility-suite/suites/v3/message/consumer.yml' + - 'compatibility-suite/suites/v3/message/provider.yml' + - 'compatibility-suite/suites/v3/generators.yml' + - 'compatibility-suite/suites/v3/matching-rules.yml' diff --git a/compatibility-suite/public/generators/.gitignore b/compatibility-suite/public/generators/.gitignore new file mode 100644 index 00000000..35340fb5 --- /dev/null +++ b/compatibility-suite/public/generators/.gitignore @@ -0,0 +1,2 @@ +*.json +*.txt diff --git a/compatibility-suite/public/generators/index.php b/compatibility-suite/public/generators/index.php new file mode 100644 index 00000000..f74b84e2 --- /dev/null +++ b/compatibility-suite/public/generators/index.php @@ -0,0 +1,33 @@ +put('/request-generators', function (Request $request, Response $response) { + file_put_contents(__DIR__ . '/body.json', $request->getBody()->getContents()); + file_put_contents(__DIR__ . '/headers.json', json_encode($request->getHeaders())); + file_put_contents(__DIR__ . '/queryParams.json', json_encode($request->getQueryParams())); + + return $response; +}); + +$app->post('/return-provider-state-values', function (Request $request, Response $response) { + $values = $request->getQueryParams(); + + $response->getBody()->write(json_encode($values)); + + return $response->withHeader('Content-Type', 'application/json'); +}); + +$app->any('{path:.*}', function ($request, $response, array $args) { + file_put_contents(__DIR__ . '/path.txt', $args['path']); + + return $response; +}); + +$app->run(); diff --git a/compatibility-suite/suites/v3/generators.yml b/compatibility-suite/suites/v3/generators.yml new file mode 100644 index 00000000..bab7c3b3 --- /dev/null +++ b/compatibility-suite/suites/v3/generators.yml @@ -0,0 +1,28 @@ +default: + suites: + v3_generators: + paths: + - '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/generators.feature' + - '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/http_generators.feature' + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\BodyGeneratorsContext': + - '@body_validator' + - 'PhpPactTest\CompatibilitySuite\Context\V3\RequestGeneratorsContext': + - '@interaction_builder' + - '@request_generator_builder' + - '@interactions_storage' + - '@pact_writer' + - '@generator_server' + - '@provider_verifier' + - '@body_storage' + - 'PhpPactTest\CompatibilitySuite\Context\V3\ResponseGeneratorsContext': + - '@interaction_builder' + - '@response_generator_builder' + - '@interactions_storage' + - '@server' + - '@client' + - '@body_storage' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 diff --git a/compatibility-suite/suites/v3/http/consumer.yml b/compatibility-suite/suites/v3/http/consumer.yml new file mode 100644 index 00000000..e164ed80 --- /dev/null +++ b/compatibility-suite/suites/v3/http/consumer.yml @@ -0,0 +1,13 @@ +default: + suites: + v3_http_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/http_consumer.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\Http\ConsumerContext': + - '@interaction_builder' + - '@pact_writer' + - '@interactions_storage' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 diff --git a/compatibility-suite/suites/v3/http/provider.yml b/compatibility-suite/suites/v3/http/provider.yml new file mode 100644 index 00000000..7d8c0111 --- /dev/null +++ b/compatibility-suite/suites/v3/http/provider.yml @@ -0,0 +1,34 @@ +default: + suites: + v3_http_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/http_provider.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\ProviderStateContext': + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ProviderContext': + - '@server' + - '@provider_verifier' + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ProviderContext': + - '@server' + - '@pact_writer' + - '@pact_broker' + - '@response_builder' + - '@interactions_storage' + - '@provider_verifier' + - 'PhpPactTest\CompatibilitySuite\Context\V3\Http\ProviderContext': + - '@pact_writer' + - '@provider_state_server' + - '@provider_verifier' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 diff --git a/compatibility-suite/suites/v3/matching-rules.yml b/compatibility-suite/suites/v3/matching-rules.yml new file mode 100644 index 00000000..090cb654 --- /dev/null +++ b/compatibility-suite/suites/v3/matching-rules.yml @@ -0,0 +1,18 @@ +default: + suites: + v3_matching_rules: + paths: + - '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/matching_rules.feature' + - '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/http_matching.feature' + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\RequestMatchingContext': + - '@interaction_builder' + - '@server' + - '@client' + - '@interactions_storage' + - '@request_builder' + - '@request_matching_rule_builder' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 diff --git a/compatibility-suite/suites/v3/message/consumer.yml b/compatibility-suite/suites/v3/message/consumer.yml new file mode 100644 index 00000000..662dc2ae --- /dev/null +++ b/compatibility-suite/suites/v3/message/consumer.yml @@ -0,0 +1,16 @@ +default: + suites: + v3_message_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/message_consumer.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\Message\ConsumerContext': + - '@specification' + - '@message_generator_builder' + - '@parser' + - '@body_validator' + - '@body_storage' + - '@fixture_loader' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 diff --git a/compatibility-suite/suites/v3/message/provider.yml b/compatibility-suite/suites/v3/message/provider.yml new file mode 100644 index 00000000..41dbc7e9 --- /dev/null +++ b/compatibility-suite/suites/v3/message/provider.yml @@ -0,0 +1,26 @@ +default: + suites: + v3_message_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V3/message_provider.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\ProviderStateContext': + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ProviderContext': + - '@server' + - '@provider_verifier' + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\V3\Message\ProviderContext': + - '@server' + - '@interaction_builder' + - '@interactions_storage' + - '@message_pact_writer' + - '@provider_verifier' + - '@parser' + - '@fixture_loader' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 + + # filters: + # tags: ~@wip diff --git a/compatibility-suite/tests/Context/V3/BodyGeneratorsContext.php b/compatibility-suite/tests/Context/V3/BodyGeneratorsContext.php new file mode 100644 index 00000000..b829e7f3 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/BodyGeneratorsContext.php @@ -0,0 +1,22 @@ +validator->validateType($path, $type); + } +} diff --git a/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php new file mode 100644 index 00000000..6105d551 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php @@ -0,0 +1,99 @@ +interaction = $this->builder->build([ + 'description' => 'interaction for a consumer test', + 'method' => 'GET', + 'path' => '/provider-states', + ]); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $this->interaction); + } + + /** + * @Given a provider state :state is specified + */ + public function aProviderStateIsSpecified(string $state): void + { + $this->interaction->addProviderState($state, []); + } + + /** + * @When the Pact file for the test is generated + */ + public function thePactFileForTheTestIsGenerated(): void + { + $this->pactWriter->write($this->id); + } + + /** + * @Then the interaction in the Pact file will contain :states provider state(s) + */ + public function theInteractionInThePactFileWillContainProviderStates(int $states): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertCount($states, $pact['interactions'][0]['providerStates']); + } + + /** + * @Then the interaction in the Pact file will contain provider state :name + */ + public function theInteractionInThePactFileWillContainProviderState(string $name): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertNotEmpty(array_filter( + $pact['interactions'][0]['providerStates'], + fn (array $providerState) => $providerState['name'] === $name + )); + } + + /** + * @Given a provider state :state is specified with the following data: + */ + public function aProviderStateIsSpecifiedWithTheFollowingData(string $state, TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $this->interaction->addProviderState($state, $row); + } + + /** + * @Then the provider state :name in the Pact file will contain the following parameters: + */ + public function theProviderStateInThePactFileWillContainTheFollowingParameters(string $name, TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $params = json_decode($row['parameters'], true); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertNotEmpty(array_filter( + $pact['interactions'][0]['providerStates'], + fn (array $providerState) => $providerState['name'] === $name && $providerState['params'] === $params + )); + } +} diff --git a/compatibility-suite/tests/Context/V3/Http/ProviderContext.php b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php new file mode 100644 index 00000000..88b8b475 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php @@ -0,0 +1,57 @@ +pactWriter->write($id); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $rows = $table->getHash(); + $pact['interactions'][0]['providerStates'] = array_map(fn (array $row): array => ['name' => $row['State Name'], 'params' => json_decode($row['Parameters'] ?? '{}', true)], $rows); + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Then the provider state callback will receive a setup call with :state and the following parameters: + */ + public function theProviderStateCallbackWillReceiveASetupCallWithAndTheFollowingParameters(string $state, TableNode $table): void + { + $params = $table->getHash()[0]; + foreach ($params as &$value) { + $value = trim($value, '"'); + } + Assert::assertTrue($this->providerStateServer->hasState(ProviderStateServerInterface::ACTION_SETUP, $state, $params)); + } + + /** + * @Then the provider state callback will receive a teardown call :state and the following parameters: + */ + public function theProviderStateCallbackWillReceiveATeardownCallAndTheFollowingParameters(string $state, TableNode $table): void + { + $params = $table->getHash()[0]; + foreach ($params as &$value) { + $value = trim($value, '"'); + } + Assert::assertTrue($this->providerStateServer->hasState(ProviderStateServerInterface::ACTION_TEARDOWN, $state, $params)); + } +} diff --git a/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php new file mode 100644 index 00000000..cd2bd79f --- /dev/null +++ b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php @@ -0,0 +1,307 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $config = new PactMessageConfig(); + $config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($specificationVersion) + ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); + $this->builder = new MessageBuilder($config); + } + + /** + * @Given a message integration is being defined for a consumer test + */ + public function aMessageIntegrationIsBeingDefinedForAConsumerTest(): void + { + $this->builder->expectsToReceive('a message'); + } + + /** + * @Given the message payload contains the :fixture JSON document + */ + public function theMessagePayloadContainsTheJsonDocument(string $fixture): void + { + $this->builder->withContent($this->fixtureLoader->loadJson($fixture . '.json')); + } + + /** + * @When the message is successfully processed + */ + public function theMessageIsSuccessfullyProcessed(): void + { + $this->process([$this, 'storeMessage']); + } + + /** + * @Then the received message payload will contain the :fixture JSON document + */ + public function theReceivedMessagePayloadWillContainTheJsonDocument(string $fixture): void + { + Assert::assertJsonStringEqualsJsonString( + $this->fixtureLoader->load($fixture . '.json'), + json_encode($this->receivedMessage->contents) + ); + } + + /** + * @Then the received message content type will be :contentType + */ + public function theReceivedMessageContentTypeWillBe(string $contentType): void + { + Assert::assertSame($contentType, $this->receivedMessage->metadata->contentType); + } + + /** + * @Then the consumer test will have passed + */ + public function theConsumerTestWillHavePassed(): void + { + Assert::assertTrue($this->verifyResult); + } + + /** + * @Then a Pact file for the message interaction will have been written + */ + public function aPactFileForTheMessageInteractionWillHaveBeenWritten(): void + { + Assert::assertTrue(file_exists($this->pactPath)); + $this->pact = json_decode(file_get_contents($this->pactPath), true); + } + + /** + * @Then the pact file will contain :messages message interaction(s) + */ + public function thePactFileWillContainMessageInteraction(int $messages): void + { + Assert::assertCount($messages, $this->pact['messages'] ?? []); + } + + /** + * @Then the first message in the pact file will contain the :fixture document + */ + public function theFirstMessageInThePactFileWillContainTheDocument(string $fixture): void + { + Assert::assertJsonStringEqualsJsonString( + $this->fixtureLoader->load($fixture), + json_encode($this->pact['messages'][0]['contents'] ?? null) + ); + } + + /** + * @Then the first message in the pact file content type will be :contentType + */ + public function theFirstMessageInThePactFileContentTypeWillBe(string $contentType): void + { + Assert::assertSame($contentType, $this->pact['messages'][0]['metadata']['contentType'] ?? null); + } + + /** + * @When the message is NOT successfully processed with a :error exception + */ + public function theMessageIsNotSuccessfullyProcessedWithAException(string $error): void + { + $this->process(fn () => throw new Exception($error)); + } + + /** + * @Then the consumer test will have failed + */ + public function theConsumerTestWillHaveFailed(): void + { + Assert::assertFalse($this->verifyResult); + } + + /** + * @Then the consumer test error will be :error + */ + public function theConsumerTestErrorWillBe(string $error): void + { + // TODO Modify MessageBuilder code to check this exception? + } + + /** + * @Then a Pact file for the message interaction will NOT have been written + */ + public function aPactFileForTheMessageInteractionWillNotHaveBeenWritten(): void + { + Assert::assertFalse(file_exists($this->pactPath)); + } + + /** + * @Given the message contains the following metadata: + */ + public function theMessageContainsTheFollowingMetadata(TableNode $table): void + { + $this->builder->withMetadata($this->parser->parseMetadataTable($table->getHash())); + } + + /** + * @Then /^the received message metadata will contain "([^"]+)" == "(.+)"$/ + */ + public function theReceivedMessageMetadataWillContain(string $key, string $value): void + { + $actual = $this->receivedMessage->metadata->{$key}; + if (is_string($actual)) { + Assert::assertSame($this->parser->parseMetadataValue($value), $actual); + } else { + Assert::assertJsonStringEqualsJsonString($this->parser->parseMetadataValue($value), json_encode($actual)); + } + } + + /** + * @Then /^the first message in the pact file will contain the message metadata "([^"]+)" == "(.+)"$/ + */ + public function theFirstMessageInThePactFileWillContainTheMessageMetadata(string $key, string $value): void + { + $actual = $this->pact['messages'][0]['metadata'][$key] ?? null; + if (is_string($actual)) { + Assert::assertSame($this->parser->parseMetadataValue($value), $actual); + } else { + Assert::assertJsonStringEqualsJsonString($this->parser->parseMetadataValue($value), json_encode($actual)); + } + } + + /** + * @Given a provider state :state for the message is specified + */ + public function aProviderStateForTheMessageIsSpecified(string $state): void + { + $this->builder->given($state, []); + } + + /** + * @Given a message is defined + */ + public function aMessageIsDefined(): void + { + $this->aMessageIntegrationIsBeingDefinedForAConsumerTest(); + } + + /** + * @Then the first message in the pact file will contain :states provider state(s) + */ + public function theFirstMessageInThePactFileWillContainProviderStates(int $states): void + { + Assert::assertCount($states, $this->pact['messages'][0]['providerStates'] ?? []); + } + + /** + * @Then the first message in the Pact file will contain provider state :state + */ + public function theFirstMessageInThePactFileWillContainProviderState(string $state): void + { + $states = array_map(fn (array $state): string => $state['name'], $this->pact['messages'][0]['providerStates']); + Assert::assertContains($state, $states); + } + + /** + * @Given a provider state :state for the message is specified with the following data: + */ + public function aProviderStateForTheMessageIsSpecifiedWithTheFollowingData(string $state, TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $this->builder->given($state, $row); + } + + /** + * @Then the provider state :state for the message will contain the following parameters: + */ + public function theProviderStateForTheMessageWillContainTheFollowingParameters(string $state, TableNode $table): void + { + $params = json_decode($table->getHash()[0]['parameters'], true); + Assert::assertContains([ + 'name' => $state, + 'params' => $params, + ], $this->pact['messages'][0]['providerStates']); + } + + /** + * @Given the message is configured with the following: + */ + public function theMessageIsConfiguredWithTheFollowing(TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $message = new Message(); + $message->setBody(isset($row['body']) ? $this->parser->parseBody($row['body']) : null); + $message->setMetadata(isset($row['metadata']) ? json_decode($row['metadata'], true) : null); + $this->messageGeneratorBuilder->build($message, $row['generators']); + if ($message->hasBody()) { + $this->builder->withContent($message->getBody()); + } + if ($message->hasMetadata()) { + $this->builder->withContent('not empty'); // any not empty text, doesn't matter. If empty or not provided, received message will be null. + $this->builder->withMetadata($message->getMetadata()); + } + } + + /** + * @Then the message contents for :path will have been replaced with a(n) :type + */ + public function theMessageContentsForWillHaveBeenReplacedWithAn(string $path, string $type): void + { + $this->bodyStorage->setBody(json_encode($this->receivedMessage->contents)); + $this->validator->validateType($path, $type); + } + + /** + * @Then the received message metadata will contain :key replaced with an :type + */ + public function theReceivedMessageMetadataWillContainReplacedWithAn(string $key, string $type): void + { + $this->bodyStorage->setBody(json_encode($this->receivedMessage->metadata)); + $this->validator->validateType("$.$key", $type); + } + + public function storeMessage(string $message): void + { + $this->receivedMessage = json_decode($message); + } + + private function process(callable $callback): void + { + $this->builder->setCallback($callback); + + $this->verifyResult = $this->builder->verify(); + } +} diff --git a/compatibility-suite/tests/Context/V3/Message/ProviderContext.php b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php new file mode 100644 index 00000000..e62e7bcd --- /dev/null +++ b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php @@ -0,0 +1,166 @@ +builder->build([ + 'No' => $this->id, + 'description' => sprintf('Interaction for message %s', $name), + 'method' => 'POST', + 'path' => '/messages', + 'body' => 'JSON: ' . json_encode(['description' => $name]), + 'response body' => $fixture, + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->ids[] = $this->id++; + } + + /** + * @BeforeStep + */ + public function registerInteractions(BeforeStepScope $scope): void + { + if ( + $this->ids + && preg_match('/^a Pact file for .* is to be verified/', $scope->getStep()->getText()) + ) { + $this->server->register(...$this->ids); + $this->ids = []; + $this->providerVerifier + ->getConfig() + ->addProviderTransport( + (new ProviderTransport()) + ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) + ->setPort($this->server->getPort()) + ->setPath('/messages') + ->setScheme('http') + ); + ; + } + } + + /** + * @Given a Pact file for :name::fixture is to be verified + */ + public function aPactFileForIsToBeVerified(string $name, string $fixture): void + { + $this->pactWriter->write($name, $fixture, "c-$name"); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Given a Pact file for :name::fixture is to be verified with provider state :state + */ + public function aPactFileForIsToBeVerifiedWithProviderState(string $name, string $fixture, string $state): void + { + $this->aPactFileForIsToBeVerified($name, $fixture); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['messages'][0]['providerState'] = $state; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + } + + /** + * @Given a provider is started that can generate the :name message with :fixture and the following metadata: + */ + public function aProviderIsStartedThatCanGenerateTheMessageWithAndTheFollowingMetadata(string $name, string $fixture, TableNode $table): void + { + $interaction = $this->builder->build([ + 'No' => $this->id, + 'description' => sprintf('Interaction for message %s', $name), + 'method' => 'POST', + 'path' => '/messages', + 'body' => 'JSON: ' . json_encode(['description' => $name]), + 'response body' => $fixture, + 'response headers' => 'Pact-Message-Metadata: ' . base64_encode(json_encode($this->parser->parseMetadataTable($table->getHash()))), + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->server->register($this->id); + $this->providerVerifier + ->getConfig() + ->addProviderTransport( + (new ProviderTransport()) + ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) + ->setPort($this->server->getPort()) + ->setPath('/messages') + ->setScheme('http') + ); + ; + } + + /** + * @Given a Pact file for :name::fixture is to be verified with the following metadata: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowingMetadata(string $name, string $fixture, TableNode $table): void + { + $this->pactWriter->write($name, $fixture); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['messages'][0]['metaData'] = $this->parser->parseMetadataTable($table->getHash()); + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + } + + /** + * @Given a Pact file for :name is to be verified with the following: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowing(string $name, TableNode $table): void + { + foreach ($table->getRowsHash() as $key => $value) { + switch ($key) { + case 'body': + $body = $value; + break; + + case 'matching rules': + $matchingRules = $this->fixtureLoader->loadJson($value); + break; + + case 'metadata': + $metadata = $value; + break; + + default: + break; + } + } + $this->aPactFileForIsToBeVerified($name, $body); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + if (isset($metadata)) { + $pact['messages'][0]['metadata'] = array_merge($pact['messages'][0]['metadata'], $this->parser->parseMetadataMultiValues($metadata)); + } + $pact['messages'][0]['matchingRules'] = $matchingRules; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + } +} diff --git a/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php b/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php new file mode 100644 index 00000000..65959d64 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php @@ -0,0 +1,123 @@ +getHash(); + $row = reset($rows); + $interaction = $this->builder->build([ + 'No' => $this->id, + 'method' => 'PUT', + 'path' => '/request-generators', + 'body' => $row['body'] ?? '', + ]); + $this->requestGeneratorBuilder->build($interaction->getRequest(), $row['generators']); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $interaction); + } + + /** + * @When the request is prepared for use + */ + public function theRequestIsPreparedForUse(): void + { + $this->generatorServer->start(); + $this->pactWriter->write($this->id); + $this->providerVerifier->getConfig()->getProviderInfo()->setPort($this->generatorServer->getPort()); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $this->providerVerifier->verify(); + $this->generatorServer->stop(); + $this->bodyStorage->setBody($this->generatorServer->getBody()); + } + + /** + * @Given the generator test mode is set as :mode + */ + public function theGeneratorTestModeIsSetAs(string $mode): void + { + // There is nothing we can do using FFI call. + } + + /** + * @When the request is prepared for use with a "providerState" context: + */ + public function theRequestIsPreparedForUseWithAProviderStateContext(TableNode $table): void + { + $this->generatorServer->start(); + $this->pactWriter->write($this->id); + $port = $this->generatorServer->getPort(); + $this->providerVerifier->getConfig()->getProviderInfo()->setPort($port); + $params = json_decode($table->getRow(0)[0], true); + $this->providerVerifier + ->getConfig() + ->getProviderState() + ->setStateChangeUrl(new Uri("http://localhost:$port/return-provider-state-values?" . http_build_query($params))) + ->setStateChangeTeardown(false); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $this->providerVerifier->verify(); + $this->generatorServer->stop(); + $this->bodyStorage->setBody($this->generatorServer->getBody()); + } + + /** + * @Then the request :part will be set as :value + */ + public function theRequestWillBeSetAs(string $part, string $value): void + { + switch ($part) { + case 'path': + $path = $this->generatorServer->getPath(); + Assert::assertSame($value, $path); + break; + + default: + break; + } + } + + /** + * @Then the request :part will match :regex + */ + public function theRequestWillMatch(string $part, string $regex): void + { + if ($part === 'path') { + Assert::assertMatchesRegularExpression("/$regex/", $this->generatorServer->getPath()); + } elseif (preg_match('/header\[(.*)\]/', $part, $matches)) { + foreach ($this->generatorServer->getHeader($matches[1]) as $value) { + Assert::assertMatchesRegularExpression("/$regex/", $value); + } + } elseif (preg_match('/queryParameter\[(.*)\]/', $part, $matches)) { + Assert::assertMatchesRegularExpression("/$regex/", $this->generatorServer->getQueryParam($matches[1])); + } + } +} diff --git a/compatibility-suite/tests/Context/V3/RequestMatchingContext.php b/compatibility-suite/tests/Context/V3/RequestMatchingContext.php new file mode 100644 index 00000000..9bf139f7 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/RequestMatchingContext.php @@ -0,0 +1,169 @@ +type = self::HEADER_TYPE; + $interaction = $this->builder->build([ + 'No' => $this->id, + 'method' => 'GET', + 'path' => '/matching', + 'headers' => implode(', ', array_map(fn (string $value) => "'$header: $value'", explode(', ', $value))), + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id, $interaction, true); + $this->server->register($this->id); + } + + /** + * @Given a request is received with a(n) :header header of :value + */ + public function aRequestIsReceivedWithAHeaderOf(string $header, string $value): void + { + $request = $this->storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id)->getRequest(); + $request->addHeader($header, $value); + $this->client->sendRequestToServer($this->id); + } + + /** + * @When the request is compared to the expected one + */ + public function theRequestIsComparedToTheExpectedOne(): void + { + $this->server->verify(); + } + + /** + * @Then the comparison should be OK + */ + public function theComparisonShouldBeOk(): void + { + Assert::assertTrue($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then the comparison should NOT be OK + */ + public function theComparisonShouldNotBeOk(): void + { + Assert::assertFalse($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then /^the mismatches will contain a mismatch with error "([^"]+)" -> "(.+)"$/ + */ + public function theMismatchesWillContainAMismatchWithError(string $path, string $error): void + { + $error = str_replace('\"', '"', $error); + $key = $this->type === self::HEADER_TYPE ? 'key' : 'path'; + $mismatches = json_decode($this->server->getVerifyResult()->getOutput(), true); + $mismatches = array_reduce($mismatches, function (array $results, array $mismatch): array { + Assert::assertSame('request-mismatch', $mismatch['type']); + $results = array_merge($results, array_filter( + $mismatch['mismatches'], + fn (array $mismatch) => $mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$this->type] + )); + + return $results; + }, []); + $mismatches = array_filter( + $mismatches, + fn (array $mismatch) => $mismatch[$key] === $path + && ( + str_contains($mismatch['mismatch'], $error) + || @preg_match("|$error|", $mismatch['mismatch']) + ) + ); + Assert::assertNotEmpty($mismatches); + } + + /** + * @Given an expected request configured with the following: + */ + public function anExpectedRequestConfiguredWithTheFollowing(TableNode $table): void + { + $this->type = self::BODY_TYPE; + $rows = $table->getHash(); + $row = reset($rows); + $interaction = $this->builder->build([ + 'No' => $this->id, + 'method' => 'POST', + 'path' => '/matching', + ] + $row); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id, $interaction, true); + try { + $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $row['matching rules']); + } catch (IntegrationJsonFormatException $exception) { + throw new PendingException($exception->getMessage()); + } + $this->server->register($this->id); + } + + /** + * @Given a request is received with the following: + */ + public function aRequestIsReceivedWithTheFollowing(TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $request = $this->storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id)->getRequest(); + $this->requestBuilder->build($request, $row); + $this->client->sendRequestToServer($this->id); + } + + /** + * @Given the following requests are received: + */ + public function theFollowingRequestsAreReceived(TableNode $table): void + { + foreach ($table->getHash() as $row) { + $request = $this->storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id)->getRequest(); + $this->requestBuilder->build($request, $row); + $this->client->sendRequestToServer($this->id); + } + } + + /** + * @When the requests are compared to the expected one + */ + public function theRequestsAreComparedToTheExpectedOne(): void + { + $this->server->verify(); + } +} diff --git a/compatibility-suite/tests/Context/V3/ResponseGeneratorsContext.php b/compatibility-suite/tests/Context/V3/ResponseGeneratorsContext.php new file mode 100644 index 00000000..c2a1ed30 --- /dev/null +++ b/compatibility-suite/tests/Context/V3/ResponseGeneratorsContext.php @@ -0,0 +1,86 @@ +getHash(); + $row = reset($rows); + $interaction = $this->builder->build([ + 'No' => $this->id, + 'method' => 'GET', + 'path' => '/response-generators', + 'response body' => $row['body'] ?? '', + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id, $interaction, true); + $this->responseGeneratorBuilder->build($interaction->getResponse(), $row['generators']); + } + + /** + * @When the response is prepared for use + */ + public function theResponseIsPreparedForUse(): void + { + $this->server->register($this->id); + $this->client->sendRequestToServer($this->id); + $this->bodyStorage->setBody($this->client->getResponse()->getBody()->getContents()); + } + + /** + * @Then the response :part will not be :value + */ + public function theResponseWillNotBe(string $part, string $value): void + { + switch ($part) { + case 'status': + $code = $this->client->getResponse()->getStatusCode(); + Assert::assertNotEquals($value, $code); + break; + + default: + break; + } + } + + /** + * @Then the response :part will match :regex + */ + public function theResponseWillMatch(string $part, string $regex): void + { + if ($part === 'status') { + Assert::assertMatchesRegularExpression("/$regex/", $this->client->getResponse()->getStatusCode()); + } elseif (preg_match('/header\[(.*)\]/', $part, $matches)) { + foreach ($this->client->getResponse()->getHeader($matches[1]) as $value) { + Assert::assertMatchesRegularExpression("/$regex/", $value); + } + } + } +} diff --git a/compatibility-suite/tests/Model/Generator.php b/compatibility-suite/tests/Model/Generator.php new file mode 100644 index 00000000..3fb70da8 --- /dev/null +++ b/compatibility-suite/tests/Model/Generator.php @@ -0,0 +1,34 @@ +generator; + } + + public function getCategory(): string + { + return $this->category; + } + + public function getSubCategory(): ?string + { + return $this->subCategory; + } + + public function getGeneratorAttributes(): array + { + return $this->generatorAttributes; + } +} diff --git a/compatibility-suite/tests/Model/Message.php b/compatibility-suite/tests/Model/Message.php new file mode 100644 index 00000000..58406380 --- /dev/null +++ b/compatibility-suite/tests/Model/Message.php @@ -0,0 +1,42 @@ +body; + } + + public function setBody(null|Binary|Text $body): void + { + $this->body = $body; + } + + public function hasBody(): bool + { + return null !== $this->body; + } + + public function getMetadata(): ?array + { + return $this->metadata; + } + + public function setMetadata(?array $metadata): void + { + $this->metadata = $metadata; + } + + public function hasMetadata(): bool + { + return null !== $this->metadata; + } +} diff --git a/compatibility-suite/tests/Service/BodyStorage.php b/compatibility-suite/tests/Service/BodyStorage.php new file mode 100644 index 00000000..66900aee --- /dev/null +++ b/compatibility-suite/tests/Service/BodyStorage.php @@ -0,0 +1,18 @@ +body = $body; + } + + public function getBody(): string + { + return $this->body; + } +} diff --git a/compatibility-suite/tests/Service/BodyStorageInterface.php b/compatibility-suite/tests/Service/BodyStorageInterface.php new file mode 100644 index 00000000..bf7865fa --- /dev/null +++ b/compatibility-suite/tests/Service/BodyStorageInterface.php @@ -0,0 +1,10 @@ +getActualValue($path); + Assert::assertTrue((bool) match ($type) { + 'integer' => is_numeric($value) && preg_match(self::INT_REGEX, $value), + 'decimal number' => is_string($value) && preg_match(self::DEC_REGEX, $value), + 'hexadecimal number' => is_string($value) && preg_match(self::HEX_REGEX, $value), + 'random string' => is_string($value), + 'string from the regex' => is_string($value) && preg_match(self::STR_REGEX, $value), + 'date' => is_string($value) && preg_match(self::DATE_REGEX, $value), + 'time' => is_string($value) && preg_match(self::TIME_REGEX, $value), + 'date-time' => is_string($value) && preg_match(self::DATETIME_REGEX, $value), + 'UUID', 'simple UUID', 'lower-case-hyphenated UUID', 'upper-case-hyphenated UUID', 'URN UUID' => Uuid::isValid($value), + 'boolean' => is_bool($value), + default => false, + }); + } + + public function validateValue(string $path, string $value): void + { + Assert::assertSame($value, $this->getActualValue($path)); + } + + private function getActualValue(string $path): mixed + { + $jsonObject = new JsonObject($this->bodyStorage->getBody(), true); + + return $jsonObject->{$path}; + } +} diff --git a/compatibility-suite/tests/Service/BodyValidatorInterface.php b/compatibility-suite/tests/Service/BodyValidatorInterface.php new file mode 100644 index 00000000..805d6bd7 --- /dev/null +++ b/compatibility-suite/tests/Service/BodyValidatorInterface.php @@ -0,0 +1,10 @@ +getGenerator()); + + $matcher = new Integer(); // Doesn't matter. Any matcher doesn't require value and accept generator will be fine. + $matcher->setGenerator(new $class(...$generator->getGeneratorAttributes())); + + return $matcher; + } +} diff --git a/compatibility-suite/tests/Service/GeneratorConverterInterface.php b/compatibility-suite/tests/Service/GeneratorConverterInterface.php new file mode 100644 index 00000000..af71456e --- /dev/null +++ b/compatibility-suite/tests/Service/GeneratorConverterInterface.php @@ -0,0 +1,11 @@ +fixtureLoader->loadJson($value); + } + + return $this->loadFromMap($map); + } + + private function loadFromMap(array $map): array + { + $generators = []; + $removeType = fn (array $values): array => array_filter( + $values, + fn (mixed $v, string $k) => $k !== 'type', + ARRAY_FILTER_USE_BOTH + ); + foreach ($map as $category => $values) { + switch ($category) { + case 'path': + case 'method': + case 'status': + $generators[] = new Generator($values['type'], $category, null, $removeType($values)); + break; + + default: + foreach ($values as $subCategory => $value) { + $generators[] = new Generator($value['type'], $category, $subCategory, $removeType($value)); + } + break; + } + } + + return $generators; + } +} diff --git a/compatibility-suite/tests/Service/GeneratorParserInterface.php b/compatibility-suite/tests/Service/GeneratorParserInterface.php new file mode 100644 index 00000000..7b77cd82 --- /dev/null +++ b/compatibility-suite/tests/Service/GeneratorParserInterface.php @@ -0,0 +1,13 @@ + + */ + public function parse(string $value): array; +} diff --git a/compatibility-suite/tests/Service/GeneratorServer.php b/compatibility-suite/tests/Service/GeneratorServer.php new file mode 100644 index 00000000..3a2e3d75 --- /dev/null +++ b/compatibility-suite/tests/Service/GeneratorServer.php @@ -0,0 +1,63 @@ +bodyFile, $this->pathFile, $this->headersFile, $this->queryParamsFile] as $file) { + @unlink($file); + } + $socket = \socket_create_listen($this->port); + \socket_getsockname($socket, $addr, $this->port); + \socket_close($socket); + $this->process = new ProviderProcess(Path::PUBLIC_PATH . '/generators/', $this->port); + $this->process->start(); + } + + public function stop(): void + { + $this->port = 0; + $this->process->stop(); + } + + public function getPort(): int + { + return $this->port; + } + + public function getBody(): string + { + return @file_get_contents($this->bodyFile); + } + + public function getPath(): string + { + return file_get_contents($this->pathFile); + } + + public function getHeader(string $header): array + { + $headers = json_decode(file_get_contents($this->headersFile), true); + + return $headers[$header] ?? []; + } + + public function getQueryParam(string $name): string + { + $queryParams = json_decode(file_get_contents($this->queryParamsFile), true); + + return $queryParams[$name] ?? ''; + } +} diff --git a/compatibility-suite/tests/Service/GeneratorServerInterface.php b/compatibility-suite/tests/Service/GeneratorServerInterface.php new file mode 100644 index 00000000..05ae02af --- /dev/null +++ b/compatibility-suite/tests/Service/GeneratorServerInterface.php @@ -0,0 +1,20 @@ +parser->parse($value) as $generator) { + switch ($generator->getCategory()) { + case 'metadata': + $metadata = $message->getMetadata() ?? []; + $metadata[$generator->getSubCategory()] = $this->converter->convert($generator); + $message->setMetadata($metadata); + break; + + case 'body': + $body = $message->getBody(); + if ($body instanceof Text && $body->getContentType() === 'application/json') { + $jsonObject = new JsonObject($body->getContents(), true); + $jsonObject->{$generator->getSubCategory()} = $this->converter->convert($generator); + $body->setContents($jsonObject); + } else { + throw new IntegrationJsonFormatException("Integration JSON format doesn't support non-JSON format"); + } + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/MessageGeneratorBuilderInterface.php b/compatibility-suite/tests/Service/MessageGeneratorBuilderInterface.php new file mode 100644 index 00000000..45c446d9 --- /dev/null +++ b/compatibility-suite/tests/Service/MessageGeneratorBuilderInterface.php @@ -0,0 +1,10 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $config = new MockServerConfig(); + $config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($this->specificationVersion) + ->setPactFileWriteMode($mode); + $driver = (new MessageDriverFactory())->create($config); + + $message = new Message(); + $message->setDescription($name); + $message->setContents($this->parser->parseBody($body)); + $driver->registerMessage($message); + $driver->writePactAndCleanUp(); + } + + public function getPactPath(): string + { + return $this->pactPath; + } +} diff --git a/compatibility-suite/tests/Service/MessagePactWriterInterface.php b/compatibility-suite/tests/Service/MessagePactWriterInterface.php new file mode 100644 index 00000000..4cc4ce8e --- /dev/null +++ b/compatibility-suite/tests/Service/MessagePactWriterInterface.php @@ -0,0 +1,12 @@ +parser->parse($value) as $generator) { + switch ($generator->getCategory()) { + case 'method': + // Can't set generator to method + break; + + case 'path': + $request->setPath($this->converter->convert($generator)); + break; + + case 'query': + if ($generator->getSubCategory()) { + $request->addQueryParameter($generator->getSubCategory(), $this->converter->convert($generator)); + } + break; + + case 'header': + if ($generator->getSubCategory()) { + $request->addHeader($generator->getSubCategory(), $this->converter->convert($generator)); + } + break; + + case 'body': + $body = $request->getBody(); + if ($body instanceof Text && $body->getContentType() === 'application/json') { + $jsonObject = new JsonObject($body->getContents(), true); + $jsonObject->{$generator->getSubCategory()} = $this->converter->convert($generator); + $body->setContents($jsonObject); + } else { + throw new IntegrationJsonFormatException("Integration JSON format doesn't support non-JSON format"); + } + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/RequestGeneratorBuilderInterface.php b/compatibility-suite/tests/Service/RequestGeneratorBuilderInterface.php new file mode 100644 index 00000000..8a409bb7 --- /dev/null +++ b/compatibility-suite/tests/Service/RequestGeneratorBuilderInterface.php @@ -0,0 +1,10 @@ +parser->parse($value) as $generator) { + switch ($generator->getCategory()) { + case 'status': + $response->setStatus($this->converter->convert($generator)); + break; + + case 'header': + if ($generator->getSubCategory()) { + $response->addHeader($generator->getSubCategory(), $this->converter->convert($generator)); + } + break; + + case 'body': + $body = $response->getBody(); + if ($body instanceof Text && $body->getContentType() === 'application/json') { + $jsonObject = new JsonObject($body->getContents(), true); + $jsonObject->{$generator->getSubCategory()} = $this->converter->convert($generator); + $body->setContents($jsonObject); + } else { + throw new IntegrationJsonFormatException("Integration JSON format doesn't support non-JSON format"); + } + break; + + default: + break; + } + } + } +} diff --git a/compatibility-suite/tests/Service/ResponseGeneratorBuilderInterface.php b/compatibility-suite/tests/Service/ResponseGeneratorBuilderInterface.php new file mode 100644 index 00000000..b7f11aca --- /dev/null +++ b/compatibility-suite/tests/Service/ResponseGeneratorBuilderInterface.php @@ -0,0 +1,10 @@ +set('generator_parser', new GeneratorParser($this->get('fixture_loader'))); + $this->set('generator_converter', new GeneratorConverter()); + $this->set('generator_server', new GeneratorServer()); + $this->set('body_storage', new BodyStorage()); + $this->set('body_validator', new BodyValidator($this->get('body_storage'))); + $this->set('message_pact_writer', new MessagePactWriter($this->get('parser'), $this->getSpecification())); + $this->set('request_generator_builder', new RequestGeneratorBuilder($this->get('generator_parser'), $this->get('generator_converter'))); + $this->set('response_generator_builder', new ResponseGeneratorBuilder($this->get('generator_parser'), $this->get('generator_converter'))); + $this->set('message_generator_builder', new MessageGeneratorBuilder($this->get('generator_parser'), $this->get('generator_converter'))); + } + + protected function getSpecification(): string + { + return '3.0.0'; + } +} diff --git a/composer.json b/composer.json index 515aed72..f53a4b8f 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "guzzlehttp/guzzle": "^7.8", "tienvx/pact-php-xml": "^0.1", "behat/behat": "^3.13", - "galbar/jsonpath": "^3.0" + "galbar/jsonpath": "^3.0", + "ramsey/uuid": "^4.7" }, "autoload": { "psr-4": { From cc8135a0f3bd3ba068b5e090e3a4fbfd7e751784 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 23 Dec 2023 20:58:15 +0700 Subject: [PATCH 175/298] test(compatibility-suite): Filter out failed scenarios --- .github/workflows/compatibility-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index 4a44444b..de583a99 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -53,4 +53,4 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!Kafka|binary body \(negative|Message provider).)*$/' From 3eda8987b3fc5063c5985142a31110831959519c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 7 Oct 2023 23:20:21 +0700 Subject: [PATCH 176/298] test(compatibility-suite): Implement V4 scenarios --- .github/workflows/compatibility-suite.yml | 16 + behat.yml | 8 + compatibility-suite/suites/v4/combined.yml | 14 + compatibility-suite/suites/v4/generators.yml | 28 ++ .../suites/v4/http/consumer.yml | 13 + .../suites/v4/http/provider.yml | 31 ++ .../suites/v4/matching-rules.yml | 23 ++ .../suites/v4/message/consumer.yml | 14 + .../suites/v4/message/provider.yml | 18 + .../suites/v4/sync-message/consumer.yml | 14 + .../Context/V4/BodyGeneratorsContext.php | 22 ++ .../tests/Context/V4/CombinedContext.php | 64 ++++ .../tests/Context/V4/Http/ConsumerContext.php | 87 +++++ .../tests/Context/V4/Http/ProviderContext.php | 106 ++++++ .../Context/V4/Message/ConsumerContext.php | 73 ++++ .../Context/V4/Message/ProviderContext.php | 159 +++++++++ .../Context/V4/ResponseGeneratorsContext.php | 56 +++ .../Context/V4/ResponseMatchingContext.php | 112 ++++++ .../V4/SyncMessage/ConsumerContext.php | 328 ++++++++++++++++++ .../tests/Service/SyncMessagePactWriter.php | 49 +++ .../SyncMessagePactWriterInterface.php | 13 + .../tests/ServiceContainer/V4.php | 19 + 22 files changed, 1267 insertions(+) create mode 100644 compatibility-suite/suites/v4/combined.yml create mode 100644 compatibility-suite/suites/v4/generators.yml create mode 100644 compatibility-suite/suites/v4/http/consumer.yml create mode 100644 compatibility-suite/suites/v4/http/provider.yml create mode 100644 compatibility-suite/suites/v4/matching-rules.yml create mode 100644 compatibility-suite/suites/v4/message/consumer.yml create mode 100644 compatibility-suite/suites/v4/message/provider.yml create mode 100644 compatibility-suite/suites/v4/sync-message/consumer.yml create mode 100644 compatibility-suite/tests/Context/V4/BodyGeneratorsContext.php create mode 100644 compatibility-suite/tests/Context/V4/CombinedContext.php create mode 100644 compatibility-suite/tests/Context/V4/Http/ConsumerContext.php create mode 100644 compatibility-suite/tests/Context/V4/Http/ProviderContext.php create mode 100644 compatibility-suite/tests/Context/V4/Message/ConsumerContext.php create mode 100644 compatibility-suite/tests/Context/V4/Message/ProviderContext.php create mode 100644 compatibility-suite/tests/Context/V4/ResponseGeneratorsContext.php create mode 100644 compatibility-suite/tests/Context/V4/ResponseMatchingContext.php create mode 100644 compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php create mode 100644 compatibility-suite/tests/Service/SyncMessagePactWriter.php create mode 100644 compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php create mode 100644 compatibility-suite/tests/ServiceContainer/V4.php diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index de583a99..8cc1ee7d 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -54,3 +54,19 @@ jobs: - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!Kafka|binary body \(negative|Message provider).)*$/' + v4: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: none + + - uses: ramsey/composer-install@v2 + + - name: Run Behat + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V4 diff --git a/behat.yml b/behat.yml index 56830032..e0a02922 100644 --- a/behat.yml +++ b/behat.yml @@ -9,3 +9,11 @@ imports: - 'compatibility-suite/suites/v3/message/provider.yml' - 'compatibility-suite/suites/v3/generators.yml' - 'compatibility-suite/suites/v3/matching-rules.yml' + - 'compatibility-suite/suites/v4/http/consumer.yml' + - 'compatibility-suite/suites/v4/http/provider.yml' + - 'compatibility-suite/suites/v4/message/consumer.yml' + - 'compatibility-suite/suites/v4/message/provider.yml' + - 'compatibility-suite/suites/v4/combined.yml' + - 'compatibility-suite/suites/v4/sync-message/consumer.yml' + - 'compatibility-suite/suites/v4/matching-rules.yml' + - 'compatibility-suite/suites/v4/generators.yml' diff --git a/compatibility-suite/suites/v4/combined.yml b/compatibility-suite/suites/v4/combined.yml new file mode 100644 index 00000000..1873d0b6 --- /dev/null +++ b/compatibility-suite/suites/v4/combined.yml @@ -0,0 +1,14 @@ +default: + suites: + v4_combined: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4/v4.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V4\CombinedContext': + - '@interaction_builder' + - '@interactions_storage' + - '@pact_writer' + - '@message_pact_writer' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/generators.yml b/compatibility-suite/suites/v4/generators.yml new file mode 100644 index 00000000..22fcc247 --- /dev/null +++ b/compatibility-suite/suites/v4/generators.yml @@ -0,0 +1,28 @@ +default: + suites: + v4_generators: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4/generators.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\BodyGeneratorsContext': + - '@body_validator' + - 'PhpPactTest\CompatibilitySuite\Context\V3\RequestGeneratorsContext': + - '@interaction_builder' + - '@request_generator_builder' + - '@interactions_storage' + - '@pact_writer' + - '@generator_server' + - '@provider_verifier' + - '@body_storage' + - 'PhpPactTest\CompatibilitySuite\Context\V4\BodyGeneratorsContext': + - '@body_validator' + - 'PhpPactTest\CompatibilitySuite\Context\V4\ResponseGeneratorsContext': + - '@interaction_builder' + - '@response_generator_builder' + - '@interactions_storage' + - '@server' + - '@client' + - '@body_storage' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/http/consumer.yml b/compatibility-suite/suites/v4/http/consumer.yml new file mode 100644 index 00000000..57b71dea --- /dev/null +++ b/compatibility-suite/suites/v4/http/consumer.yml @@ -0,0 +1,13 @@ +default: + suites: + v4_http_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4/http_consumer.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V4\Http\ConsumerContext': + - '@interaction_builder' + - '@pact_writer' + - '@interactions_storage' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/http/provider.yml b/compatibility-suite/suites/v4/http/provider.yml new file mode 100644 index 00000000..cdfd7dec --- /dev/null +++ b/compatibility-suite/suites/v4/http/provider.yml @@ -0,0 +1,31 @@ +default: + suites: + v4_http_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4/http_provider.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - '@request_matching_rule_builder' + - '@response_matching_rule_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Transform\InteractionsContext': + - '@interaction_builder' + - '@matching_rules_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ProviderContext': + - '@server' + - '@provider_verifier' + - '@provider_state_server' + - 'PhpPactTest\CompatibilitySuite\Context\V1\Http\ProviderContext': + - '@server' + - '@pact_writer' + - '@pact_broker' + - '@response_builder' + - '@interactions_storage' + - '@provider_verifier' + - 'PhpPactTest\CompatibilitySuite\Context\V4\Http\ProviderContext': + - '@pact_writer' + - '@provider_verifier' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/matching-rules.yml b/compatibility-suite/suites/v4/matching-rules.yml new file mode 100644 index 00000000..34c4d7dc --- /dev/null +++ b/compatibility-suite/suites/v4/matching-rules.yml @@ -0,0 +1,23 @@ +default: + suites: + v4_matching_rules: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4/matching_rules.feature' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V3\RequestMatchingContext': + - '@interaction_builder' + - '@server' + - '@client' + - '@interactions_storage' + - '@request_builder' + - '@request_matching_rule_builder' + - 'PhpPactTest\CompatibilitySuite\Context\V4\ResponseMatchingContext': + - '@interaction_builder' + - '@interactions_storage' + - '@response_matching_rule_builder' + - '@server' + - '@pact_writer' + - '@provider_verifier' + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/message/consumer.yml b/compatibility-suite/suites/v4/message/consumer.yml new file mode 100644 index 00000000..25ae890f --- /dev/null +++ b/compatibility-suite/suites/v4/message/consumer.yml @@ -0,0 +1,14 @@ +default: + suites: + v4_message_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V4\Message\ConsumerContext': + - '@message_pact_writer' + + filters: + tags: "@consumer&&@message" + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/message/provider.yml b/compatibility-suite/suites/v4/message/provider.yml new file mode 100644 index 00000000..c66c344f --- /dev/null +++ b/compatibility-suite/suites/v4/message/provider.yml @@ -0,0 +1,18 @@ +default: + suites: + v4_message_provider: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V4\Message\ProviderContext': + - '@server' + - '@interaction_builder' + - '@interactions_storage' + - '@message_pact_writer' + - '@provider_verifier' + + filters: + tags: "@provider&&@message" + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/suites/v4/sync-message/consumer.yml b/compatibility-suite/suites/v4/sync-message/consumer.yml new file mode 100644 index 00000000..ea263247 --- /dev/null +++ b/compatibility-suite/suites/v4/sync-message/consumer.yml @@ -0,0 +1,14 @@ +default: + suites: + v4_sync_message_consumer: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features/V4' ] + + contexts: + - 'PhpPactTest\CompatibilitySuite\Context\Shared\Hook\SetUpContext' + - 'PhpPactTest\CompatibilitySuite\Context\V4\SyncMessage\ConsumerContext': + - '@sync_message_pact_writer' + + filters: + tags: "@SynchronousMessage&&@message" + + services: PhpPactTest\CompatibilitySuite\ServiceContainer\V4 diff --git a/compatibility-suite/tests/Context/V4/BodyGeneratorsContext.php b/compatibility-suite/tests/Context/V4/BodyGeneratorsContext.php new file mode 100644 index 00000000..f72012f3 --- /dev/null +++ b/compatibility-suite/tests/Context/V4/BodyGeneratorsContext.php @@ -0,0 +1,22 @@ +validator->validateValue($path, $value); + } +} diff --git a/compatibility-suite/tests/Context/V4/CombinedContext.php b/compatibility-suite/tests/Context/V4/CombinedContext.php new file mode 100644 index 00000000..6a1b0bce --- /dev/null +++ b/compatibility-suite/tests/Context/V4/CombinedContext.php @@ -0,0 +1,64 @@ +builder->build([ + 'description' => 'http interaction', + 'method' => 'GET', + 'path' => '/v4-features', + ]); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $interaction); + } + + /** + * @Given a message interaction is being defined for a consumer test + */ + public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void + { + } + + /** + * @When the Pact file for the test is generated + */ + public function thePactFileForTheTestIsGenerated(): void + { + $this->pactWriter->write($this->id, 'c', 'p', PactConfigInterface::MODE_MERGE); + $this->messagePactWriter->write('message interaction', '', 'c', 'p', PactConfigInterface::MODE_MERGE); + } + + /** + * @Then there will be an interaction in the Pact file with a type of :type + */ + public function thereWillBeAnInteractionInThePactFileWithATypeOf(string $type): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $types = array_map(fn (array $interaction) => $interaction['type'], $pact['interactions']); + Assert::assertContains($type, $types); + } +} diff --git a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php new file mode 100644 index 00000000..4bab8b17 --- /dev/null +++ b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php @@ -0,0 +1,87 @@ +interaction = $this->builder->build([ + 'description' => 'interaction for a consumer test', + 'method' => 'GET', + 'path' => '/v4-features', + ]); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $this->interaction); + } + + /** + * @Then the first interaction in the Pact file will have a type of :type + */ + public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($type, $pact['interactions'][0]['type']); + } + + /** + * @Given a key of :key is specified for the HTTP interaction + */ + public function aKeyOfIsSpecifiedForTheHttpInteraction(string $key): void + { + throw new PendingException("Can't set interaction's key using FFI call"); + } + + /** + * @Then the first interaction in the Pact file will have :name = :value + */ + public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($value, $pact['interactions'][0][$name]); + } + + /** + * @Given the HTTP interaction is marked as pending + */ + public function theHttpInteractionIsMarkedAsPending(): void + { + throw new PendingException("Can't set interaction's pending using FFI call"); + } + + /** + * @Given a comment :value is added to the HTTP interaction + */ + public function aCommentIsAddedToTheHttpInteraction(string $value): void + { + throw new PendingException("Can't set interaction's comment using FFI call"); + } + + /** + * @When the Pact file for the test is generated + */ + public function thePactFileForTheTestIsGenerated(): void + { + $this->pactWriter->write($this->id); + } +} diff --git a/compatibility-suite/tests/Context/V4/Http/ProviderContext.php b/compatibility-suite/tests/Context/V4/Http/ProviderContext.php new file mode 100644 index 00000000..4f769e2a --- /dev/null +++ b/compatibility-suite/tests/Context/V4/Http/ProviderContext.php @@ -0,0 +1,106 @@ +pactWriter->write($id); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['interactions'][0]['pending'] = true; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Then there will be a pending :error error + */ + public function thereWillBeAPendingError(string $error): void + { + $output = json_decode($this->providerVerifier->getVerifyResult()->getOutput(), true); + $errors = array_reduce( + $output['pendingErrors'], + function (array $errors, array $error) { + switch ($error['mismatch']['type']) { + case 'error': + $errors[] = Mismatch::VERIFIER_MISMATCH_ERROR_MAP[$error['mismatch']['message']]; + break; + + case 'mismatches': + foreach ($error['mismatch']['mismatches'] as $mismatch) { + $errors[] = Mismatch::VERIFIER_MISMATCH_TYPE_MAP[$mismatch['type']]; + } + break; + + default: + break; + } + + return $errors; + }, + [] + ); + Assert::assertContains($error, $errors); + } + + /** + * @Given a Pact file for interaction :id is to be verified with the following comments: + */ + public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingComments(int $id, TableNode $table): void + { + $comments = []; + foreach ($table->getHash() as $row) { + switch ($row['type']) { + case 'text': + $comments['text'][] = $row['comment']; + break; + + case 'testname': + $comments['testname'] = $row['comment']; + break; + + default: + # code... + break; + } + } + $this->pactWriter->write($id); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['interactions'][0]['comments'] = $comments; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Then the comment :comment will have been printed to the console + */ + public function theCommentWillHaveBeenPrintedToTheConsole(string $comment): void + { + Assert::assertStringContainsString($comment, $this->providerVerifier->getVerifyResult()->getOutput()); + } + + /** + * @Then the :name will displayed as the original test name + */ + public function theWillDisplayedAsTheOriginalTestName(string $name): void + { + Assert::assertStringContainsString(sprintf('Test Name: %s', $name), $this->providerVerifier->getVerifyResult()->getOutput()); + } +} diff --git a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php new file mode 100644 index 00000000..f5e98b78 --- /dev/null +++ b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php @@ -0,0 +1,73 @@ +pactWriter->write('a message', ''); + } + + /** + * @Then the first interaction in the Pact file will have a type of :type + */ + public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($type, $pact['interactions'][0]['type']); + } + + /** + * @Given a key of :key is specified for the message interaction + */ + public function aKeyOfIsSpecifiedForTheMessageInteraction(string $key): void + { + throw new PendingException("Can't set message's key using FFI call"); + } + + /** + * @Given the message interaction is marked as pending + */ + public function theMessageInteractionIsMarkedAsPending(): void + { + throw new PendingException("Can't set message's pending using FFI call"); + } + + /** + * @Given a comment :value is added to the message interaction + */ + public function aCommentIsAddedToTheMessageInteraction(string $value): void + { + throw new PendingException("Can't set message's comment using FFI call"); + } + + /** + * @Then the first interaction in the Pact file will have :name = :value + */ + public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($value, $pact['interactions'][0][$name]); + } +} diff --git a/compatibility-suite/tests/Context/V4/Message/ProviderContext.php b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php new file mode 100644 index 00000000..44550566 --- /dev/null +++ b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php @@ -0,0 +1,159 @@ +builder->build([ + 'No' => $this->id, + 'description' => sprintf('Interaction for message %s', $name), + 'method' => 'POST', + 'path' => '/messages', + 'body' => 'JSON: ' . json_encode(['description' => $name]), + 'response body' => $fixture, + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->server->register($this->id); + $this->providerVerifier + ->getConfig() + ->addProviderTransport( + (new ProviderTransport()) + ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) + ->setPort($this->server->getPort()) + ->setPath('/messages') + ->setScheme('http') + ); + ; + } + + /** + * @Given a Pact file for :name::fixture is to be verified, but is marked pending + */ + public function aPactFileForIsToBeVerifiedButIsMarkedPending(string $name, string $fixture): void + { + $this->pactWriter->write($name, $fixture); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['interactions'][0]['pending'] = true; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + } + + /** + * @Given a Pact file for :name::fixture is to be verified with the following comments: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowingComments(string $name, string $fixture, TableNode $table): void + { + $comments = []; + foreach ($table->getHash() as $row) { + switch ($row['type']) { + case 'text': + $comments['text'][] = $row['comment']; + break; + + case 'testname': + $comments['testname'] = $row['comment']; + break; + + default: + # code... + break; + } + } + $this->pactWriter->write($name, $fixture); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact['interactions'][0]['comments'] = $comments; + file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + } + + /** + * @When the verification is run + */ + public function theVerificationIsRun(): void + { + $this->providerVerifier->getConfig()->getProviderInfo()->setPort($this->server->getPort()); + $this->providerVerifier->verify(); + } + + /** + * @Then the verification will be successful + */ + public function theVerificationWillBeSuccessful(): void + { + Assert::assertTrue($this->providerVerifier->getVerifyResult()->isSuccess()); + } + + /** + * @Then there will be a pending :error error + */ + public function thereWillBeAPendingError(string $error): void + { + $output = json_decode($this->providerVerifier->getVerifyResult()->getOutput(), true); + $errors = array_reduce( + $output['pendingErrors'], + function (array $errors, array $error) { + switch ($error['mismatch']['type']) { + case 'error': + $errors[] = Mismatch::VERIFIER_MISMATCH_ERROR_MAP[$error['mismatch']['message']]; + break; + + case 'mismatches': + foreach ($error['mismatch']['mismatches'] as $mismatch) { + $errors[] = Mismatch::VERIFIER_MISMATCH_TYPE_MAP[$mismatch['type']]; + } + break; + + default: + break; + } + + return $errors; + }, + [] + ); + Assert::assertContains($error, $errors); + } + + /** + * @Then the comment :comment will have been printed to the console + */ + public function theCommentWillHaveBeenPrintedToTheConsole(string $comment): void + { + Assert::assertStringContainsString($comment, $this->providerVerifier->getVerifyResult()->getOutput()); + } + + /** + * @Then the :name will displayed as the original test name + */ + public function theWillDisplayedAsTheOriginalTestName(string $name): void + { + Assert::assertStringContainsString(sprintf('Test Name: %s', $name), $this->providerVerifier->getVerifyResult()->getOutput()); + } +} diff --git a/compatibility-suite/tests/Context/V4/ResponseGeneratorsContext.php b/compatibility-suite/tests/Context/V4/ResponseGeneratorsContext.php new file mode 100644 index 00000000..93dcb826 --- /dev/null +++ b/compatibility-suite/tests/Context/V4/ResponseGeneratorsContext.php @@ -0,0 +1,56 @@ +builder->build([ + 'No' => $this->id, + 'method' => 'GET', + 'path' => '/response-generators', + 'response body' => 'file: basic.json', + ]); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $this->id, $interaction, true); + $this->responseGeneratorBuilder->build($interaction->getResponse(), 'mockserver-generator.json'); + + $this->server->register($this->id); + $this->client->sendRequestToServer($this->id); + + $body = $this->client->getResponse()->getBody()->getContents(); + $href = json_decode($table->getRow(0)[0], true)['href']; + $serverBaseUri = $this->server->getBaseUri(); + $search = [ + (string) $serverBaseUri->withHost('127.0.0.1'), + (string) $serverBaseUri->withHost('::1'), + ]; + $body = str_replace($search, $href, $body); + $this->bodyStorage->setBody($body); + } +} diff --git a/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php b/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php new file mode 100644 index 00000000..d878570c --- /dev/null +++ b/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php @@ -0,0 +1,112 @@ +getHash(); + $row = reset($rows); + $interaction = $this->builder->build([ + 'No' => $this->id, + 'method' => 'GET', + 'path' => '/matching', + ] + $row); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $interaction); + $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $row['matching rules']); + $this->pactWriter->write($this->id); + $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + } + + /** + * @Given a status :status response is received + */ + public function aStatusResponseIsReceived(int $status): void + { + $interaction = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $this->id); + $interaction->getResponse()->setStatus($status); + $this->server->register($this->id); + } + + /** + * @When the response is compared to the expected one + */ + public function theResponseIsComparedToTheExpectedOne(): void + { + $this->providerVerifier->getConfig()->getProviderInfo()->setPort($this->server->getPort()); + $this->providerVerifier->verify(); + } + + /** + * @Then the response comparison should be OK + */ + public function theResponseComparisonShouldBeOk(): void + { + Assert::assertTrue($this->providerVerifier->getVerifyResult()->isSuccess()); + } + + /** + * @Then the response comparison should NOT be OK + */ + public function theResponseComparisonShouldNotBeOk(): void + { + Assert::assertFalse($this->providerVerifier->getVerifyResult()->isSuccess()); + } + + /** + * @Then the response mismatches will contain a :type mismatch with error :error + */ + public function theResponseMismatchesWillContainAMismatchWithError(string $type, string $error): void + { + $output = json_decode($this->providerVerifier->getVerifyResult()->getOutput(), true); + $errors = array_reduce( + $output['errors'], + function (array $errors, array $error) use ($type) { + switch ($error['mismatch']['type']) { + case 'mismatches': + foreach ($error['mismatch']['mismatches'] as $mismatch) { + if ($mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$type]) { + $errors[] = $mismatch['mismatch']; + } + } + break; + + default: + break; + } + + return $errors; + }, + [] + ); + Assert::assertContains($error, $errors); + } +} diff --git a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php new file mode 100644 index 00000000..0f86ef1a --- /dev/null +++ b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php @@ -0,0 +1,328 @@ +message = new Message(); + $this->message->setDescription('a synchronous message'); + } + + /** + * @When the Pact file for the test is generated + */ + public function thePactFileForTheTestIsGenerated(): void + { + $this->pactWriter->write($this->message); + } + + /** + * @Then the first interaction in the Pact file will have a type of :type + */ + public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($type, $pact['interactions'][0]['type']); + } + + /** + * @Given a key of :key is specified for the synchronous message interaction + */ + public function aKeyOfIsSpecifiedForTheSynchronousMessageInteraction(string $key): void + { + throw new PendingException("Can't set sync message's key using FFI call"); + } + + /** + * @Given the synchronous message interaction is marked as pending + */ + public function theSynchronousMessageInteractionIsMarkedAsPending(): void + { + throw new PendingException("Can't set sync message's pending using FFI call"); + } + + /** + * @Given a comment :value is added to the synchronous message interaction + */ + public function aCommentIsAddedToTheSynchronousMessageInteraction(string $value): void + { + throw new PendingException("Can't set message's comment using FFI call"); + } + + /** + * @Then the first interaction in the Pact file will have :name = :value + */ + public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void + { + $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertSame($value, $pact['interactions'][0][$name]); + } + + /** + * @Given the message request payload contains the :fixture JSON document + */ + public function theMessageRequestPayloadContainsTheJsonDocument(string $fixture): void + { + throw new PendingException("Can't set sync message's request payload using FFI call"); + } + + /** + * @Given the message response payload contains the :fixture document + */ + public function theMessageResponsePayloadContainsTheDocument(string $fixture): void + { + throw new PendingException("Can't set sync message's response payload using FFI call"); + } + + /** + * @Then the received message payload will contain the :fixture document + */ + public function theReceivedMessagePayloadWillContainTheDocument(string $fixture): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then a Pact file for the message interaction will have been written + */ + public function aPactFileForTheMessageInteractionWillHaveBeenWritten(): void + { + Assert::assertTrue(file_exists($this->pactWriter->getPactPath())); + $this->pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + } + + /** + * @Then the pact file will contain :num interaction + */ + public function thePactFileWillContainInteraction(int $num): void + { + Assert::assertCount($num, $this->pact['interactions']); + } + + /** + * @Then the first interaction in the pact file will contain the :fixture document as the request + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheRequest(string $fixture): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file request content type will be :contentType + */ + public function theFirstInteractionInThePactFileRequestContentTypeWillBe(string $contentType): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file will contain the :fixture document as a response + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsAResponse(string $fixture): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file response content type will be :contentType + */ + public function theFirstInteractionInThePactFileResponseContentTypeWillBe(string $contentType): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file will contain :num response messages + */ + public function theFirstInteractionInThePactFileWillContainResponseMessages(int $num): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file will contain the :fixture document as the first response message + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheFirstResponseMessage(string $fixture): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the first interaction in the pact file will contain the :fixture document as the second response message + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheSecondResponseMessage(string $fixture): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Given the message request contains the following metadata: + */ + public function theMessageRequestContainsTheFollowingMetadata(TableNode $table): void + { + throw new PendingException("Can't set sync message's metadata using FFI call"); + } + + /** + * @Then /^the received message request metadata will contain "([^"]+)" == "(.+)"$/ + */ + public function theReceivedMessageRequestMetadataWillContain(string $key, string $value): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then /^the first message in the pact file will contain the request message metadata "([^"]+)" == "(.+)"$/ + */ + public function theFirstMessageInThePactFileWillContainTheRequestMessageMetadata(string $key, string $value): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Given a provider state :state for the synchronous message is specified + */ + public function aProviderStateForTheSynchronousMessageIsSpecified(string $state): void + { + $this->message->addProviderState($state, []); + } + + /** + * @Given a provider state :state for the synchronous message is specified with the following data: + */ + public function aProviderStateForTheSynchronousMessageIsSpecifiedWithTheFollowingData(string $state, TableNode $table): void + { + $rows = $table->getHash(); + $row = reset($rows); + $this->message->addProviderState($state, $row); + } + + /** + * @Then the first message in the pact file will contain :states provider state(s) + */ + public function theFirstMessageInThePactFileWillContainProviderStates(int $states): void + { + Assert::assertCount($states, $this->pact['interactions'][0]['providerStates'] ?? []); + } + + /** + * @Then the first message in the Pact file will contain provider state :state + */ + public function theFirstMessageInThePactFileWillContainProviderState(string $state): void + { + $states = array_map(fn (array $state): string => $state['name'], $this->pact['interactions'][0]['providerStates']); + Assert::assertContains($state, $states); + } + + /** + * @Then the provider state :state for the message will contain the following parameters: + */ + public function theProviderStateForTheMessageWillContainTheFollowingParameters(string $state, TableNode $table): void + { + $params = json_decode($table->getHash()[0]['parameters'], true); + Assert::assertContains([ + 'name' => $state, + 'params' => $params, + ], $this->pact['interactions'][0]['providerStates']); + } + + /** + * @Given the message request is configured with the following: + */ + public function theMessageRequestIsConfiguredWithTheFollowing(TableNode $table): void + { + throw new PendingException("Can't set sync message's request generators using FFI call"); + } + + /** + * @Given the message response is configured with the following: + */ + public function theMessageResponseIsConfiguredWithTheFollowing(TableNode $table): void + { + throw new PendingException("Can't set sync message's response generators using FFI call"); + } + + /** + * @Then the message request contents for :path will have been replaced with a(n) :type + */ + public function theMessageRequestContentsForWillHaveBeenReplacedWithAn(string $path, string $type): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the message response contents for :path will have been replaced with a(n) :type + */ + public function theMessageResponseContentsForWillHaveBeenReplacedWithAn(string $path, string $type): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the received message request metadata will contain :key replaced with a(n) :type + */ + public function theReceivedMessageRequestMetadataWillContainReplacedWithAn(string $key, string $type): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the received message response metadata will contain :key == :value + */ + public function theReceivedMessageResponseMetadataWillContain(string $key, string $value): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the received message response metadata will contain :key replaced with an :type + */ + public function theReceivedMessageResponseMetadataWillContainReplacedWithAn(string $key, string $type): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @When the message is successfully processed + */ + public function theMessageIsSuccessfullyProcessed(): void + { + $this->thePactFileForTheTestIsGenerated(); // TODO Implement other pending steps first then update this step + } + + /** + * @Then the consumer test will have passed + */ + public function theConsumerTestWillHavePassed(): void + { + throw new PendingException('Implement previous pending step first'); + } + + /** + * @Then the received message content type will be :contentType + */ + public function theReceivedMessageContentTypeWillBe(string $contentType): void + { + throw new PendingException('Implement previous pending step first'); + } +} diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriter.php b/compatibility-suite/tests/Service/SyncMessagePactWriter.php new file mode 100644 index 00000000..d15b0ee7 --- /dev/null +++ b/compatibility-suite/tests/Service/SyncMessagePactWriter.php @@ -0,0 +1,49 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $config = new MockServerConfig(); + $config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($this->specificationVersion) + ->setPactFileWriteMode($mode); + $client = new Client(); + $pactRegistry = new PactRegistry($client); + $pactDriver = new PactDriver($client, $config, $pactRegistry); + $messageRegistry = new SyncMessageRegistry($client, $pactRegistry); + + $pactDriver->setUp(); + $messageRegistry->registerMessage($message); + $pactDriver->writePact(); + $pactDriver->cleanUp(); + } + + public function getPactPath(): string + { + return $this->pactPath; + } +} diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php b/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php new file mode 100644 index 00000000..409256dc --- /dev/null +++ b/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php @@ -0,0 +1,13 @@ +set('sync_message_pact_writer', new SyncMessagePactWriter($this->getSpecification())); + } + + protected function getSpecification(): string + { + return '4.0.0'; + } +} From 19c9938a5bf8838728e51570df6e9dc48c40157c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 24 Dec 2023 17:52:41 +0700 Subject: [PATCH 177/298] test(compatibility-suite): Add PactPath DTO --- .../tests/Context/V1/Http/ProviderContext.php | 18 +++++++---- .../tests/Context/V3/Http/ConsumerContext.php | 11 ++++--- .../tests/Context/V3/Http/ProviderContext.php | 12 ++++--- .../Context/V3/Message/ConsumerContext.php | 14 ++++---- .../Context/V3/Message/ProviderContext.php | 22 +++++++------ .../Context/V3/RequestGeneratorsContext.php | 11 ++++--- .../tests/Context/V4/CombinedContext.php | 10 +++--- .../tests/Context/V4/Http/ConsumerContext.php | 9 ++++-- .../tests/Context/V4/Http/ProviderContext.php | 20 +++++++----- .../Context/V4/Message/ConsumerContext.php | 10 ++++-- .../Context/V4/Message/ProviderContext.php | 19 ++++++----- .../Context/V4/ResponseMatchingContext.php | 6 ++-- .../V4/SyncMessage/ConsumerContext.php | 13 +++++--- compatibility-suite/tests/Model/PactPath.php | 32 +++++++++++++++++++ .../tests/Service/MessagePactWriter.php | 18 +++-------- .../Service/MessagePactWriterInterface.php | 5 ++- .../tests/Service/PactBroker.php | 12 +++---- .../tests/Service/PactWriter.php | 18 +++-------- .../tests/Service/PactWriterInterface.php | 5 ++- .../tests/Service/ProviderVerifier.php | 3 +- compatibility-suite/tests/Service/Server.php | 16 ++++------ .../tests/Service/ServerInterface.php | 3 +- .../tests/Service/SyncMessagePactWriter.php | 18 +++-------- .../SyncMessagePactWriterInterface.php | 5 ++- 24 files changed, 175 insertions(+), 135 deletions(-) create mode 100644 compatibility-suite/tests/Model/PactPath.php diff --git a/compatibility-suite/tests/Context/V1/Http/ProviderContext.php b/compatibility-suite/tests/Context/V1/Http/ProviderContext.php index 89e3ad78..e3a55924 100644 --- a/compatibility-suite/tests/Context/V1/Http/ProviderContext.php +++ b/compatibility-suite/tests/Context/V1/Http/ProviderContext.php @@ -8,6 +8,7 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\Config\PublishOptions; use PhpPact\Standalone\ProviderVerifier\Model\Source\Broker; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\PactBrokerInterface; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; @@ -41,8 +42,9 @@ public function aProviderIsStartedThatReturnsTheResponseFromInteraction(int $id) */ public function aPactFileForInteractionIsToBeVerified(int $id): void { - $this->pactWriter->write($id, "c-$id"); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $pactPath = new PactPath("c-$id"); + $this->pactWriter->write($id, $pactPath); + $this->providerVerifier->addSource($pactPath); } /** @@ -59,7 +61,8 @@ public function aProviderIsStartedThatReturnsTheResponsesFromInteractions(string */ public function aPactFileForInteractionIsToBeVerifiedFromAPactBroker(int $id): void { - $this->pactWriter->write($id, "c-$id"); + $pactPath = new PactPath("c-$id"); + $this->pactWriter->write($id, $pactPath); $this->pactBroker->publish($id); $broker = new Broker(); $broker->setUrl(new Uri('http:/localhost:9292')); @@ -107,11 +110,12 @@ public function aFailedVerificationResultWillBePublishedBackForTheInteraction(in */ public function aPactFileForInteractionIsToBeVerifiedWithAProviderStateDefined(int $id, string $state): void { - $this->pactWriter->write($id, "c-$id"); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pactPath = new PactPath("c-$id"); + $this->pactWriter->write($id, $pactPath); + $pact = json_decode(file_get_contents($pactPath), true); $pact['interactions'][0]['providerStates'][] = ['name' => $state]; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + file_put_contents($pactPath, json_encode($pact)); + $this->providerVerifier->addSource($pactPath); } /** diff --git a/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php index 6105d551..417765e1 100644 --- a/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V3/Http/ConsumerContext.php @@ -5,6 +5,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; use PhpPact\Consumer\Model\Interaction; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; @@ -14,12 +15,14 @@ final class ConsumerContext implements Context { private Interaction $interaction; private int $id = 1; + private PactPath $pactPath; public function __construct( private InteractionBuilderInterface $builder, private PactWriterInterface $pactWriter, private InteractionsStorageInterface $storage, ) { + $this->pactPath = new PactPath(); } /** @@ -48,7 +51,7 @@ public function aProviderStateIsSpecified(string $state): void */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write($this->id); + $this->pactWriter->write($this->id, $this->pactPath); } /** @@ -56,7 +59,7 @@ public function thePactFileForTheTestIsGenerated(): void */ public function theInteractionInThePactFileWillContainProviderStates(int $states): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertCount($states, $pact['interactions'][0]['providerStates']); } @@ -65,7 +68,7 @@ public function theInteractionInThePactFileWillContainProviderStates(int $states */ public function theInteractionInThePactFileWillContainProviderState(string $name): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertNotEmpty(array_filter( $pact['interactions'][0]['providerStates'], fn (array $providerState) => $providerState['name'] === $name @@ -90,7 +93,7 @@ public function theProviderStateInThePactFileWillContainTheFollowingParameters(s $rows = $table->getHash(); $row = reset($rows); $params = json_decode($row['parameters'], true); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertNotEmpty(array_filter( $pact['interactions'][0]['providerStates'], fn (array $providerState) => $providerState['name'] === $name && $providerState['params'] === $params diff --git a/compatibility-suite/tests/Context/V3/Http/ProviderContext.php b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php index 88b8b475..85d6e460 100644 --- a/compatibility-suite/tests/Context/V3/Http/ProviderContext.php +++ b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php @@ -4,6 +4,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; use PhpPactTest\CompatibilitySuite\Service\ProviderStateServerInterface; use PhpPactTest\CompatibilitySuite\Service\ProviderVerifierInterface; @@ -11,11 +12,14 @@ final class ProviderContext implements Context { + private PactPath $pactPath; + public function __construct( private PactWriterInterface $pactWriter, private ProviderStateServerInterface $providerStateServer, private ProviderVerifierInterface $providerVerifier, ) { + $this->pactPath = new PactPath(); } /** @@ -23,12 +27,12 @@ public function __construct( */ public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingProviderStatesDefined(int $id, TableNode $table): void { - $this->pactWriter->write($id); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->pactWriter->write($id, $this->pactPath); + $pact = json_decode(file_get_contents($this->pactPath), true); $rows = $table->getHash(); $pact['interactions'][0]['providerStates'] = array_map(fn (array $row): array => ['name' => $row['State Name'], 'params' => json_decode($row['Parameters'] ?? '{}', true)], $rows); - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + file_put_contents($this->pactPath, json_encode($pact)); + $this->providerVerifier->addSource($this->pactPath); } /** diff --git a/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php index cd2bd79f..21fb531e 100644 --- a/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php @@ -10,6 +10,7 @@ use PhpPact\Standalone\PactMessage\PactMessageConfig; use PhpPactTest\CompatibilitySuite\Constant\Path; use PhpPactTest\CompatibilitySuite\Model\Message; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\BodyStorageInterface; use PhpPactTest\CompatibilitySuite\Service\BodyValidatorInterface; use PhpPactTest\CompatibilitySuite\Service\FixtureLoaderInterface; @@ -23,7 +24,7 @@ final class ConsumerContext implements Context private object|null $receivedMessage; private bool $verifyResult; private array $pact; - private string $pactPath; + private PactPath $pactPath; public function __construct( private string $specificationVersion, @@ -33,15 +34,12 @@ public function __construct( private BodyStorageInterface $bodyStorage, private FixtureLoaderInterface $fixtureLoader ) { - $consumer = sprintf('compatibility-suite_message-consumer_specification-%s_c', $specificationVersion); - $provider = 'p'; - $pactDir = Path::PACTS_PATH; - $this->pactPath = "$pactDir/$consumer-$provider.json"; + $this->pactPath = new PactPath(sprintf('message_consumer_specification_%s', $specificationVersion)); $config = new PactMessageConfig(); $config - ->setConsumer($consumer) - ->setProvider($provider) - ->setPactDir($pactDir) + ->setConsumer($this->pactPath->getConsumer()) + ->setProvider($this->pactPath->getProvider()) + ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($specificationVersion) ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); $this->builder = new MessageBuilder($config); diff --git a/compatibility-suite/tests/Context/V3/Message/ProviderContext.php b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php index e62e7bcd..687a354d 100644 --- a/compatibility-suite/tests/Context/V3/Message/ProviderContext.php +++ b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php @@ -6,6 +6,7 @@ use Behat\Behat\Hook\Scope\BeforeStepScope; use Behat\Gherkin\Node\TableNode; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\FixtureLoaderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; @@ -18,6 +19,7 @@ final class ProviderContext implements Context { private int $id = 1; private array $ids = []; + private PactPath $pactPath; public function __construct( private ServerInterface $server, @@ -28,6 +30,7 @@ public function __construct( private ParserInterface $parser, private FixtureLoaderInterface $fixtureLoader ) { + $this->pactPath = new PactPath(); } /** @@ -77,8 +80,8 @@ public function registerInteractions(BeforeStepScope $scope): void */ public function aPactFileForIsToBeVerified(string $name, string $fixture): void { - $this->pactWriter->write($name, $fixture, "c-$name"); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $this->pactWriter->write($name, $fixture, $this->pactPath); + $this->providerVerifier->addSource($this->pactPath); } /** @@ -87,9 +90,9 @@ public function aPactFileForIsToBeVerified(string $name, string $fixture): void public function aPactFileForIsToBeVerifiedWithProviderState(string $name, string $fixture, string $state): void { $this->aPactFileForIsToBeVerified($name, $fixture); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['messages'][0]['providerState'] = $state; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + file_put_contents($this->pactPath, json_encode($pact)); } /** @@ -125,11 +128,10 @@ public function aProviderIsStartedThatCanGenerateTheMessageWithAndTheFollowingMe */ public function aPactFileForIsToBeVerifiedWithTheFollowingMetadata(string $name, string $fixture, TableNode $table): void { - $this->pactWriter->write($name, $fixture); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->aPactFileForIsToBeVerified($name, $fixture); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['messages'][0]['metaData'] = $this->parser->parseMetadataTable($table->getHash()); - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + file_put_contents($this->pactPath, json_encode($pact)); } /** @@ -156,11 +158,11 @@ public function aPactFileForIsToBeVerifiedWithTheFollowing(string $name, TableNo } } $this->aPactFileForIsToBeVerified($name, $body); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); if (isset($metadata)) { $pact['messages'][0]['metadata'] = array_merge($pact['messages'][0]['metadata'], $this->parser->parseMetadataMultiValues($metadata)); } $pact['messages'][0]['matchingRules'] = $matchingRules; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + file_put_contents($this->pactPath, json_encode($pact)); } } diff --git a/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php b/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php index 65959d64..3446cfeb 100644 --- a/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php +++ b/compatibility-suite/tests/Context/V3/RequestGeneratorsContext.php @@ -5,6 +5,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; use GuzzleHttp\Psr7\Uri; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\BodyStorageInterface; use PhpPactTest\CompatibilitySuite\Service\GeneratorServerInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; @@ -17,6 +18,7 @@ final class RequestGeneratorsContext implements Context { private int $id = 1; + private PactPath $pactPath; public function __construct( private InteractionBuilderInterface $builder, @@ -27,6 +29,7 @@ public function __construct( private ProviderVerifierInterface $providerVerifier, private BodyStorageInterface $bodyStorage, ) { + $this->pactPath = new PactPath(); } /** @@ -52,9 +55,9 @@ public function aRequestConfiguredWithTheFollowingGenerators(TableNode $table): public function theRequestIsPreparedForUse(): void { $this->generatorServer->start(); - $this->pactWriter->write($this->id); + $this->pactWriter->write($this->id, $this->pactPath); $this->providerVerifier->getConfig()->getProviderInfo()->setPort($this->generatorServer->getPort()); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $this->providerVerifier->addSource($this->pactPath); $this->providerVerifier->verify(); $this->generatorServer->stop(); $this->bodyStorage->setBody($this->generatorServer->getBody()); @@ -74,7 +77,7 @@ public function theGeneratorTestModeIsSetAs(string $mode): void public function theRequestIsPreparedForUseWithAProviderStateContext(TableNode $table): void { $this->generatorServer->start(); - $this->pactWriter->write($this->id); + $this->pactWriter->write($this->id, $this->pactPath); $port = $this->generatorServer->getPort(); $this->providerVerifier->getConfig()->getProviderInfo()->setPort($port); $params = json_decode($table->getRow(0)[0], true); @@ -83,7 +86,7 @@ public function theRequestIsPreparedForUseWithAProviderStateContext(TableNode $t ->getProviderState() ->setStateChangeUrl(new Uri("http://localhost:$port/return-provider-state-values?" . http_build_query($params))) ->setStateChangeTeardown(false); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $this->providerVerifier->addSource($this->pactPath); $this->providerVerifier->verify(); $this->generatorServer->stop(); $this->bodyStorage->setBody($this->generatorServer->getBody()); diff --git a/compatibility-suite/tests/Context/V4/CombinedContext.php b/compatibility-suite/tests/Context/V4/CombinedContext.php index 6a1b0bce..91415bf1 100644 --- a/compatibility-suite/tests/Context/V4/CombinedContext.php +++ b/compatibility-suite/tests/Context/V4/CombinedContext.php @@ -4,7 +4,7 @@ use Behat\Behat\Context\Context; use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Model\Interaction; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\MessagePactWriterInterface; @@ -14,6 +14,7 @@ final class CombinedContext implements Context { private int $id = 1; + private PactPath $pactPath; public function __construct( private InteractionBuilderInterface $builder, @@ -21,6 +22,7 @@ public function __construct( private PactWriterInterface $pactWriter, private MessagePactWriterInterface $messagePactWriter, ) { + $this->pactPath = new PactPath(); } /** @@ -48,8 +50,8 @@ public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write($this->id, 'c', 'p', PactConfigInterface::MODE_MERGE); - $this->messagePactWriter->write('message interaction', '', 'c', 'p', PactConfigInterface::MODE_MERGE); + $this->pactWriter->write($this->id, $this->pactPath, PactConfigInterface::MODE_MERGE); + $this->messagePactWriter->write('message interaction', '', $this->pactPath, PactConfigInterface::MODE_MERGE); } /** @@ -57,7 +59,7 @@ public function thePactFileForTheTestIsGenerated(): void */ public function thereWillBeAnInteractionInThePactFileWithATypeOf(string $type): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); $types = array_map(fn (array $interaction) => $interaction['type'], $pact['interactions']); Assert::assertContains($type, $types); } diff --git a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php index 4bab8b17..297ef737 100644 --- a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php @@ -5,6 +5,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Tester\Exception\PendingException; use PhpPact\Consumer\Model\Interaction; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; @@ -14,12 +15,14 @@ final class ConsumerContext implements Context { private Interaction $interaction; private int $id = 1; + private PactPath $pactPath; public function __construct( private InteractionBuilderInterface $builder, private PactWriterInterface $pactWriter, private InteractionsStorageInterface $storage, ) { + $this->pactPath = new PactPath(); } /** @@ -40,7 +43,7 @@ public function anHttpInteractionIsBeingDefinedForAConsumerTest(): void */ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($type, $pact['interactions'][0]['type']); } @@ -57,7 +60,7 @@ public function aKeyOfIsSpecifiedForTheHttpInteraction(string $key): void */ public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($value, $pact['interactions'][0][$name]); } @@ -82,6 +85,6 @@ public function aCommentIsAddedToTheHttpInteraction(string $value): void */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write($this->id); + $this->pactWriter->write($this->id, $this->pactPath); } } diff --git a/compatibility-suite/tests/Context/V4/Http/ProviderContext.php b/compatibility-suite/tests/Context/V4/Http/ProviderContext.php index 4f769e2a..e2a3c9f8 100644 --- a/compatibility-suite/tests/Context/V4/Http/ProviderContext.php +++ b/compatibility-suite/tests/Context/V4/Http/ProviderContext.php @@ -5,16 +5,20 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; use PhpPactTest\CompatibilitySuite\Constant\Mismatch; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; use PhpPactTest\CompatibilitySuite\Service\ProviderVerifierInterface; use PHPUnit\Framework\Assert; final class ProviderContext implements Context { + private PactPath $pactPath; + public function __construct( private PactWriterInterface $pactWriter, private ProviderVerifierInterface $providerVerifier, ) { + $this->pactPath = new PactPath(); } /** @@ -22,11 +26,11 @@ public function __construct( */ public function aPactFileForInteractionIsToBeVerifiedButIsMarkedPending(int $id): void { - $this->pactWriter->write($id); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->pactWriter->write($id, $this->pactPath); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['pending'] = true; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + file_put_contents($this->pactPath, json_encode($pact)); + $this->providerVerifier->addSource($this->pactPath); } /** @@ -81,11 +85,11 @@ public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingComments(in break; } } - $this->pactWriter->write($id); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->pactWriter->write($id, $this->pactPath); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['comments'] = $comments; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + file_put_contents($this->pactPath, json_encode($pact)); + $this->providerVerifier->addSource($this->pactPath); } /** diff --git a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php index f5e98b78..418783cd 100644 --- a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php @@ -4,14 +4,18 @@ use Behat\Behat\Context\Context; use Behat\Behat\Tester\Exception\PendingException; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\MessagePactWriterInterface; use PHPUnit\Framework\Assert; final class ConsumerContext implements Context { + private PactPath $pactPath; + public function __construct( private MessagePactWriterInterface $pactWriter ) { + $this->pactPath = new PactPath(); } /** @@ -26,7 +30,7 @@ public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write('a message', ''); + $this->pactWriter->write('a message', '', $this->pactPath); } /** @@ -34,7 +38,7 @@ public function thePactFileForTheTestIsGenerated(): void */ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($type, $pact['interactions'][0]['type']); } @@ -67,7 +71,7 @@ public function aCommentIsAddedToTheMessageInteraction(string $value): void */ public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($value, $pact['interactions'][0][$name]); } } diff --git a/compatibility-suite/tests/Context/V4/Message/ProviderContext.php b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php index 44550566..b53a4774 100644 --- a/compatibility-suite/tests/Context/V4/Message/ProviderContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php @@ -6,6 +6,7 @@ use Behat\Gherkin\Node\TableNode; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPactTest\CompatibilitySuite\Constant\Mismatch; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\MessagePactWriterInterface; @@ -16,6 +17,7 @@ final class ProviderContext implements Context { private int $id = 1; + private PactPath $pactPath; public function __construct( private ServerInterface $server, @@ -24,6 +26,7 @@ public function __construct( private MessagePactWriterInterface $pactWriter, private ProviderVerifierInterface $providerVerifier, ) { + $this->pactPath = new PactPath(); } /** @@ -58,11 +61,11 @@ public function aProviderIsStartedThatCanGenerateTheMessageWith(string $name, st */ public function aPactFileForIsToBeVerifiedButIsMarkedPending(string $name, string $fixture): void { - $this->pactWriter->write($name, $fixture); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->pactWriter->write($name, $fixture, $this->pactPath); + $this->providerVerifier->addSource($this->pactPath); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['pending'] = true; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + file_put_contents($this->pactPath, json_encode($pact)); } /** @@ -86,11 +89,11 @@ public function aPactFileForIsToBeVerifiedWithTheFollowingComments(string $name, break; } } - $this->pactWriter->write($name, $fixture); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $this->pactWriter->write($name, $fixture, $this->pactPath); + $this->providerVerifier->addSource($this->pactPath); + $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['comments'] = $comments; - file_put_contents($this->pactWriter->getPactPath(), json_encode($pact)); + file_put_contents($this->pactPath, json_encode($pact)); } /** diff --git a/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php b/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php index d878570c..fe66c372 100644 --- a/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php +++ b/compatibility-suite/tests/Context/V4/ResponseMatchingContext.php @@ -5,6 +5,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; use PhpPactTest\CompatibilitySuite\Constant\Mismatch; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\PactWriterInterface; @@ -42,8 +43,9 @@ public function anExpectedResponseConfiguredWithTheFollowing(TableNode $table): $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $this->id, $interaction); $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $this->id, $interaction); $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $row['matching rules']); - $this->pactWriter->write($this->id); - $this->providerVerifier->addSource($this->pactWriter->getPactPath()); + $pactPath = new PactPath(); + $this->pactWriter->write($this->id, $pactPath); + $this->providerVerifier->addSource($pactPath); } /** diff --git a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php index 0f86ef1a..0067a758 100644 --- a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php @@ -6,6 +6,7 @@ use Behat\Behat\Tester\Exception\PendingException; use Behat\Gherkin\Node\TableNode; use PhpPact\Consumer\Model\Message; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\SyncMessagePactWriterInterface; use PHPUnit\Framework\Assert; @@ -13,10 +14,12 @@ final class ConsumerContext implements Context { private Message $message; private array $pact; + private PactPath $pactPath; public function __construct( private SyncMessagePactWriterInterface $pactWriter, ) { + $this->pactPath = new PactPath(); } /** @@ -33,7 +36,7 @@ public function aSynchronousMessageInteractionIsBeingDefinedForAConsumerTest(): */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write($this->message); + $this->pactWriter->write($this->message, $this->pactPath); } /** @@ -41,7 +44,7 @@ public function thePactFileForTheTestIsGenerated(): void */ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($type, $pact['interactions'][0]['type']); } @@ -74,7 +77,7 @@ public function aCommentIsAddedToTheSynchronousMessageInteraction(string $value) */ public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { - $pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + $pact = json_decode(file_get_contents($this->pactPath), true); Assert::assertSame($value, $pact['interactions'][0][$name]); } @@ -107,8 +110,8 @@ public function theReceivedMessagePayloadWillContainTheDocument(string $fixture) */ public function aPactFileForTheMessageInteractionWillHaveBeenWritten(): void { - Assert::assertTrue(file_exists($this->pactWriter->getPactPath())); - $this->pact = json_decode(file_get_contents($this->pactWriter->getPactPath()), true); + Assert::assertTrue(file_exists($this->pactPath)); + $this->pact = json_decode(file_get_contents($this->pactPath), true); } /** diff --git a/compatibility-suite/tests/Model/PactPath.php b/compatibility-suite/tests/Model/PactPath.php new file mode 100644 index 00000000..4738582a --- /dev/null +++ b/compatibility-suite/tests/Model/PactPath.php @@ -0,0 +1,32 @@ +consumer; + } + + public function getProvider(): string + { + return self::PROVIDER; + } + + public function __toString(): string + { + $pactDir = Path::PACTS_PATH; + + return "$pactDir/$this->consumer-{$this->getProvider()}.json"; + } +} diff --git a/compatibility-suite/tests/Service/MessagePactWriter.php b/compatibility-suite/tests/Service/MessagePactWriter.php index 334b269b..ee220a86 100644 --- a/compatibility-suite/tests/Service/MessagePactWriter.php +++ b/compatibility-suite/tests/Service/MessagePactWriter.php @@ -7,26 +7,23 @@ use PhpPact\Consumer\Model\Message; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPactTest\CompatibilitySuite\Constant\Path; +use PhpPactTest\CompatibilitySuite\Model\PactPath; class MessagePactWriter implements MessagePactWriterInterface { - private string $pactPath; - public function __construct( private ParserInterface $parser, private string $specificationVersion, ) { } - public function write(string $name, string $body, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void + public function write(string $name, string $body, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void { - $pactDir = Path::PACTS_PATH; - $this->pactPath = "$pactDir/$consumer-$provider.json"; $config = new MockServerConfig(); $config - ->setConsumer($consumer) - ->setProvider($provider) - ->setPactDir($pactDir) + ->setConsumer($pactPath->getConsumer()) + ->setProvider($pactPath->getProvider()) + ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); $driver = (new MessageDriverFactory())->create($config); @@ -37,9 +34,4 @@ public function write(string $name, string $body, string $consumer = 'c', string $driver->registerMessage($message); $driver->writePactAndCleanUp(); } - - public function getPactPath(): string - { - return $this->pactPath; - } } diff --git a/compatibility-suite/tests/Service/MessagePactWriterInterface.php b/compatibility-suite/tests/Service/MessagePactWriterInterface.php index 4cc4ce8e..7612d0b0 100644 --- a/compatibility-suite/tests/Service/MessagePactWriterInterface.php +++ b/compatibility-suite/tests/Service/MessagePactWriterInterface.php @@ -3,10 +3,9 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; +use PhpPactTest\CompatibilitySuite\Model\PactPath; interface MessagePactWriterInterface { - public function write(string $name, string $body, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void; - - public function getPactPath(): string; + public function write(string $name, string $body, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void; } diff --git a/compatibility-suite/tests/Service/PactBroker.php b/compatibility-suite/tests/Service/PactBroker.php index 31373a25..d15a81fb 100644 --- a/compatibility-suite/tests/Service/PactBroker.php +++ b/compatibility-suite/tests/Service/PactBroker.php @@ -3,12 +3,12 @@ namespace PhpPactTest\CompatibilitySuite\Service; use GuzzleHttp\Client; +use PhpPactTest\CompatibilitySuite\Model\PactPath; final class PactBroker implements PactBrokerInterface { private Client $client; - private string $consumer; - private string $provider = 'p'; + private PactPath $pactPath; public function __construct(private string $specificationVersion) { @@ -17,9 +17,9 @@ public function __construct(private string $specificationVersion) public function publish(int $id): void { - $this->consumer = "c-$id"; - $this->client->put("http://localhost:9292/pacts/provider/$this->provider/consumer/$this->consumer/version/1.0.0", [ - 'body' => file_get_contents(__DIR__."/../../pacts/$this->consumer-$this->provider.json"), + $this->pactPath = new PactPath("c-$id"); + $this->client->put("http://localhost:9292/pacts/provider/{$this->pactPath->getProvider()}/consumer/{$this->pactPath->getConsumer()}/version/1.0.0", [ + 'body' => file_get_contents($this->pactPath), 'headers' => ['Content-Type' => 'application/json'], ]); } @@ -52,6 +52,6 @@ public function stop(): void public function getMatrix(): array { - return json_decode(file_get_contents("http://localhost:9292/matrix.json?q[][pacticipant]=$this->consumer&q[][pacticipant]=$this->provider"), true); + return json_decode(file_get_contents("http://localhost:9292/matrix.json?q[][pacticipant]={$this->pactPath->getConsumer()}&q[][pacticipant]={$this->pactPath->getProvider()}"), true); } } diff --git a/compatibility-suite/tests/Service/PactWriter.php b/compatibility-suite/tests/Service/PactWriter.php index bf57537f..6bfa2186 100644 --- a/compatibility-suite/tests/Service/PactWriter.php +++ b/compatibility-suite/tests/Service/PactWriter.php @@ -9,26 +9,23 @@ use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPactTest\CompatibilitySuite\Constant\Path; +use PhpPactTest\CompatibilitySuite\Model\PactPath; class PactWriter implements PactWriterInterface { - private string $pactPath; - public function __construct( private InteractionsStorageInterface $storage, private string $specificationVersion, ) { } - public function write(int $id, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void + public function write(int $id, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void { - $pactDir = Path::PACTS_PATH; - $this->pactPath = "$pactDir/$consumer-$provider.json"; $config = new MockServerConfig(); $config - ->setConsumer($consumer) - ->setProvider($provider) - ->setPactDir($pactDir) + ->setConsumer($pactPath->getConsumer()) + ->setProvider($pactPath->getProvider()) + ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); $client = new Client(); @@ -42,9 +39,4 @@ public function write(int $id, string $consumer = 'c', string $provider = 'p', s $pactDriver->writePact(); $pactDriver->cleanUp(); } - - public function getPactPath(): string - { - return $this->pactPath; - } } diff --git a/compatibility-suite/tests/Service/PactWriterInterface.php b/compatibility-suite/tests/Service/PactWriterInterface.php index 90b0dbb4..482fbdc4 100644 --- a/compatibility-suite/tests/Service/PactWriterInterface.php +++ b/compatibility-suite/tests/Service/PactWriterInterface.php @@ -3,10 +3,9 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; +use PhpPactTest\CompatibilitySuite\Model\PactPath; interface PactWriterInterface { - public function write(int $id, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void; - - public function getPactPath(): string; + public function write(int $id, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void; } diff --git a/compatibility-suite/tests/Service/ProviderVerifier.php b/compatibility-suite/tests/Service/ProviderVerifier.php index 111aa3ae..81ca76e0 100644 --- a/compatibility-suite/tests/Service/ProviderVerifier.php +++ b/compatibility-suite/tests/Service/ProviderVerifier.php @@ -7,6 +7,7 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; use PhpPact\Standalone\ProviderVerifier\Verifier; use PhpPactTest\CompatibilitySuite\Model\Logger; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Model\VerifyResult; final class ProviderVerifier implements ProviderVerifierInterface @@ -21,7 +22,7 @@ public function __construct() $this->config = new VerifierConfig(); $this->config ->getProviderInfo() - ->setName('p') + ->setName(PactPath::PROVIDER) ->setHost('localhost'); } diff --git a/compatibility-suite/tests/Service/Server.php b/compatibility-suite/tests/Service/Server.php index 2de267b1..2eb29a0c 100644 --- a/compatibility-suite/tests/Service/Server.php +++ b/compatibility-suite/tests/Service/Server.php @@ -15,6 +15,7 @@ use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPactTest\CompatibilitySuite\Constant\Path; use PhpPactTest\CompatibilitySuite\Model\Logger; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Model\VerifyResult; use Psr\Http\Message\UriInterface; @@ -26,21 +27,18 @@ final class Server implements ServerInterface private MockServerInterface $mockServer; private VerifyResult $verifyResult; private Logger $logger; - private string $pactPath; + private PactPath $pactPath; public function __construct( string $specificationVersion, private InteractionsStorageInterface $storage ) { - $consumer = sprintf('compatibility-suite_server_specification-%s_c', $specificationVersion); - $provider = 'p'; - $pactDir = Path::PACTS_PATH; - $this->pactPath = "$pactDir/$consumer-$provider.json"; + $this->pactPath = new PactPath(sprintf('server_specification_%s', $specificationVersion)); $this->config = new MockServerConfig(); $this->config - ->setConsumer($consumer) - ->setProvider($provider) - ->setPactDir($pactDir) + ->setConsumer($this->pactPath->getConsumer()) + ->setProvider($this->pactPath->getProvider()) + ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($specificationVersion) ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); @@ -83,7 +81,7 @@ public function getVerifyResult(): VerifyResult return $this->verifyResult; } - public function getPactPath(): string + public function getPactPath(): PactPath { return $this->pactPath; } diff --git a/compatibility-suite/tests/Service/ServerInterface.php b/compatibility-suite/tests/Service/ServerInterface.php index 2015caf6..3ba56290 100644 --- a/compatibility-suite/tests/Service/ServerInterface.php +++ b/compatibility-suite/tests/Service/ServerInterface.php @@ -2,6 +2,7 @@ namespace PhpPactTest\CompatibilitySuite\Service; +use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Model\VerifyResult; use Psr\Http\Message\UriInterface; @@ -15,7 +16,7 @@ public function verify(): void; public function getVerifyResult(): VerifyResult; - public function getPactPath(): string; + public function getPactPath(): PactPath; public function getPort(): int; } diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriter.php b/compatibility-suite/tests/Service/SyncMessagePactWriter.php index d15b0ee7..74b2e06a 100644 --- a/compatibility-suite/tests/Service/SyncMessagePactWriter.php +++ b/compatibility-suite/tests/Service/SyncMessagePactWriter.php @@ -10,25 +10,22 @@ use PhpPact\Standalone\MockService\MockServerConfig; use PhpPact\SyncMessage\Registry\Interaction\SyncMessageRegistry; use PhpPactTest\CompatibilitySuite\Constant\Path; +use PhpPactTest\CompatibilitySuite\Model\PactPath; class SyncMessagePactWriter implements SyncMessagePactWriterInterface { - private string $pactPath; - public function __construct( private string $specificationVersion, ) { } - public function write(Message $message, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void + public function write(Message $message, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void { - $pactDir = Path::PACTS_PATH; - $this->pactPath = "$pactDir/$consumer-$provider.json"; $config = new MockServerConfig(); $config - ->setConsumer($consumer) - ->setProvider($provider) - ->setPactDir($pactDir) + ->setConsumer($pactPath->getConsumer()) + ->setProvider($pactPath->getProvider()) + ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); $client = new Client(); @@ -41,9 +38,4 @@ public function write(Message $message, string $consumer = 'c', string $provider $pactDriver->writePact(); $pactDriver->cleanUp(); } - - public function getPactPath(): string - { - return $this->pactPath; - } } diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php b/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php index 409256dc..8ef29920 100644 --- a/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php +++ b/compatibility-suite/tests/Service/SyncMessagePactWriterInterface.php @@ -4,10 +4,9 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Model\Message; +use PhpPactTest\CompatibilitySuite\Model\PactPath; interface SyncMessagePactWriterInterface { - public function write(Message $message, string $consumer = 'c', string $provider = 'p', string $mode = PactConfigInterface::MODE_OVERWRITE): void; - - public function getPactPath(): string; + public function write(Message $message, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void; } From f034d33ea713d88b23e602a9d255ed483b11bc5f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 24 Dec 2023 23:46:44 +0700 Subject: [PATCH 178/298] test(compatibility-suite): Remove PactPath::getProvider() method --- .../tests/Context/V3/Message/ConsumerContext.php | 2 +- compatibility-suite/tests/Model/PactPath.php | 9 +-------- compatibility-suite/tests/Service/MessagePactWriter.php | 2 +- compatibility-suite/tests/Service/PactBroker.php | 4 ++-- compatibility-suite/tests/Service/PactWriter.php | 2 +- compatibility-suite/tests/Service/Server.php | 2 +- .../tests/Service/SyncMessagePactWriter.php | 2 +- 7 files changed, 8 insertions(+), 15 deletions(-) diff --git a/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php index 21fb531e..a50dfc15 100644 --- a/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V3/Message/ConsumerContext.php @@ -38,7 +38,7 @@ public function __construct( $config = new PactMessageConfig(); $config ->setConsumer($this->pactPath->getConsumer()) - ->setProvider($this->pactPath->getProvider()) + ->setProvider(PactPath::PROVIDER) ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($specificationVersion) ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); diff --git a/compatibility-suite/tests/Model/PactPath.php b/compatibility-suite/tests/Model/PactPath.php index 4738582a..d2293c08 100644 --- a/compatibility-suite/tests/Model/PactPath.php +++ b/compatibility-suite/tests/Model/PactPath.php @@ -18,15 +18,8 @@ public function getConsumer(): string return $this->consumer; } - public function getProvider(): string - { - return self::PROVIDER; - } - public function __toString(): string { - $pactDir = Path::PACTS_PATH; - - return "$pactDir/$this->consumer-{$this->getProvider()}.json"; + return sprintf("%s/%s-%s.json", Path::PACTS_PATH, $this->consumer, self::PROVIDER); } } diff --git a/compatibility-suite/tests/Service/MessagePactWriter.php b/compatibility-suite/tests/Service/MessagePactWriter.php index ee220a86..cdbe2500 100644 --- a/compatibility-suite/tests/Service/MessagePactWriter.php +++ b/compatibility-suite/tests/Service/MessagePactWriter.php @@ -22,7 +22,7 @@ public function write(string $name, string $body, PactPath $pactPath, string $mo $config = new MockServerConfig(); $config ->setConsumer($pactPath->getConsumer()) - ->setProvider($pactPath->getProvider()) + ->setProvider(PactPath::PROVIDER) ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); diff --git a/compatibility-suite/tests/Service/PactBroker.php b/compatibility-suite/tests/Service/PactBroker.php index d15a81fb..4887e749 100644 --- a/compatibility-suite/tests/Service/PactBroker.php +++ b/compatibility-suite/tests/Service/PactBroker.php @@ -18,7 +18,7 @@ public function __construct(private string $specificationVersion) public function publish(int $id): void { $this->pactPath = new PactPath("c-$id"); - $this->client->put("http://localhost:9292/pacts/provider/{$this->pactPath->getProvider()}/consumer/{$this->pactPath->getConsumer()}/version/1.0.0", [ + $this->client->put(sprintf('http://localhost:9292/pacts/provider/%s/consumer/%s/version/1.0.0', PactPath::PROVIDER, $this->pactPath->getConsumer()), [ 'body' => file_get_contents($this->pactPath), 'headers' => ['Content-Type' => 'application/json'], ]); @@ -52,6 +52,6 @@ public function stop(): void public function getMatrix(): array { - return json_decode(file_get_contents("http://localhost:9292/matrix.json?q[][pacticipant]={$this->pactPath->getConsumer()}&q[][pacticipant]={$this->pactPath->getProvider()}"), true); + return json_decode(file_get_contents(sprintf('http://localhost:9292/matrix.json?q[][pacticipant]=%s&q[][pacticipant]=%s', $this->pactPath->getConsumer(), PactPath::PROVIDER)), true); } } diff --git a/compatibility-suite/tests/Service/PactWriter.php b/compatibility-suite/tests/Service/PactWriter.php index 6bfa2186..e56a2a04 100644 --- a/compatibility-suite/tests/Service/PactWriter.php +++ b/compatibility-suite/tests/Service/PactWriter.php @@ -24,7 +24,7 @@ public function write(int $id, PactPath $pactPath, string $mode = PactConfigInte $config = new MockServerConfig(); $config ->setConsumer($pactPath->getConsumer()) - ->setProvider($pactPath->getProvider()) + ->setProvider(PactPath::PROVIDER) ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); diff --git a/compatibility-suite/tests/Service/Server.php b/compatibility-suite/tests/Service/Server.php index 2eb29a0c..6f7d90cf 100644 --- a/compatibility-suite/tests/Service/Server.php +++ b/compatibility-suite/tests/Service/Server.php @@ -37,7 +37,7 @@ public function __construct( $this->config = new MockServerConfig(); $this->config ->setConsumer($this->pactPath->getConsumer()) - ->setProvider($this->pactPath->getProvider()) + ->setProvider(PactPath::PROVIDER) ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($specificationVersion) ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriter.php b/compatibility-suite/tests/Service/SyncMessagePactWriter.php index 74b2e06a..f7b2b923 100644 --- a/compatibility-suite/tests/Service/SyncMessagePactWriter.php +++ b/compatibility-suite/tests/Service/SyncMessagePactWriter.php @@ -24,7 +24,7 @@ public function write(Message $message, PactPath $pactPath, string $mode = PactC $config = new MockServerConfig(); $config ->setConsumer($pactPath->getConsumer()) - ->setProvider($pactPath->getProvider()) + ->setProvider(PactPath::PROVIDER) ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); From d9b5b471b4cf70126a36ed4fd954f8e99db3e98f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 26 Dec 2023 23:59:18 +0700 Subject: [PATCH 179/298] refactor: Move xml to core --- composer.json | 1 - .../tests/Service/HttpClientServiceTest.php | 2 +- .../Xml/Model/Builder/ElementTrait.php | 54 +++ .../Xml/Model/Builder/GeneratorTrait.php | 21 ++ .../Xml/Model/Builder/MatcherTrait.php | 21 ++ src/PhpPact/Xml/Model/Builder/TextTrait.php | 35 ++ src/PhpPact/Xml/Model/Matcher/Generator.php | 43 +++ src/PhpPact/Xml/Model/Matcher/Matcher.php | 43 +++ src/PhpPact/Xml/XmlBuilder.php | 32 ++ src/PhpPact/Xml/XmlElement.php | 113 ++++++ src/PhpPact/Xml/XmlText.php | 71 ++++ .../PhpPact/Xml/XmlBuilderIntegrationTest.php | 336 ++++++++++++++++++ tests/PhpPact/Xml/XmlBuilderTest.php | 110 ++++++ tests/PhpPact/Xml/XmlElementTest.php | 86 +++++ tests/PhpPact/Xml/XmlTextTest.php | 73 ++++ 15 files changed, 1039 insertions(+), 2 deletions(-) create mode 100644 src/PhpPact/Xml/Model/Builder/ElementTrait.php create mode 100644 src/PhpPact/Xml/Model/Builder/GeneratorTrait.php create mode 100644 src/PhpPact/Xml/Model/Builder/MatcherTrait.php create mode 100644 src/PhpPact/Xml/Model/Builder/TextTrait.php create mode 100644 src/PhpPact/Xml/Model/Matcher/Generator.php create mode 100644 src/PhpPact/Xml/Model/Matcher/Matcher.php create mode 100644 src/PhpPact/Xml/XmlBuilder.php create mode 100644 src/PhpPact/Xml/XmlElement.php create mode 100644 src/PhpPact/Xml/XmlText.php create mode 100644 tests/PhpPact/Xml/XmlBuilderIntegrationTest.php create mode 100644 tests/PhpPact/Xml/XmlBuilderTest.php create mode 100644 tests/PhpPact/Xml/XmlElementTest.php create mode 100644 tests/PhpPact/Xml/XmlTextTest.php diff --git a/composer.json b/composer.json index f53a4b8f..577c5b37 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,6 @@ "phpstan/phpstan": "^1.9", "phpunit/phpunit": ">=8.5.23 <10", "guzzlehttp/guzzle": "^7.8", - "tienvx/pact-php-xml": "^0.1", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", "ramsey/uuid": "^4.7" diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index c4e31582..1ed429e7 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -2,7 +2,7 @@ namespace XmlConsumer\Tests\Service; -use Tienvx\PactPhpXml\XmlBuilder; +use PhpPact\Xml\XmlBuilder; use XmlConsumer\Service\HttpClientService; use PhpPact\Consumer\InteractionBuilder; use PhpPact\Consumer\Model\ConsumerRequest; diff --git a/src/PhpPact/Xml/Model/Builder/ElementTrait.php b/src/PhpPact/Xml/Model/Builder/ElementTrait.php new file mode 100644 index 00000000..cffc7fc0 --- /dev/null +++ b/src/PhpPact/Xml/Model/Builder/ElementTrait.php @@ -0,0 +1,54 @@ +root = new XmlElement(...$options); + } + + public function add(callable ...$options): callable + { + return fn (XmlElement $element) => $element->addChild(new XmlElement(...$options)); + } + + public function name(string $name): callable + { + return fn (XmlElement $element) => $element->setName($name); + } + + public function text(callable ...$options): callable + { + return fn (XmlElement $element) => $element->setText(new XmlText(...$options)); + } + + public function attribute(string $name, mixed $value): callable + { + return fn (XmlElement $element) => $element->addAttribute($name, $value); + } + + public function eachLike(int $min = 1, ?int $max = null, int $examples = 1): callable + { + return function (XmlElement $element) use ($min, $max, $examples): void { + $options = [ + 'min' => $min, + 'examples' => $examples, + ]; + if (isset($max)) { + $options['max'] = $max; + } + $element->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('type'), + fn (Matcher $matcher) => $matcher->setOptions($options), + )); + }; + } +} diff --git a/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php b/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php new file mode 100644 index 00000000..32a5be4e --- /dev/null +++ b/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php @@ -0,0 +1,21 @@ + $generator->setType($type); + } + + /** + * @param array $options + */ + public function generatorOptions(array $options): callable + { + return fn (Generator $generator) => $generator->setOptions($options); + } +} diff --git a/src/PhpPact/Xml/Model/Builder/MatcherTrait.php b/src/PhpPact/Xml/Model/Builder/MatcherTrait.php new file mode 100644 index 00000000..4e38d0e8 --- /dev/null +++ b/src/PhpPact/Xml/Model/Builder/MatcherTrait.php @@ -0,0 +1,21 @@ + $matcher->setType($type); + } + + /** + * @param array $options + */ + public function matcherOptions(array $options): callable + { + return fn (Matcher $matcher) => $matcher->setOptions($options); + } +} diff --git a/src/PhpPact/Xml/Model/Builder/TextTrait.php b/src/PhpPact/Xml/Model/Builder/TextTrait.php new file mode 100644 index 00000000..bd785fc6 --- /dev/null +++ b/src/PhpPact/Xml/Model/Builder/TextTrait.php @@ -0,0 +1,35 @@ + $text->setContent($content); + } + + public function matcher(callable ...$options): callable + { + return fn (XmlText $text) => $text->setMatcher(new Matcher(...$options)); + } + + public function generator(callable ...$options): callable + { + return fn (XmlText $text) => $text->setGenerator(new Generator(...$options)); + } + + public function contentLike(string|int|float $content): callable + { + return function (XmlText $text) use ($content): void { + $text->setContent($content); + $text->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('type') + )); + }; + } +} diff --git a/src/PhpPact/Xml/Model/Matcher/Generator.php b/src/PhpPact/Xml/Model/Matcher/Generator.php new file mode 100644 index 00000000..5419223f --- /dev/null +++ b/src/PhpPact/Xml/Model/Matcher/Generator.php @@ -0,0 +1,43 @@ + + */ + protected array $options = []; + + public function __construct(callable ...$options) + { + array_walk($options, fn (callable $option) => $option($this)); + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + /** + * @param array $options + */ + public function setOptions(array $options): self + { + $this->options = $options; + + return $this; + } + + /** + * @return array + */ + public function getArray(): array + { + return ['pact:generator:type' => $this->type] + $this->options; + } +} diff --git a/src/PhpPact/Xml/Model/Matcher/Matcher.php b/src/PhpPact/Xml/Model/Matcher/Matcher.php new file mode 100644 index 00000000..4e7e3a4b --- /dev/null +++ b/src/PhpPact/Xml/Model/Matcher/Matcher.php @@ -0,0 +1,43 @@ + + */ + protected array $options = []; + + public function __construct(callable ...$options) + { + array_walk($options, fn (callable $option) => $option($this)); + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + /** + * @param array $options + */ + public function setOptions(array $options): self + { + $this->options = $options; + + return $this; + } + + /** + * @return array + */ + public function getArray(): array + { + return ['pact:matcher:type' => $this->type] + $this->options; + } +} diff --git a/src/PhpPact/Xml/XmlBuilder.php b/src/PhpPact/Xml/XmlBuilder.php new file mode 100644 index 00000000..76260fda --- /dev/null +++ b/src/PhpPact/Xml/XmlBuilder.php @@ -0,0 +1,32 @@ + + */ + public function getArray(): array + { + return [ + 'version' => $this->version, + 'charset' => $this->charset, + 'root' => $this->root->getArray(), + ]; + } +} diff --git a/src/PhpPact/Xml/XmlElement.php b/src/PhpPact/Xml/XmlElement.php new file mode 100644 index 00000000..31eb9c00 --- /dev/null +++ b/src/PhpPact/Xml/XmlElement.php @@ -0,0 +1,113 @@ + + */ + private array $attributes = []; + + private ?XmlText $text = null; + + private ?Matcher $matcher = null; + + private ?Generator $generator = null; + + public function __construct(callable ...$options) + { + array_walk($options, fn (callable $option) => $option($this)); + } + + public function setName(string $name): self + { + $this->name = preg_replace('/(^[0-9]+|[^a-zA-Z0-9\-\_\:]+)/', '', $name); + + return $this; + } + + public function addChild(self $child): self + { + $this->children[] = $child; + + return $this; + } + + public function setText(?XmlText $text): self + { + $this->text = $text; + + return $this; + } + + public function setMatcher(?Matcher $matcher): self + { + $this->matcher = $matcher; + + return $this; + } + + public function setGenerator(?Generator $generator): self + { + $this->generator = $generator; + + return $this; + } + + public function addAttribute(string $name, mixed $value): self + { + $this->attributes[$name] = $value; + + return $this; + } + + /** + * @return array + */ + public function getArray(): array + { + if ($this->matcher) { + $result = [ + 'value' => $this->getBaseArray(), + ]; + $result += $this->matcher->getArray(); + + if ($this->generator) { + $result += $this->generator->getArray(); + } + } else { + $result = $this->getBaseArray(); + } + + return $result; + } + + /** + * @return array + */ + private function getBaseArray(): array + { + $result = [ + 'name' => $this->name, + 'children' => array_map(fn (XmlElement $element) => $element->getArray(), $this->children), + 'attributes' => $this->attributes + ]; + + if ($this->text) { + $result['children'][] = $this->text->getArray(); + } + + return $result; + } +} diff --git a/src/PhpPact/Xml/XmlText.php b/src/PhpPact/Xml/XmlText.php new file mode 100644 index 00000000..435cfc12 --- /dev/null +++ b/src/PhpPact/Xml/XmlText.php @@ -0,0 +1,71 @@ + $option($this)); + } + + public function setContent(string|int|float $content): self + { + $this->content = $content; + + return $this; + } + + public function setMatcher(?Matcher $matcher): self + { + $this->matcher = $matcher; + + return $this; + } + + public function setGenerator(?Generator $generator): self + { + $this->generator = $generator; + + return $this; + } + + /** + * @return array + */ + public function getArray(): array + { + $result = $this->getBaseArray(); + + if ($this->matcher) { + $result['matcher'] = $this->matcher->getArray(); + } + + if ($this->generator) { + $generator = $this->generator->getArray(); + $result['pact:generator:type'] = $generator['pact:generator:type']; + $result['matcher'] += array_diff_key($generator, array_flip(['pact:generator:type'])); + } + + return $result; + } + + /** + * @return array + */ + private function getBaseArray(): array + { + return [ + 'content' => $this->content, + ]; + } +} diff --git a/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php b/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php new file mode 100644 index 00000000..58040aab --- /dev/null +++ b/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php @@ -0,0 +1,336 @@ +builder = new XmlBuilder('1.0', 'UTF-8'); + $this->matcher = new Matcher(); + } + + public function testBuildWithMatchersOnAttributes(): void + { + $this->builder + ->root( + $this->builder->name('ns1:projects'), + $this->builder->attribute('id', '1234'), + $this->builder->attribute('xmlns:ns1', 'http://some.namespace/and/more/stuff'), + $this->builder->add( + $this->builder->eachLike(examples: 2), + $this->builder->name('ns1:project'), + $this->builder->attribute('id', $this->matcher->integerV3(1)), + $this->builder->attribute('type', 'activity'), + $this->builder->attribute('name', $this->matcher->string('Project 1')), + $this->builder->attribute('due', $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss.SZ", '2016-02-11T09:46:56.023Z')), + $this->builder->add( + $this->builder->name('ns1:tasks'), + $this->builder->add( + $this->builder->eachLike(examples: 5), + $this->builder->name('ns1:task'), + $this->builder->attribute('id', $this->matcher->integerV3(1)), + $this->builder->attribute('name', $this->matcher->string('Task 1')), + $this->builder->attribute('done', $this->matcher->boolean()), + ), + ), + ), + ); + + $expectedArray = [ + 'version' => '1.0', + 'charset' => 'UTF-8', + 'root' => [ + 'name' => 'ns1:projects', + 'children' => [ + [ + 'value' => [ + 'name' => 'ns1:project', + 'children' => [ + [ + 'name' => 'ns1:tasks', + 'children' => [ + [ + 'value' => [ + 'name' => 'ns1:task', + 'children' => [], + 'attributes' => [ + 'id' => [ + 'pact:matcher:type' => + 'integer', + 'value' => 1, + ], + 'name' => [ + 'pact:matcher:type' => 'type', + 'value' => 'Task 1', + ], + 'done' => [ + 'pact:matcher:type' => 'type', + 'value' => true, + ], + ], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 5, + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [ + 'id' => [ + 'pact:matcher:type' => 'integer', + 'value' => 1, + ], + 'type' => 'activity', + 'name' => [ + 'pact:matcher:type' => 'type', + 'value' => 'Project 1', + ], + 'due' => [ + 'pact:matcher:type' => 'datetime', + 'format' => "yyyy-MM-dd'T'HH:mm:ss.SZ", + 'value' => '2016-02-11T09:46:56.023Z', + ], + ], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 2, + ], + ], + 'attributes' => [ + 'id' => '1234', + 'xmlns:ns1' => 'http://some.namespace/and/more/stuff', + ], + ], + ]; + + $this->assertSame(json_encode($expectedArray), json_encode($this->builder->getArray())); + } + + public function testBuildWithMatchersOnContent(): void + { + $this->builder + ->root( + $this->builder->name('movies'), + $this->builder->add( + $this->builder->eachLike(), + $this->builder->name('movie'), + $this->builder->add( + $this->builder->name('title'), + $this->builder->text( + $this->builder->contentLike('PHP: Behind the Parser'), + ), + ), + $this->builder->add( + $this->builder->name('characters'), + $this->builder->add( + $this->builder->eachLike(examples: 2), + $this->builder->name('character'), + $this->builder->add( + $this->builder->name('name'), + $this->builder->text( + $this->builder->contentLike('Ms. Coder'), + ), + ), + $this->builder->add( + $this->builder->name('actor'), + $this->builder->text( + $this->builder->contentLike('Onlivia Actora'), + ), + ), + ), + ), + $this->builder->add( + $this->builder->name('plot'), + $this->builder->text( + $this->builder->contentLike( + $plot = <<builder->add( + $this->builder->name('great-lines'), + $this->builder->add( + $this->builder->eachLike(), + $this->builder->name('line'), + $this->builder->text( + $this->builder->contentLike('PHP solves all my web problems'), + ), + ), + ), + $this->builder->add( + $this->builder->name('rating'), + $this->builder->attribute('type', 'thumbs'), + $this->builder->text( + $this->builder->contentLike(7), + ), + ), + $this->builder->add( + $this->builder->name('rating'), + $this->builder->attribute('type', 'stars'), + $this->builder->text( + $this->builder->contentLike(5), + ), + ), + ), + ); + + $expectedArray = [ + 'version' => '1.0', + 'charset' => 'UTF-8', + 'root' => [ + 'name' => 'movies', + 'children' => [ + [ + 'value' => [ + 'name' => 'movie', + 'children' => [ + [ + 'name' => 'title', + 'children' => [ + [ + 'content' => 'PHP: Behind the Parser', + 'matcher' => [ + 'pact:matcher:type' => 'type', + ], + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'characters', + 'children' => [ + [ + 'value' => [ + 'name' => 'character', + 'children' => [ + [ + 'name' => 'name', + 'children' => [ + [ + 'content' => + 'Ms. Coder', + 'matcher' => [ + 'pact:matcher:type' => + 'type', + ], + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'actor', + 'children' => [ + [ + 'content' => + 'Onlivia Actora', + 'matcher' => [ + 'pact:matcher:type' => + 'type', + ], + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 2, + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'plot', + 'children' => [ + [ + 'content' => $plot, + 'matcher' => [ + 'pact:matcher:type' => 'type', + ], + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'great-lines', + 'children' => [ + [ + 'value' => [ + 'name' => 'line', + 'children' => [ + [ + 'content' => + 'PHP solves all my web problems', + 'matcher' => [ + 'pact:matcher:type' => + 'type', + ], + ], + ], + 'attributes' => [], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 1, + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'rating', + 'children' => [ + [ + 'content' => 7, + 'matcher' => [ + 'pact:matcher:type' => 'type', + ], + ], + ], + 'attributes' => ['type' => 'thumbs'], + ], + [ + 'name' => 'rating', + 'children' => [ + [ + 'content' => 5, + 'matcher' => [ + 'pact:matcher:type' => 'type', + ], + ], + ], + 'attributes' => ['type' => 'stars'], + ], + ], + 'attributes' => [], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 1, + ], + ], + 'attributes' => [], + ], + ]; + + $this->assertSame(json_encode($expectedArray), json_encode($this->builder->getArray())); + } +} diff --git a/tests/PhpPact/Xml/XmlBuilderTest.php b/tests/PhpPact/Xml/XmlBuilderTest.php new file mode 100644 index 00000000..ac8ad8cc --- /dev/null +++ b/tests/PhpPact/Xml/XmlBuilderTest.php @@ -0,0 +1,110 @@ +root( + $builder->name('Root'), + $builder->add( + $builder->name('First Child Second Element'), + $builder->text( + $builder->contentLike('Example Test') + ) + ), + $builder->add( + $builder->name('Second Parent'), + $builder->add( + $builder->name('Second child 1'), + $builder->attribute('myAttr', 'Attr Value') + ), + $builder->add( + $builder->name('Second child 2'), + $builder->text( + $builder->content('Test') + ) + ), + $builder->add( + $builder->name('Third Parent'), + $builder->add( + $builder->eachLike(), + $builder->name('Child') + ) + ), + ), + $builder->add( + $builder->name('First Child Third Element'), + ), + ) + ; + + $expectedArray = [ + 'version' => '1.0', + 'charset' => 'UTF-8', + 'root' => [ + 'name' => 'Root', + 'children' => [ + [ + 'name' => 'FirstChildSecondElement', + 'children' => [ + [ + 'content' => 'Example Test', + 'matcher' => ['pact:matcher:type' => 'type'], + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'SecondParent', + 'children' => [ + [ + 'name' => 'Secondchild1', + 'children' => [], + 'attributes' => ['myAttr' => 'Attr Value'], + ], + [ + 'name' => 'Secondchild2', + 'children' => [['content' => 'Test']], + 'attributes' => [], + ], + [ + 'name' => 'ThirdParent', + 'children' => [ + [ + 'value' => [ + 'name' => 'Child', + 'children' => [], + 'attributes' => [], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + 'examples' => 1, + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + [ + 'name' => 'FirstChildThirdElement', + 'children' => [], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + ]; + + + $this->assertSame(json_encode($expectedArray), json_encode($builder->getArray())); + } +} diff --git a/tests/PhpPact/Xml/XmlElementTest.php b/tests/PhpPact/Xml/XmlElementTest.php new file mode 100644 index 00000000..66fa3127 --- /dev/null +++ b/tests/PhpPact/Xml/XmlElementTest.php @@ -0,0 +1,86 @@ +element = new XmlElement(); + $this->element->setName('Child'); + $this->element->addAttribute('myAttr', 'attr-value'); + } + + public function testGetMatcherArray(): void + { + $this->element->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('type'), + fn (Matcher $matcher) => $matcher->setOptions(['examples' => 7]), + )); + + $this->assertSame( + json_encode([ + 'value' => [ + 'name' => 'Child', + 'children' => [], + 'attributes' => [ + 'myAttr' => 'attr-value', + ], + ], + 'pact:matcher:type' => 'type', + 'examples' => 7, + ]), + json_encode($this->element->getArray()) + ); + } + + public function testGetGeneratorArray(): void + { + $this->element->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('type'), + fn (Matcher $matcher) => $matcher->setOptions(['examples' => 7]), + )); + $this->element->setGenerator(new Generator( + fn (Generator $generator) => $generator->setType('Uuid'), + fn (Generator $generator) => $generator->setOptions(['format' => 'simple']), + )); + + $this->assertSame( + json_encode([ + 'value' => [ + 'name' => 'Child', + 'children' => [], + 'attributes' => [ + 'myAttr' => 'attr-value', + ] + ], + 'pact:matcher:type' => 'type', + 'examples' => 7, + 'pact:generator:type' => 'Uuid', + 'format' => 'simple', + ]), + json_encode($this->element->getArray()) + ); + } + + public function testGetBaseArray(): void + { + $this->assertSame( + json_encode([ + 'name' => 'Child', + 'children' => [], + 'attributes' => [ + 'myAttr' => 'attr-value', + ] + ]), + json_encode($this->element->getArray()) + ); + } +} diff --git a/tests/PhpPact/Xml/XmlTextTest.php b/tests/PhpPact/Xml/XmlTextTest.php new file mode 100644 index 00000000..7371664e --- /dev/null +++ b/tests/PhpPact/Xml/XmlTextTest.php @@ -0,0 +1,73 @@ +text = new XmlText(); + $this->text->setContent('testing'); + } + + public function testGetMatcherArray(): void + { + $this->text->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('include'), + fn (Matcher $matcher) => $matcher->setOptions(['value' => "te"]), + )); + + $this->assertSame( + json_encode([ + 'content' => 'testing', + 'matcher' => [ + 'pact:matcher:type' => 'include', + 'value' => 'te', + ] + ]), + json_encode($this->text->getArray()) + ); + } + + public function testGetGeneratorArray(): void + { + $this->text->setContent(7); + $this->text->setMatcher(new Matcher( + fn (Matcher $matcher) => $matcher->setType('integer'), + )); + $this->text->setGenerator(new Generator( + fn (Generator $generator) => $generator->setType('RandomInt'), + fn (Generator $generator) => $generator->setOptions(['min' => 2, 'max' => 8]), + )); + + $this->assertSame( + json_encode([ + 'content' => 7, + 'matcher' => [ + 'pact:matcher:type' => 'integer', + 'min' => 2, + 'max' => 8, + ], + 'pact:generator:type' => 'RandomInt' + ]), + json_encode($this->text->getArray()) + ); + } + + public function testGetBaseArray(): void + { + $this->assertSame( + json_encode([ + 'content' => 'testing', + ]), + json_encode($this->text->getArray()) + ); + } +} From c95ffcb591acb118be4ca39cd227e804d40aab65 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 29 Dec 2023 21:07:24 +0700 Subject: [PATCH 180/298] chore: Check string generator is working --- example/generators/consumer/tests/Service/GeneratorsTest.php | 2 ++ src/PhpPact/Consumer/Matcher/Matchers/StringValue.php | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index 0bd2846d..eca73caa 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -7,6 +7,7 @@ use PhpPact\Consumer\InteractionBuilder; use PhpPact\Consumer\Matcher\HttpStatus; use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Matcher\Matchers\StringValue; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; use PhpPact\Standalone\MockService\MockServerConfig; @@ -93,6 +94,7 @@ public function testGetGenerators(): void $this->assertTrue($this->validateDateTime($body['time'], 'H:i:s')); $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-d\TH:i:s")); $this->assertIsString($body['string']); + $this->assertNotSame(StringValue::DEFAULT_VALUE, $body['string']); $this->assertIsNumeric($body['number']); $this->assertNotSame('http://localhost/users/1234/posts/latest', $body['url']); $this->assertRegExp('/.*(\\/users\\/\\d+\\/posts\\/latest)$/', $body['url']); diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php index 6cbc4bc2..1459d9b0 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php @@ -9,6 +9,8 @@ */ class StringValue extends GeneratorAwareMatcher { + public const DEFAULT_VALUE = 'some string'; + public function __construct(private ?string $value = null) { if ($value === null) { @@ -28,7 +30,7 @@ public function jsonSerialize(): array { $data = [ 'pact:matcher:type' => $this->getType(), - 'value' => $this->getValue() ?? 'some string', + 'value' => $this->getValue() ?? self::DEFAULT_VALUE, ]; if ($this->getGenerator()) { From a98ca6ffe7d35543dab2905385c0aa7354469e20 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 31 Dec 2023 15:25:58 +0700 Subject: [PATCH 181/298] refactor: Move code from matchers to formatters --- .../Matcher/Formatters/MinimalFormatter.php | 20 ++++++++ .../Formatters/ValueOptionalFormatter.php | 26 ++++++++++ .../Formatters/ValueRequiredFormatter.php | 17 +++++++ .../Matcher/Matchers/AbstractDateTime.php | 1 + .../Matcher/Matchers/AbstractMatcher.php | 49 +++++++++++++++++++ .../Matcher/Matchers/ArrayContains.php | 20 +++++--- .../Consumer/Matcher/Matchers/Boolean.php | 1 + .../Consumer/Matcher/Matchers/ContentType.php | 20 ++++---- .../Consumer/Matcher/Matchers/Decimal.php | 1 + .../Consumer/Matcher/Matchers/EachKey.php | 21 +++++--- .../Consumer/Matcher/Matchers/EachValue.php | 21 +++++--- .../Consumer/Matcher/Matchers/Equality.php | 19 +++---- .../Matchers/GeneratorAwareMatcher.php | 29 ++--------- .../Consumer/Matcher/Matchers/Includes.php | 20 ++++---- .../Consumer/Matcher/Matchers/Integer.php | 1 + .../Consumer/Matcher/Matchers/MaxType.php | 25 +++++----- .../Consumer/Matcher/Matchers/MinMaxType.php | 23 +++++---- .../Consumer/Matcher/Matchers/MinType.php | 27 +++++++--- .../Consumer/Matcher/Matchers/NotEmpty.php | 19 +++---- .../Consumer/Matcher/Matchers/NullValue.php | 26 ++++++---- .../Consumer/Matcher/Matchers/Number.php | 1 + .../Consumer/Matcher/Matchers/Regex.php | 1 + .../Consumer/Matcher/Matchers/Semver.php | 1 + .../Consumer/Matcher/Matchers/StatusCode.php | 1 + .../Consumer/Matcher/Matchers/StringValue.php | 17 ++----- .../Consumer/Matcher/Matchers/Type.php | 19 +++---- .../Consumer/Matcher/Matchers/Values.php | 19 +++---- .../Matcher/Model/FormatterAwareInterface.php | 10 ++++ .../Matcher/Model/FormatterInterface.php | 11 +++++ .../Matcher/Model/MatcherInterface.php | 2 + .../Formatters/MinimalFormatterTest.php | 28 +++++++++++ .../Formatters/ValueOptionalFormatterTest.php | 25 ++++++++++ .../Formatters/ValueRequiredFormatterTest.php | 25 ++++++++++ .../Matcher/Matchers/ContentTypeTest.php | 2 +- .../Consumer/Matcher/Matchers/EachKeyTest.php | 2 +- .../Matcher/Matchers/EachValueTest.php | 2 +- .../Matcher/Matchers/StringValueTest.php | 2 +- 37 files changed, 394 insertions(+), 160 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/FormatterAwareInterface.php create mode 100644 src/PhpPact/Consumer/Matcher/Model/FormatterInterface.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/MinimalFormatterTest.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php diff --git a/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php new file mode 100644 index 00000000..61eb095d --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php @@ -0,0 +1,20 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + { + return [ + 'pact:matcher:type' => $matcher->getType(), + ] + $matcher->getAttributes()->getData(); + } +} diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php new file mode 100644 index 00000000..30aea26d --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php @@ -0,0 +1,26 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + { + $data = [ + 'pact:matcher:type' => $matcher->getType(), + ]; + + if ($generator) { + return $data + ['pact:generator:type' => $generator->getType()] + $matcher->getAttributes()->merge($generator->getAttributes())->getData(); + } + + return $data + $matcher->getAttributes()->getData() + ['value' => $value]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php new file mode 100644 index 00000000..a1285a6e --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php @@ -0,0 +1,17 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + { + return parent::format($matcher, $generator, $value) + ['value' => $value]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php index 47c4ec28..c11dbad9 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php @@ -6,6 +6,7 @@ abstract class AbstractDateTime extends GeneratorAwareMatcher { public function __construct(protected string $format, private ?string $value = null) { + parent::__construct(); } /** diff --git a/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php b/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php new file mode 100644 index 00000000..021ae779 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php @@ -0,0 +1,49 @@ +formatter = new ValueOptionalFormatter(); + } + + public function setFormatter(FormatterInterface $formatter): void + { + $this->formatter = $formatter; + } + + public function getFormatter(): FormatterInterface + { + return $this->formatter; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->getFormatter()->format($this, null, $this->getValue()); + } + + public function getAttributes(): Attributes + { + return new Attributes($this, $this->getAttributesData()); + } + + /** + * @return array + */ + abstract protected function getAttributesData(): array; + + abstract protected function getValue(): mixed; +} diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php index da9368df..f5f04df2 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php @@ -2,29 +2,35 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; +use PhpPact\Consumer\Matcher\Formatters\MinimalFormatter; /** * Checks if all the variants are present in an array. */ -class ArrayContains implements MatcherInterface +class ArrayContains extends AbstractMatcher { /** * @param array $variants */ public function __construct(private array $variants) { + $this->setFormatter(new MinimalFormatter()); } /** * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'variants' => array_values($this->variants), - ]; + return ['variants' => array_values($this->variants)]; + } + + /** + * @todo Change return type to `null` + */ + protected function getValue(): mixed + { + return null; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php index 0965e378..08e3e398 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php @@ -14,6 +14,7 @@ public function __construct(private ?bool $value = null) if ($value === null) { $this->setGenerator(new RandomBoolean()); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php index 052e6e41..a8d8635d 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php @@ -2,26 +2,24 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * Match binary data by its content type (magic file check) */ -class ContentType implements MatcherInterface +class ContentType extends AbstractMatcher { public function __construct(private string $contentType) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } - /** - * @return array - */ - public function jsonSerialize(): array + protected function getValue(): string { - return [ - 'value' => $this->contentType, - 'pact:matcher:type' => $this->getType(), - ]; + return $this->contentType; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php index 69d2e54a..de994b2b 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php @@ -14,6 +14,7 @@ public function __construct(private ?float $value = null) if ($value === null) { $this->setGenerator(new RandomDecimal()); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php index d0b67067..8a941234 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php @@ -7,7 +7,7 @@ /** * Allows defining matching rules to apply to the keys in a map */ -class EachKey implements MatcherInterface +class EachKey extends AbstractMatcher { /** * @param array|object $value @@ -15,18 +15,23 @@ class EachKey implements MatcherInterface */ public function __construct(private object|array $value, private array $rules) { + parent::__construct(); } /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value, - 'rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules), - ]; + return ['rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules)]; + } + + /** + * @return array|object + */ + protected function getValue(): object|array + { + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php index 1533fba1..3b0c8bbd 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php @@ -7,7 +7,7 @@ /** * Allows defining matching rules to apply to the values in a collection. For maps, delgates to the Values matcher. */ -class EachValue implements MatcherInterface +class EachValue extends AbstractMatcher { /** * @param array|object $value @@ -15,18 +15,23 @@ class EachValue implements MatcherInterface */ public function __construct(private object|array $value, private array $rules) { + parent::__construct(); } /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value, - 'rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules), - ]; + return ['rules' => array_map(fn (MatcherInterface $rule) => $rule, $this->rules)]; + } + + /** + * @return array|object + */ + protected function getValue(): object|array + { + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php index bbf779ee..e6d12c42 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php @@ -2,29 +2,30 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This is the default matcher, and relies on the equals operator */ -class Equality implements MatcherInterface +class Equality extends AbstractMatcher { /** * @param object|array|string|float|int|bool|null $value */ public function __construct(private object|array|string|float|int|bool|null $value) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } /** - * @return array + * @return object|array|string|float|int|bool|null */ - public function jsonSerialize(): array + protected function getValue(): object|array|string|float|int|bool|null { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value - ]; + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php b/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php index a194742d..e3cd0174 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php @@ -4,12 +4,10 @@ use PhpPact\Consumer\Matcher\Exception\GeneratorNotRequiredException; use PhpPact\Consumer\Matcher\Exception\GeneratorRequiredException; -use PhpPact\Consumer\Matcher\Model\Attributes; use PhpPact\Consumer\Matcher\Model\GeneratorAwareInterface; use PhpPact\Consumer\Matcher\Model\GeneratorInterface; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; -abstract class GeneratorAwareMatcher implements MatcherInterface, GeneratorAwareInterface +abstract class GeneratorAwareMatcher extends AbstractMatcher implements GeneratorAwareInterface { private ?GeneratorInterface $generator = null; @@ -28,37 +26,16 @@ public function getGenerator(): ?GeneratorInterface */ public function jsonSerialize(): array { - $data = [ - 'pact:matcher:type' => $this->getType(), - ]; - if (null === $this->getValue()) { if (!$this->generator) { throw new GeneratorRequiredException(sprintf("Generator is required for matcher '%s' when example value is not set", $this->getType())); } - return $data + ['pact:generator:type' => $this->generator->getType()] + $this->getMergedAttributes()->getData(); + return $this->getFormatter()->format($this, $this->generator, null); } elseif ($this->generator) { throw new GeneratorNotRequiredException(sprintf("Generator '%s' is not required for matcher '%s' when example value is set", $this->generator->getType(), $this->getType())); } - return $data + $this->getAttributes()->getData() + ['value' => $this->getValue()]; - } - - protected function getAttributes(): Attributes - { - return new Attributes($this, $this->getAttributesData()); - } - - protected function getMergedAttributes(): Attributes - { - return $this->getAttributes()->merge($this->generator->getAttributes()); + return $this->getFormatter()->format($this, $this->generator, $this->getValue()); } - - /** - * @return array - */ - abstract protected function getAttributesData(): array; - - abstract protected function getValue(): mixed; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Includes.php b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php index 7435d919..094d94a7 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Includes.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php @@ -2,26 +2,24 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This checks if the string representation of a value contains the substring. */ -class Includes implements MatcherInterface +class Includes extends AbstractMatcher { public function __construct(private string $value) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } - /** - * @return array - */ - public function jsonSerialize(): array + protected function getValue(): string { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value, - ]; + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Integer.php b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php index bacc17a8..3919854c 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Integer.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php @@ -14,6 +14,7 @@ public function __construct(private ?int $value = null) if ($value === null) { $this->setGenerator(new RandomInt()); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php index 098c80b9..50a925dc 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php @@ -2,33 +2,36 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This executes a type based match against the values, that is, they are equal if they are the same type. * In addition, if the values represent a collection, the length of the actual value is compared against the maximum. */ -class MaxType implements MatcherInterface +class MaxType extends AbstractMatcher { /** - * @param array$values + * @param array $values */ public function __construct( private array $values, private int $max, ) { + parent::__construct(); + } + + /** + * @return array + */ + protected function getAttributesData(): array + { + return ['max' => $this->max]; } /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getValue(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'max' => $this->max, - 'value' => array_values($this->values), - ]; + return array_values($this->values); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php index 35586b24..f87fb753 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php @@ -2,13 +2,11 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This executes a type based match against the values, that is, they are equal if they are the same type. * In addition, if the values represent a collection, the length of the actual value is compared against the minimum and maximum. */ -class MinMaxType implements MatcherInterface +class MinMaxType extends AbstractMatcher { /** * @param array $values @@ -18,21 +16,28 @@ public function __construct( private int $min, private int $max, ) { + parent::__construct(); } /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getAttributesData(): array { return [ - 'pact:matcher:type' => $this->getType(), - 'min' => $this->min, - 'max' => $this->max, - 'value' => array_values($this->values), + 'min' => $this->min, + 'max' => $this->max, ]; } + /** + * @return array + */ + protected function getValue(): array + { + return array_values($this->values); + } + public function getType(): string { return 'type'; diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php index cc4673b9..4aabf493 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MinType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php @@ -2,13 +2,11 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This executes a type based match against the values, that is, they are equal if they are the same type. * In addition, if the values represent a collection, the length of the actual value is compared against the minimum. */ -class MinType implements MatcherInterface +class MinType extends AbstractMatcher { /** * @param array $values @@ -17,6 +15,7 @@ public function __construct( private array $values, private int $min, ) { + parent::__construct(); } /** @@ -24,15 +23,27 @@ public function __construct( */ public function jsonSerialize(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'min' => $this->min, - 'value' => array_values($this->values), - ]; + return $this->getFormatter()->format($this, null, array_values($this->values)); } public function getType(): string { return 'type'; } + + /** + * @return array + */ + protected function getAttributesData(): array + { + return ['min' => $this->min]; + } + + /** + * @return array + */ + protected function getValue(): array + { + return array_values($this->values); + } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php index b87ad6d9..85aaf0eb 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php @@ -2,29 +2,30 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * Value must be present and not empty (not null or the empty string) */ -class NotEmpty implements MatcherInterface +class NotEmpty extends AbstractMatcher { /** * @param object|array|string|float|int|bool $value */ public function __construct(private object|array|string|float|int|bool $value) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } /** - * @return array + * @return object|array|string|float|int|bool */ - public function jsonSerialize(): array + protected function getValue(): object|array|string|float|int|bool { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value, - ]; + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php index 5c00f7e7..2aa4cf8d 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php @@ -2,25 +2,33 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; +use PhpPact\Consumer\Matcher\Formatters\MinimalFormatter; /** * Match if the value is a null value (this is content specific, for JSON will match a JSON null) */ -class NullValue implements MatcherInterface +class NullValue extends AbstractMatcher { - /** - * @return array - */ - public function jsonSerialize(): array + public function __construct() { - return [ - 'pact:matcher:type' => $this->getType(), - ]; + $this->setFormatter(new MinimalFormatter()); } public function getType(): string { return 'null'; } + + protected function getAttributesData(): array + { + return []; + } + + /** + * @todo Change return type to `null` + */ + protected function getValue(): mixed + { + return null; + } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Number.php b/src/PhpPact/Consumer/Matcher/Matchers/Number.php index d98b9694..9f45fb4f 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Number.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Number.php @@ -14,6 +14,7 @@ public function __construct(private int|float|null $value = null) if ($value === null) { $this->setGenerator(new RandomInt()); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Regex.php b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php index 2d5fa859..d16582ee 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Regex.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php @@ -20,6 +20,7 @@ public function __construct( if ($values === null) { $this->setGenerator(new RegexGenerator($this->regex)); } + parent::__construct(); } /** diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Semver.php b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php index 11c63673..231c7f21 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Semver.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php @@ -14,6 +14,7 @@ public function __construct(private ?string $value = null) if ($value === null) { $this->setGenerator(new Regex('\d+\.\d+\.\d+')); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php b/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php index 94690d96..e1132733 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php @@ -31,6 +31,7 @@ public function __construct(private string $status, private ?int $value = null) $this->setGenerator(new RandomInt($min, $max)); } + parent::__construct(); } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php index 1459d9b0..da0fcc15 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php @@ -2,6 +2,7 @@ namespace PhpPact\Consumer\Matcher\Matchers; +use PhpPact\Consumer\Matcher\Formatters\ValueRequiredFormatter; use PhpPact\Consumer\Matcher\Generators\RandomString; /** @@ -16,6 +17,7 @@ public function __construct(private ?string $value = null) if ($value === null) { $this->setGenerator(new RandomString()); } + $this->setFormatter(new ValueRequiredFormatter()); } public function getType(): string @@ -28,16 +30,7 @@ public function getType(): string */ public function jsonSerialize(): array { - $data = [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->getValue() ?? self::DEFAULT_VALUE, - ]; - - if ($this->getGenerator()) { - return $data + ['pact:generator:type' => $this->getGenerator()->getType()] + $this->getMergedAttributes()->getData(); - } - - return $data; + return $this->getFormatter()->format($this, $this->getGenerator(), $this->getValue()); } protected function getAttributesData(): array @@ -45,8 +38,8 @@ protected function getAttributesData(): array return []; } - protected function getValue(): ?string + protected function getValue(): string { - return $this->value; + return $this->value ?? self::DEFAULT_VALUE; } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Type.php b/src/PhpPact/Consumer/Matcher/Matchers/Type.php index 912561fa..b8595330 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Type.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Type.php @@ -2,29 +2,30 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * This executes a type based match against the values, that is, they are equal if they are the same type. */ -class Type implements MatcherInterface +class Type extends AbstractMatcher { /** * @param object|array|string|float|int|bool|null $value */ public function __construct(private object|array|string|float|int|bool|null $value) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } /** - * @return array + * @return object|array|string|float|int|bool|null */ - public function jsonSerialize(): array + protected function getValue(): object|array|string|float|int|bool|null { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->value, - ]; + return $this->value; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Values.php b/src/PhpPact/Consumer/Matcher/Matchers/Values.php index 789c4643..11feaf12 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Values.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Values.php @@ -2,29 +2,30 @@ namespace PhpPact\Consumer\Matcher\Matchers; -use PhpPact\Consumer\Matcher\Model\MatcherInterface; - /** * Match the values in a map, ignoring the keys */ -class Values implements MatcherInterface +class Values extends AbstractMatcher { /** * @param array $values */ public function __construct(private array $values) { + parent::__construct(); + } + + protected function getAttributesData(): array + { + return []; } /** - * @return array + * @return array */ - public function jsonSerialize(): array + protected function getValue(): array { - return [ - 'pact:matcher:type' => $this->getType(), - 'value' => $this->values, - ]; + return $this->values; } public function getType(): string diff --git a/src/PhpPact/Consumer/Matcher/Model/FormatterAwareInterface.php b/src/PhpPact/Consumer/Matcher/Model/FormatterAwareInterface.php new file mode 100644 index 00000000..d757cb21 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Model/FormatterAwareInterface.php @@ -0,0 +1,10 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array; +} diff --git a/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php index df200149..1e4fb48f 100644 --- a/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php +++ b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php @@ -7,4 +7,6 @@ interface MatcherInterface extends JsonSerializable { public function getType(): string; + + public function getAttributes(): Attributes; } diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/MinimalFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/MinimalFormatterTest.php new file mode 100644 index 00000000..3cbcc2c9 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/MinimalFormatterTest.php @@ -0,0 +1,28 @@ +assertSame([ + 'pact:matcher:type' => 'date', + 'format' => 'yyyy-MM-dd', + ], $formatter->format($matcher, $generator, $value)); + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php new file mode 100644 index 00000000..bb1eaefc --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php @@ -0,0 +1,25 @@ +assertSame($result, $formatter->format($matcher, $generator, $value)); + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php new file mode 100644 index 00000000..c8dbd387 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php @@ -0,0 +1,25 @@ +assertSame($result, $formatter->format($matcher, $generator, $value)); + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php index 4b837779..4670af8c 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/ContentTypeTest.php @@ -10,7 +10,7 @@ class ContentTypeTest extends TestCase public function testSerialize(): void { $contentType = new ContentType('text/csv'); - $this->assertSame( + $this->assertJsonStringEqualsJsonString( '{"value":"text\/csv","pact:matcher:type":"contentType"}', json_encode($contentType) ); diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php index 537d554a..d068e4ab 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/EachKeyTest.php @@ -23,7 +23,7 @@ public function testSerialize(): void new Regex('\w{3}'), ]; $eachKey = new EachKey($value, $rules); - $this->assertSame( + $this->assertJsonStringEqualsJsonString( '{"pact:matcher:type":"eachKey","value":{"abc":123,"def":111,"ghi":{"test":"value"}},"rules":[{"pact:matcher:type":"type","value":"string"},{"pact:matcher:type":"regex","pact:generator:type":"Regex","regex":"\\\\w{3}"}]}', json_encode($eachKey) ); diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php index 9356292b..95dc7fb0 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/EachValueTest.php @@ -21,7 +21,7 @@ public function testSerialize(): void new Regex('\w{2}\d'), ]; $eachValue = new EachValue($value, $rules); - $this->assertSame( + $this->assertJsonStringEqualsJsonString( '{"pact:matcher:type":"eachValue","value":["ab1","cd2","ef9"],"rules":[{"pact:matcher:type":"type","value":"string"},{"pact:matcher:type":"regex","pact:generator:type":"Regex","regex":"\\\\w{2}\\\\d"}]}', json_encode($eachValue) ); diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php index 602bead2..b9ca2805 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/StringValueTest.php @@ -21,6 +21,6 @@ protected function setUp(): void public function testSerialize(?string $value, string $json): void { $this->matcher = new StringValue($value); - $this->assertSame($json, json_encode($this->matcher)); + $this->assertJsonStringEqualsJsonString($json, json_encode($this->matcher)); } } From 05c4c55657400429d33715e409fbf1f2be4a9f71 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 31 Dec 2023 22:29:21 +0700 Subject: [PATCH 182/298] refactor!: Re-use existing matchers for xml --- .../tests/Service/HttpClientServiceTest.php | 51 ++++------- .../xml/pacts/xmlConsumer-xmlProvider.json | 16 ++-- .../Exception/InvalidValueException.php | 7 ++ .../Formatters/XmlContentFormatter.php | 29 +++++++ .../Formatters/XmlElementFormatter.php | 31 +++++++ .../Exception/InvalidXmlElementException.php | 7 ++ src/PhpPact/Xml/Exception/XmlException.php | 9 ++ .../Xml/Model/Builder/ElementTrait.php | 36 ++++---- .../Xml/Model/Builder/GeneratorTrait.php | 21 ----- .../Xml/Model/Builder/MatcherTrait.php | 21 ----- src/PhpPact/Xml/Model/Builder/TextTrait.php | 32 +++---- src/PhpPact/Xml/Model/Matcher/Generator.php | 43 ---------- src/PhpPact/Xml/Model/Matcher/Matcher.php | 43 ---------- src/PhpPact/Xml/XmlBuilder.php | 11 +-- src/PhpPact/Xml/XmlElement.php | 81 +++++++---------- src/PhpPact/Xml/XmlText.php | 58 ++----------- .../Formatters/XmlContentFormatterTest.php | 23 +++++ .../Formatters/XmlElementFormatterTest.php | 39 +++++++++ .../PhpPact/Xml/XmlBuilderIntegrationTest.php | 77 ++++++----------- tests/PhpPact/Xml/XmlBuilderTest.php | 38 +++++--- tests/PhpPact/Xml/XmlElementTest.php | 86 ++++++++----------- tests/PhpPact/Xml/XmlTextTest.php | 69 +++++---------- 22 files changed, 348 insertions(+), 480 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/InvalidValueException.php create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php create mode 100644 src/PhpPact/Xml/Exception/InvalidXmlElementException.php create mode 100644 src/PhpPact/Xml/Exception/XmlException.php delete mode 100644 src/PhpPact/Xml/Model/Builder/GeneratorTrait.php delete mode 100644 src/PhpPact/Xml/Model/Builder/MatcherTrait.php delete mode 100644 src/PhpPact/Xml/Model/Matcher/Generator.php delete mode 100644 src/PhpPact/Xml/Model/Matcher/Matcher.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index 1ed429e7..94563c7f 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -24,69 +24,54 @@ public function testGetMovies() $xmlBuilder ->root( $xmlBuilder->name('movies'), - $xmlBuilder->add( - $xmlBuilder->eachLike(), + $xmlBuilder->eachLike( + $xmlBuilder->examples(1), $xmlBuilder->name('movie'), $xmlBuilder->add( $xmlBuilder->name('title'), - $xmlBuilder->text( - $xmlBuilder->contentLike('PHP: Behind the Parser'), - ), + $xmlBuilder->contentLike('PHP: Behind the Parser'), ), $xmlBuilder->add( $xmlBuilder->name('characters'), - $xmlBuilder->add( - $xmlBuilder->eachLike(examples: 2), + $xmlBuilder->eachLike( + $xmlBuilder->examples(2), $xmlBuilder->name('character'), $xmlBuilder->add( $xmlBuilder->name('name'), - $xmlBuilder->text( - $xmlBuilder->contentLike('Ms. Coder'), - ), + $xmlBuilder->contentLike('Ms. Coder'), ), $xmlBuilder->add( $xmlBuilder->name('actor'), - $xmlBuilder->text( - $xmlBuilder->contentLike('Onlivia Actora'), - ), + $xmlBuilder->contentLike('Onlivia Actora'), ), ), ), $xmlBuilder->add( $xmlBuilder->name('plot'), - $xmlBuilder->text( - $xmlBuilder->contentLike( - <<contentLike( + <<add( $xmlBuilder->name('great-lines'), - $xmlBuilder->add( - $xmlBuilder->eachLike(), + $xmlBuilder->eachLike( $xmlBuilder->name('line'), - $xmlBuilder->text( - $xmlBuilder->contentLike('PHP solves all my web problems'), - ), + $xmlBuilder->contentLike('PHP solves all my web problems'), ), ), $xmlBuilder->add( $xmlBuilder->name('rating'), $xmlBuilder->attribute('type', 'thumbs'), - $xmlBuilder->text( - $xmlBuilder->contentLike(7), - ), + $xmlBuilder->contentLike(7), ), $xmlBuilder->add( $xmlBuilder->name('rating'), $xmlBuilder->attribute('type', 'stars'), - $xmlBuilder->text( - $xmlBuilder->contentLike(5), - ), + $xmlBuilder->contentLike(5), ), ), ); @@ -96,7 +81,7 @@ public function testGetMovies() ->setStatus(200) ->addHeader('Content-Type', 'text/xml') ->setBody( - json_encode($xmlBuilder->getArray()) + json_encode($xmlBuilder) ); $config = new MockServerConfig(); diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index a8e4a8e9..da5c5acb 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -28,8 +28,7 @@ "combine": "AND", "matchers": [ { - "match": "type", - "min": 1 + "match": "type" } ] }, @@ -37,8 +36,7 @@ "combine": "AND", "matchers": [ { - "match": "type", - "min": 1 + "match": "type" } ] }, @@ -62,8 +60,7 @@ "combine": "AND", "matchers": [ { - "match": "type", - "min": 1 + "match": "type" } ] }, @@ -103,7 +100,8 @@ ] } }, - "header": {} + "header": {}, + "status": {} }, "status": 200 } @@ -111,9 +109,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.8", + "ffi": "0.4.11", "mockserver": "1.2.4", - "models": "1.1.11" + "models": "1.1.12" }, "pactSpecification": { "version": "3.0.0" diff --git a/src/PhpPact/Consumer/Matcher/Exception/InvalidValueException.php b/src/PhpPact/Consumer/Matcher/Exception/InvalidValueException.php new file mode 100644 index 00000000..1cd0da8e --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/InvalidValueException.php @@ -0,0 +1,7 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + { + $data = [ + 'content' => $value, + 'matcher' => [ + 'pact:matcher:type' => $matcher->getType(), + ] + $matcher->getAttributes()->merge($generator ? $generator->getAttributes() : new Attributes($matcher))->getData(), + ]; + + if ($generator) { + $data['pact:generator:type'] = $generator->getType(); + } + + return $data; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php new file mode 100644 index 00000000..924f7d13 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php @@ -0,0 +1,31 @@ + + */ + public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + { + if (!$value instanceof XmlElement) { + throw new InvalidValueException('Value must be xml element'); + } + + $result = parent::format($matcher, $generator, $value); + $examples = $value->getExamples(); + + if (null !== $examples) { + $result['examples'] = $examples; + $value->setExamples(null); + } + + return $result; + } +} diff --git a/src/PhpPact/Xml/Exception/InvalidXmlElementException.php b/src/PhpPact/Xml/Exception/InvalidXmlElementException.php new file mode 100644 index 00000000..d62ad2e7 --- /dev/null +++ b/src/PhpPact/Xml/Exception/InvalidXmlElementException.php @@ -0,0 +1,7 @@ +root = new XmlElement(...$options); } + public function examples(int $examples): callable + { + return fn (XmlElement $element) => $element->setExamples($examples); + } + public function add(callable ...$options): callable { return fn (XmlElement $element) => $element->addChild(new XmlElement(...$options)); @@ -25,30 +31,18 @@ public function name(string $name): callable return fn (XmlElement $element) => $element->setName($name); } - public function text(callable ...$options): callable - { - return fn (XmlElement $element) => $element->setText(new XmlText(...$options)); - } - - public function attribute(string $name, mixed $value): callable + public function attribute(string $name, string|float|int|bool|MatcherInterface $value): callable { return fn (XmlElement $element) => $element->addAttribute($name, $value); } - public function eachLike(int $min = 1, ?int $max = null, int $examples = 1): callable + public function eachLike(callable ...$options): callable { - return function (XmlElement $element) use ($min, $max, $examples): void { - $options = [ - 'min' => $min, - 'examples' => $examples, - ]; - if (isset($max)) { - $options['max'] = $max; - } - $element->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('type'), - fn (Matcher $matcher) => $matcher->setOptions($options), - )); + return function (XmlElement $element) use ($options): void { + $child = new XmlElement(...$options); + $matcher = new Type($child); + $matcher->setFormatter(new XmlElementFormatter()); + $element->addChild($matcher); }; } } diff --git a/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php b/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php deleted file mode 100644 index 32a5be4e..00000000 --- a/src/PhpPact/Xml/Model/Builder/GeneratorTrait.php +++ /dev/null @@ -1,21 +0,0 @@ - $generator->setType($type); - } - - /** - * @param array $options - */ - public function generatorOptions(array $options): callable - { - return fn (Generator $generator) => $generator->setOptions($options); - } -} diff --git a/src/PhpPact/Xml/Model/Builder/MatcherTrait.php b/src/PhpPact/Xml/Model/Builder/MatcherTrait.php deleted file mode 100644 index 4e38d0e8..00000000 --- a/src/PhpPact/Xml/Model/Builder/MatcherTrait.php +++ /dev/null @@ -1,21 +0,0 @@ - $matcher->setType($type); - } - - /** - * @param array $options - */ - public function matcherOptions(array $options): callable - { - return fn (Matcher $matcher) => $matcher->setOptions($options); - } -} diff --git a/src/PhpPact/Xml/Model/Builder/TextTrait.php b/src/PhpPact/Xml/Model/Builder/TextTrait.php index bd785fc6..4fe8a7f2 100644 --- a/src/PhpPact/Xml/Model/Builder/TextTrait.php +++ b/src/PhpPact/Xml/Model/Builder/TextTrait.php @@ -2,34 +2,26 @@ namespace PhpPact\Xml\Model\Builder; -use PhpPact\Xml\Model\Matcher\Generator; -use PhpPact\Xml\Model\Matcher\Matcher; +use PhpPact\Consumer\Matcher\Formatters\XmlContentFormatter; +use PhpPact\Consumer\Matcher\Matchers\Type; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; +use PhpPact\Xml\XmlElement; use PhpPact\Xml\XmlText; trait TextTrait { - public function content(string|int|float $content): callable + public function content(string|float|int|bool|null|MatcherInterface $content): callable { - return fn (XmlText $text) => $text->setContent($content); + return fn (XmlElement $element) => $element->setText(new XmlText($content)); } - public function matcher(callable ...$options): callable + public function contentLike(string|float|int|bool|null $content): callable { - return fn (XmlText $text) => $text->setMatcher(new Matcher(...$options)); - } - - public function generator(callable ...$options): callable - { - return fn (XmlText $text) => $text->setGenerator(new Generator(...$options)); - } - - public function contentLike(string|int|float $content): callable - { - return function (XmlText $text) use ($content): void { - $text->setContent($content); - $text->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('type') - )); + return function (XmlElement $element) use ($content): void { + $matcher = new Type($content); + $matcher->setFormatter(new XmlContentFormatter()); + $text = new XmlText($matcher); + $element->setText($text); }; } } diff --git a/src/PhpPact/Xml/Model/Matcher/Generator.php b/src/PhpPact/Xml/Model/Matcher/Generator.php deleted file mode 100644 index 5419223f..00000000 --- a/src/PhpPact/Xml/Model/Matcher/Generator.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ - protected array $options = []; - - public function __construct(callable ...$options) - { - array_walk($options, fn (callable $option) => $option($this)); - } - - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - - /** - * @param array $options - */ - public function setOptions(array $options): self - { - $this->options = $options; - - return $this; - } - - /** - * @return array - */ - public function getArray(): array - { - return ['pact:generator:type' => $this->type] + $this->options; - } -} diff --git a/src/PhpPact/Xml/Model/Matcher/Matcher.php b/src/PhpPact/Xml/Model/Matcher/Matcher.php deleted file mode 100644 index 4e7e3a4b..00000000 --- a/src/PhpPact/Xml/Model/Matcher/Matcher.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ - protected array $options = []; - - public function __construct(callable ...$options) - { - array_walk($options, fn (callable $option) => $option($this)); - } - - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - - /** - * @param array $options - */ - public function setOptions(array $options): self - { - $this->options = $options; - - return $this; - } - - /** - * @return array - */ - public function getArray(): array - { - return ['pact:matcher:type' => $this->type] + $this->options; - } -} diff --git a/src/PhpPact/Xml/XmlBuilder.php b/src/PhpPact/Xml/XmlBuilder.php index 76260fda..65653d2f 100644 --- a/src/PhpPact/Xml/XmlBuilder.php +++ b/src/PhpPact/Xml/XmlBuilder.php @@ -2,17 +2,14 @@ namespace PhpPact\Xml; +use JsonSerializable; use PhpPact\Xml\Model\Builder\ElementTrait; -use PhpPact\Xml\Model\Builder\GeneratorTrait; -use PhpPact\Xml\Model\Builder\MatcherTrait; use PhpPact\Xml\Model\Builder\TextTrait; -class XmlBuilder +class XmlBuilder implements JsonSerializable { use ElementTrait; use TextTrait; - use MatcherTrait; - use GeneratorTrait; public function __construct(private string $version, private string $charset) { @@ -21,12 +18,12 @@ public function __construct(private string $version, private string $charset) /** * @return array */ - public function getArray(): array + public function jsonSerialize(): array { return [ 'version' => $this->version, 'charset' => $this->charset, - 'root' => $this->root->getArray(), + 'root' => $this->root, ]; } } diff --git a/src/PhpPact/Xml/XmlElement.php b/src/PhpPact/Xml/XmlElement.php index 31eb9c00..9f3d10d1 100644 --- a/src/PhpPact/Xml/XmlElement.php +++ b/src/PhpPact/Xml/XmlElement.php @@ -2,32 +2,34 @@ namespace PhpPact\Xml; -use PhpPact\Xml\Model\Matcher\Generator; -use PhpPact\Xml\Model\Matcher\Matcher; +use JsonSerializable; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; +use PhpPact\Xml\Exception\InvalidXmlElementException; -class XmlElement +class XmlElement implements JsonSerializable { private string $name; /** - * @var XmlElement[] + * @var array */ private array $children = []; /** - * @var array + * @var array */ private array $attributes = []; private ?XmlText $text = null; - private ?Matcher $matcher = null; - - private ?Generator $generator = null; + private ?int $examples = null; public function __construct(callable ...$options) { array_walk($options, fn (callable $option) => $option($this)); + if (!isset($this->name)) { + throw new InvalidXmlElementException("Xml element's name is required"); + } } public function setName(string $name): self @@ -37,7 +39,7 @@ public function setName(string $name): self return $this; } - public function addChild(self $child): self + public function addChild(self|MatcherInterface $child): self { $this->children[] = $child; @@ -51,21 +53,7 @@ public function setText(?XmlText $text): self return $this; } - public function setMatcher(?Matcher $matcher): self - { - $this->matcher = $matcher; - - return $this; - } - - public function setGenerator(?Generator $generator): self - { - $this->generator = $generator; - - return $this; - } - - public function addAttribute(string $name, mixed $value): self + public function addAttribute(string $name, string|float|int|bool|MatcherInterface $value): self { $this->attributes[$name] = $value; @@ -75,39 +63,34 @@ public function addAttribute(string $name, mixed $value): self /** * @return array */ - public function getArray(): array - { - if ($this->matcher) { - $result = [ - 'value' => $this->getBaseArray(), - ]; - $result += $this->matcher->getArray(); - - if ($this->generator) { - $result += $this->generator->getArray(); - } - } else { - $result = $this->getBaseArray(); - } - - return $result; - } - - /** - * @return array - */ - private function getBaseArray(): array + public function jsonSerialize(): array { $result = [ 'name' => $this->name, - 'children' => array_map(fn (XmlElement $element) => $element->getArray(), $this->children), - 'attributes' => $this->attributes + 'children' => $this->children, + 'attributes' => $this->attributes, ]; + if (null !== $this->examples) { + $result['examples'] = $this->examples; + } + if ($this->text) { - $result['children'][] = $this->text->getArray(); + $result['children'][] = $this->text; } return $result; } + + public function setExamples(?int $examples): self + { + $this->examples = $examples; + + return $this; + } + + public function getExamples(): ?int + { + return $this->examples; + } } diff --git a/src/PhpPact/Xml/XmlText.php b/src/PhpPact/Xml/XmlText.php index 435cfc12..395afbfa 100644 --- a/src/PhpPact/Xml/XmlText.php +++ b/src/PhpPact/Xml/XmlText.php @@ -2,68 +2,24 @@ namespace PhpPact\Xml; -use PhpPact\Xml\Model\Matcher\Generator; -use PhpPact\Xml\Model\Matcher\Matcher; +use JsonSerializable; +use PhpPact\Consumer\Matcher\Model\MatcherInterface; -class XmlText +class XmlText implements JsonSerializable { - private string|int|float $content; - - private ?Matcher $matcher = null; - - private ?Generator $generator = null; - - public function __construct(callable ...$options) - { - array_walk($options, fn (callable $option) => $option($this)); - } - - public function setContent(string|int|float $content): self - { - $this->content = $content; - - return $this; - } - - public function setMatcher(?Matcher $matcher): self - { - $this->matcher = $matcher; - - return $this; - } - - public function setGenerator(?Generator $generator): self + public function __construct(private string|float|int|bool|null|MatcherInterface $content) { - $this->generator = $generator; - - return $this; } /** * @return array */ - public function getArray(): array + public function jsonSerialize(): array { - $result = $this->getBaseArray(); - - if ($this->matcher) { - $result['matcher'] = $this->matcher->getArray(); - } - - if ($this->generator) { - $generator = $this->generator->getArray(); - $result['pact:generator:type'] = $generator['pact:generator:type']; - $result['matcher'] += array_diff_key($generator, array_flip(['pact:generator:type'])); + if ($this->content instanceof MatcherInterface) { + return $this->content->jsonSerialize(); } - return $result; - } - - /** - * @return array - */ - private function getBaseArray(): array - { return [ 'content' => $this->content, ]; diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php new file mode 100644 index 00000000..1caaacfb --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php @@ -0,0 +1,23 @@ +assertSame($result, $formatter->format($matcher, $generator, $value)); + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php new file mode 100644 index 00000000..b3657d3f --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php @@ -0,0 +1,39 @@ +expectException(InvalidValueException::class); + $this->expectExceptionMessage('Value must be xml element'); + $formatter = new XmlElementFormatter(); + $formatter->format(new StringValue(), new RandomString(), 'example value'); + } + + /** + * @testWith [null, {"pact:matcher:type": "type", "value": {"name": "test", "children": [], "attributes": []}}] + * [123, {"pact:matcher:type": "type", "value": {"name": "test", "children": [], "attributes": []}, "examples": 123}] + */ + public function testFormat(?int $examples, array $result): void + { + $value = new XmlElement( + fn (XmlElement $element) => $element->setName('test'), + fn (XmlElement $element) => $element->setExamples($examples), + ); + $matcher = new Type($value); + $formatter = new XmlElementFormatter(); + $this->assertSame(json_encode($result), json_encode($formatter->format($matcher, null, $value))); + } +} diff --git a/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php b/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php index 58040aab..72fdd971 100644 --- a/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php +++ b/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php @@ -26,8 +26,8 @@ public function testBuildWithMatchersOnAttributes(): void $this->builder->name('ns1:projects'), $this->builder->attribute('id', '1234'), $this->builder->attribute('xmlns:ns1', 'http://some.namespace/and/more/stuff'), - $this->builder->add( - $this->builder->eachLike(examples: 2), + $this->builder->eachLike( + $this->builder->examples(2), $this->builder->name('ns1:project'), $this->builder->attribute('id', $this->matcher->integerV3(1)), $this->builder->attribute('type', 'activity'), @@ -35,8 +35,8 @@ public function testBuildWithMatchersOnAttributes(): void $this->builder->attribute('due', $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss.SZ", '2016-02-11T09:46:56.023Z')), $this->builder->add( $this->builder->name('ns1:tasks'), - $this->builder->add( - $this->builder->eachLike(examples: 5), + $this->builder->eachLike( + $this->builder->examples(5), $this->builder->name('ns1:task'), $this->builder->attribute('id', $this->matcher->integerV3(1)), $this->builder->attribute('name', $this->matcher->string('Task 1')), @@ -53,6 +53,7 @@ public function testBuildWithMatchersOnAttributes(): void 'name' => 'ns1:projects', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'ns1:project', 'children' => [ @@ -60,6 +61,7 @@ public function testBuildWithMatchersOnAttributes(): void 'name' => 'ns1:tasks', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'ns1:task', 'children' => [], @@ -79,8 +81,6 @@ public function testBuildWithMatchersOnAttributes(): void ], ], ], - 'pact:matcher:type' => 'type', - 'min' => 1, 'examples' => 5, ], ], @@ -104,8 +104,6 @@ public function testBuildWithMatchersOnAttributes(): void ], ], ], - 'pact:matcher:type' => 'type', - 'min' => 1, 'examples' => 2, ], ], @@ -116,7 +114,7 @@ public function testBuildWithMatchersOnAttributes(): void ], ]; - $this->assertSame(json_encode($expectedArray), json_encode($this->builder->getArray())); + $this->assertSame(json_encode($expectedArray), json_encode($this->builder)); } public function testBuildWithMatchersOnContent(): void @@ -124,69 +122,53 @@ public function testBuildWithMatchersOnContent(): void $this->builder ->root( $this->builder->name('movies'), - $this->builder->add( - $this->builder->eachLike(), + $this->builder->eachLike( $this->builder->name('movie'), $this->builder->add( $this->builder->name('title'), - $this->builder->text( - $this->builder->contentLike('PHP: Behind the Parser'), - ), + $this->builder->contentLike('PHP: Behind the Parser'), ), $this->builder->add( $this->builder->name('characters'), - $this->builder->add( - $this->builder->eachLike(examples: 2), + $this->builder->eachLike( + $this->builder->examples(2), $this->builder->name('character'), $this->builder->add( $this->builder->name('name'), - $this->builder->text( - $this->builder->contentLike('Ms. Coder'), - ), + $this->builder->contentLike('Ms. Coder'), ), $this->builder->add( $this->builder->name('actor'), - $this->builder->text( - $this->builder->contentLike('Onlivia Actora'), - ), + $this->builder->contentLike('Onlivia Actora'), ), ), ), $this->builder->add( $this->builder->name('plot'), - $this->builder->text( - $this->builder->contentLike( - $plot = <<builder->contentLike( + $plot = <<builder->add( $this->builder->name('great-lines'), - $this->builder->add( - $this->builder->eachLike(), + $this->builder->eachLike( $this->builder->name('line'), - $this->builder->text( - $this->builder->contentLike('PHP solves all my web problems'), - ), + $this->builder->contentLike('PHP solves all my web problems'), ), ), $this->builder->add( $this->builder->name('rating'), $this->builder->attribute('type', 'thumbs'), - $this->builder->text( - $this->builder->contentLike(7), - ), + $this->builder->contentLike(7), ), $this->builder->add( $this->builder->name('rating'), $this->builder->attribute('type', 'stars'), - $this->builder->text( - $this->builder->contentLike(5), - ), + $this->builder->contentLike(5), ), ), ); @@ -198,6 +180,7 @@ public function testBuildWithMatchersOnContent(): void 'name' => 'movies', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'movie', 'children' => [ @@ -217,6 +200,7 @@ public function testBuildWithMatchersOnContent(): void 'name' => 'characters', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'character', 'children' => [ @@ -251,8 +235,6 @@ public function testBuildWithMatchersOnContent(): void ], 'attributes' => [], ], - 'pact:matcher:type' => 'type', - 'min' => 1, 'examples' => 2, ], ], @@ -274,6 +256,7 @@ public function testBuildWithMatchersOnContent(): void 'name' => 'great-lines', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'line', 'children' => [ @@ -288,9 +271,6 @@ public function testBuildWithMatchersOnContent(): void ], 'attributes' => [], ], - 'pact:matcher:type' => 'type', - 'min' => 1, - 'examples' => 1, ], ], 'attributes' => [], @@ -322,15 +302,12 @@ public function testBuildWithMatchersOnContent(): void ], 'attributes' => [], ], - 'pact:matcher:type' => 'type', - 'min' => 1, - 'examples' => 1, ], ], 'attributes' => [], ], ]; - $this->assertSame(json_encode($expectedArray), json_encode($this->builder->getArray())); + $this->assertSame(json_encode($expectedArray), json_encode($this->builder)); } } diff --git a/tests/PhpPact/Xml/XmlBuilderTest.php b/tests/PhpPact/Xml/XmlBuilderTest.php index ac8ad8cc..5cef4158 100644 --- a/tests/PhpPact/Xml/XmlBuilderTest.php +++ b/tests/PhpPact/Xml/XmlBuilderTest.php @@ -2,12 +2,30 @@ namespace PhpPactTest\Xml; +use PhpPact\Xml\Exception\InvalidXmlElementException; use PHPUnit\Framework\TestCase; use PhpPact\Xml\XmlBuilder; class XmlBuilderTest extends TestCase { - public function testGetArray(): void + public function testJsonSerializeInvalidXmlElement(): void + { + $this->expectException(InvalidXmlElementException::class); + $this->expectExceptionMessage("Xml element's name is required"); + + $builder = new XmlBuilder('1.0', 'UTF-8'); + + $builder + ->root( + $builder->name('Root'), + $builder->add(), + ) + ; + + json_encode($builder); + } + + public function testJsonSerialize(): void { $builder = new XmlBuilder('1.0', 'UTF-8'); @@ -16,9 +34,7 @@ public function testGetArray(): void $builder->name('Root'), $builder->add( $builder->name('First Child Second Element'), - $builder->text( - $builder->contentLike('Example Test') - ) + $builder->contentLike('Example Test') ), $builder->add( $builder->name('Second Parent'), @@ -28,14 +44,11 @@ public function testGetArray(): void ), $builder->add( $builder->name('Second child 2'), - $builder->text( - $builder->content('Test') - ) + $builder->content('Test') ), $builder->add( $builder->name('Third Parent'), - $builder->add( - $builder->eachLike(), + $builder->eachLike( $builder->name('Child') ) ), @@ -79,14 +92,12 @@ public function testGetArray(): void 'name' => 'ThirdParent', 'children' => [ [ + 'pact:matcher:type' => 'type', 'value' => [ 'name' => 'Child', 'children' => [], 'attributes' => [], ], - 'pact:matcher:type' => 'type', - 'min' => 1, - 'examples' => 1, ], ], 'attributes' => [], @@ -104,7 +115,6 @@ public function testGetArray(): void ], ]; - - $this->assertSame(json_encode($expectedArray), json_encode($builder->getArray())); + $this->assertSame(json_encode($expectedArray), json_encode($builder)); } } diff --git a/tests/PhpPact/Xml/XmlElementTest.php b/tests/PhpPact/Xml/XmlElementTest.php index 66fa3127..9b989f77 100644 --- a/tests/PhpPact/Xml/XmlElementTest.php +++ b/tests/PhpPact/Xml/XmlElementTest.php @@ -2,9 +2,8 @@ namespace PhpPactTest\Xml; +use PhpPact\Xml\XmlText; use PHPUnit\Framework\TestCase; -use PhpPact\Xml\Model\Matcher\Generator; -use PhpPact\Xml\Model\Matcher\Matcher; use PhpPact\Xml\XmlElement; class XmlElementTest extends TestCase @@ -13,74 +12,61 @@ class XmlElementTest extends TestCase public function setUp(): void { - $this->element = new XmlElement(); - $this->element->setName('Child'); - $this->element->addAttribute('myAttr', 'attr-value'); + $child = new XmlElement( + fn (XmlElement $element) => $element->setName('Child'), + ); + $this->element = new XmlElement( + fn (XmlElement $element) => $element->setName('Parent'), + fn (XmlElement $element) => $element->addAttribute('myAttr', 'attr-value'), + fn (XmlElement $element) => $element->setExamples(7), + fn (XmlElement $element) => $element->addChild($child), + ); } - public function testGetMatcherArray(): void + public function testJsonSerializeWithoutText(): void { - $this->element->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('type'), - fn (Matcher $matcher) => $matcher->setOptions(['examples' => 7]), - )); - $this->assertSame( json_encode([ - 'value' => [ - 'name' => 'Child', - 'children' => [], - 'attributes' => [ - 'myAttr' => 'attr-value', + 'name' => 'Parent', + 'children' => [ + [ + 'name' => 'Child', + 'children' => [], + 'attributes' => [], ], ], - 'pact:matcher:type' => 'type', + 'attributes' => [ + 'myAttr' => 'attr-value', + ], 'examples' => 7, ]), - json_encode($this->element->getArray()) + json_encode($this->element) ); } - public function testGetGeneratorArray(): void + public function testJsonSerializeWithText(): void { - $this->element->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('type'), - fn (Matcher $matcher) => $matcher->setOptions(['examples' => 7]), - )); - $this->element->setGenerator(new Generator( - fn (Generator $generator) => $generator->setType('Uuid'), - fn (Generator $generator) => $generator->setOptions(['format' => 'simple']), - )); + $this->element->setText(new XmlText('Inner text')); $this->assertSame( json_encode([ - 'value' => [ - 'name' => 'Child', - 'children' => [], - 'attributes' => [ - 'myAttr' => 'attr-value', - ] + 'name' => 'Parent', + 'children' => [ + [ + 'name' => 'Child', + 'children' => [], + 'attributes' => [], + ], + [ + 'content' => 'Inner text', + ], ], - 'pact:matcher:type' => 'type', - 'examples' => 7, - 'pact:generator:type' => 'Uuid', - 'format' => 'simple', - ]), - json_encode($this->element->getArray()) - ); - } - - public function testGetBaseArray(): void - { - $this->assertSame( - json_encode([ - 'name' => 'Child', - 'children' => [], 'attributes' => [ 'myAttr' => 'attr-value', - ] + ], + 'examples' => 7, ]), - json_encode($this->element->getArray()) + json_encode($this->element) ); } } diff --git a/tests/PhpPact/Xml/XmlTextTest.php b/tests/PhpPact/Xml/XmlTextTest.php index 7371664e..4094b9e4 100644 --- a/tests/PhpPact/Xml/XmlTextTest.php +++ b/tests/PhpPact/Xml/XmlTextTest.php @@ -2,54 +2,37 @@ namespace PhpPactTest\Xml; +use PhpPact\Consumer\Matcher\Formatters\XmlContentFormatter; +use PhpPact\Consumer\Matcher\Generators\RandomInt; +use PhpPact\Consumer\Matcher\Matchers\Integer; use PHPUnit\Framework\TestCase; -use PhpPact\Xml\Model\Matcher\Generator; -use PhpPact\Xml\Model\Matcher\Matcher; use PhpPact\Xml\XmlText; class XmlTextTest extends TestCase { - private XmlText $text; - - public function setUp(): void + /** + * @testWith ["example text"] + * [1.23] + * [481] + * [false] + * [true] + * [null] + */ + public function testJsonSerializePredefinedTypes(mixed $content): void { - $this->text = new XmlText(); - $this->text->setContent('testing'); - } - - public function testGetMatcherArray(): void - { - $this->text->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('include'), - fn (Matcher $matcher) => $matcher->setOptions(['value' => "te"]), - )); - - $this->assertSame( - json_encode([ - 'content' => 'testing', - 'matcher' => [ - 'pact:matcher:type' => 'include', - 'value' => 'te', - ] - ]), - json_encode($this->text->getArray()) - ); + $text = new XmlText($content); + $this->assertSame(json_encode(['content' => $content]), json_encode($text)); } - public function testGetGeneratorArray(): void + public function testJsonSerializeMatcher(): void { - $this->text->setContent(7); - $this->text->setMatcher(new Matcher( - fn (Matcher $matcher) => $matcher->setType('integer'), - )); - $this->text->setGenerator(new Generator( - fn (Generator $generator) => $generator->setType('RandomInt'), - fn (Generator $generator) => $generator->setOptions(['min' => 2, 'max' => 8]), - )); - + $matcher = new Integer(); + $matcher->setGenerator(new RandomInt(2, 8)); + $matcher->setFormatter(new XmlContentFormatter()); + $text = new XmlText($matcher); $this->assertSame( json_encode([ - 'content' => 7, + 'content' => null, 'matcher' => [ 'pact:matcher:type' => 'integer', 'min' => 2, @@ -57,17 +40,7 @@ public function testGetGeneratorArray(): void ], 'pact:generator:type' => 'RandomInt' ]), - json_encode($this->text->getArray()) - ); - } - - public function testGetBaseArray(): void - { - $this->assertSame( - json_encode([ - 'content' => 'testing', - ]), - json_encode($this->text->getArray()) + json_encode($text) ); } } From e3d0d06f883b28ccf83a2a7d46990df06e42d489 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 1 Jan 2024 13:56:50 +0700 Subject: [PATCH 183/298] test: Merge xml builder tests --- .../PhpPact/Xml/XmlBuilderIntegrationTest.php | 313 ------------------ tests/PhpPact/Xml/XmlBuilderTest.php | 180 +++++----- 2 files changed, 105 insertions(+), 388 deletions(-) delete mode 100644 tests/PhpPact/Xml/XmlBuilderIntegrationTest.php diff --git a/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php b/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php deleted file mode 100644 index 72fdd971..00000000 --- a/tests/PhpPact/Xml/XmlBuilderIntegrationTest.php +++ /dev/null @@ -1,313 +0,0 @@ -builder = new XmlBuilder('1.0', 'UTF-8'); - $this->matcher = new Matcher(); - } - - public function testBuildWithMatchersOnAttributes(): void - { - $this->builder - ->root( - $this->builder->name('ns1:projects'), - $this->builder->attribute('id', '1234'), - $this->builder->attribute('xmlns:ns1', 'http://some.namespace/and/more/stuff'), - $this->builder->eachLike( - $this->builder->examples(2), - $this->builder->name('ns1:project'), - $this->builder->attribute('id', $this->matcher->integerV3(1)), - $this->builder->attribute('type', 'activity'), - $this->builder->attribute('name', $this->matcher->string('Project 1')), - $this->builder->attribute('due', $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss.SZ", '2016-02-11T09:46:56.023Z')), - $this->builder->add( - $this->builder->name('ns1:tasks'), - $this->builder->eachLike( - $this->builder->examples(5), - $this->builder->name('ns1:task'), - $this->builder->attribute('id', $this->matcher->integerV3(1)), - $this->builder->attribute('name', $this->matcher->string('Task 1')), - $this->builder->attribute('done', $this->matcher->boolean()), - ), - ), - ), - ); - - $expectedArray = [ - 'version' => '1.0', - 'charset' => 'UTF-8', - 'root' => [ - 'name' => 'ns1:projects', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'ns1:project', - 'children' => [ - [ - 'name' => 'ns1:tasks', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'ns1:task', - 'children' => [], - 'attributes' => [ - 'id' => [ - 'pact:matcher:type' => - 'integer', - 'value' => 1, - ], - 'name' => [ - 'pact:matcher:type' => 'type', - 'value' => 'Task 1', - ], - 'done' => [ - 'pact:matcher:type' => 'type', - 'value' => true, - ], - ], - ], - 'examples' => 5, - ], - ], - 'attributes' => [], - ], - ], - 'attributes' => [ - 'id' => [ - 'pact:matcher:type' => 'integer', - 'value' => 1, - ], - 'type' => 'activity', - 'name' => [ - 'pact:matcher:type' => 'type', - 'value' => 'Project 1', - ], - 'due' => [ - 'pact:matcher:type' => 'datetime', - 'format' => "yyyy-MM-dd'T'HH:mm:ss.SZ", - 'value' => '2016-02-11T09:46:56.023Z', - ], - ], - ], - 'examples' => 2, - ], - ], - 'attributes' => [ - 'id' => '1234', - 'xmlns:ns1' => 'http://some.namespace/and/more/stuff', - ], - ], - ]; - - $this->assertSame(json_encode($expectedArray), json_encode($this->builder)); - } - - public function testBuildWithMatchersOnContent(): void - { - $this->builder - ->root( - $this->builder->name('movies'), - $this->builder->eachLike( - $this->builder->name('movie'), - $this->builder->add( - $this->builder->name('title'), - $this->builder->contentLike('PHP: Behind the Parser'), - ), - $this->builder->add( - $this->builder->name('characters'), - $this->builder->eachLike( - $this->builder->examples(2), - $this->builder->name('character'), - $this->builder->add( - $this->builder->name('name'), - $this->builder->contentLike('Ms. Coder'), - ), - $this->builder->add( - $this->builder->name('actor'), - $this->builder->contentLike('Onlivia Actora'), - ), - ), - ), - $this->builder->add( - $this->builder->name('plot'), - $this->builder->contentLike( - $plot = <<builder->add( - $this->builder->name('great-lines'), - $this->builder->eachLike( - $this->builder->name('line'), - $this->builder->contentLike('PHP solves all my web problems'), - ), - ), - $this->builder->add( - $this->builder->name('rating'), - $this->builder->attribute('type', 'thumbs'), - $this->builder->contentLike(7), - ), - $this->builder->add( - $this->builder->name('rating'), - $this->builder->attribute('type', 'stars'), - $this->builder->contentLike(5), - ), - ), - ); - - $expectedArray = [ - 'version' => '1.0', - 'charset' => 'UTF-8', - 'root' => [ - 'name' => 'movies', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'movie', - 'children' => [ - [ - 'name' => 'title', - 'children' => [ - [ - 'content' => 'PHP: Behind the Parser', - 'matcher' => [ - 'pact:matcher:type' => 'type', - ], - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'characters', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'character', - 'children' => [ - [ - 'name' => 'name', - 'children' => [ - [ - 'content' => - 'Ms. Coder', - 'matcher' => [ - 'pact:matcher:type' => - 'type', - ], - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'actor', - 'children' => [ - [ - 'content' => - 'Onlivia Actora', - 'matcher' => [ - 'pact:matcher:type' => - 'type', - ], - ], - ], - 'attributes' => [], - ], - ], - 'attributes' => [], - ], - 'examples' => 2, - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'plot', - 'children' => [ - [ - 'content' => $plot, - 'matcher' => [ - 'pact:matcher:type' => 'type', - ], - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'great-lines', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'line', - 'children' => [ - [ - 'content' => - 'PHP solves all my web problems', - 'matcher' => [ - 'pact:matcher:type' => - 'type', - ], - ], - ], - 'attributes' => [], - ], - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'rating', - 'children' => [ - [ - 'content' => 7, - 'matcher' => [ - 'pact:matcher:type' => 'type', - ], - ], - ], - 'attributes' => ['type' => 'thumbs'], - ], - [ - 'name' => 'rating', - 'children' => [ - [ - 'content' => 5, - 'matcher' => [ - 'pact:matcher:type' => 'type', - ], - ], - ], - 'attributes' => ['type' => 'stars'], - ], - ], - 'attributes' => [], - ], - ], - ], - 'attributes' => [], - ], - ]; - - $this->assertSame(json_encode($expectedArray), json_encode($this->builder)); - } -} diff --git a/tests/PhpPact/Xml/XmlBuilderTest.php b/tests/PhpPact/Xml/XmlBuilderTest.php index 5cef4158..abc731c7 100644 --- a/tests/PhpPact/Xml/XmlBuilderTest.php +++ b/tests/PhpPact/Xml/XmlBuilderTest.php @@ -2,119 +2,149 @@ namespace PhpPactTest\Xml; +use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Xml\Exception\InvalidXmlElementException; use PHPUnit\Framework\TestCase; use PhpPact\Xml\XmlBuilder; class XmlBuilderTest extends TestCase { + private XmlBuilder $builder; + private Matcher $matcher; + + protected function setUp(): void + { + parent::setUp(); + + $this->builder = new XmlBuilder('1.0', 'UTF-8'); + $this->matcher = new Matcher(); + } + public function testJsonSerializeInvalidXmlElement(): void { $this->expectException(InvalidXmlElementException::class); $this->expectExceptionMessage("Xml element's name is required"); - $builder = new XmlBuilder('1.0', 'UTF-8'); - - $builder + $this->builder ->root( - $builder->name('Root'), - $builder->add(), + $this->builder->name('Root'), + $this->builder->add(), ) ; - json_encode($builder); + json_encode($this->builder); } public function testJsonSerialize(): void { - $builder = new XmlBuilder('1.0', 'UTF-8'); - - $builder + $this->builder ->root( - $builder->name('Root'), - $builder->add( - $builder->name('First Child Second Element'), - $builder->contentLike('Example Test') - ), - $builder->add( - $builder->name('Second Parent'), - $builder->add( - $builder->name('Second child 1'), - $builder->attribute('myAttr', 'Attr Value') - ), - $builder->add( - $builder->name('Second child 2'), - $builder->content('Test') + $this->builder->name('ns1:projects'), + $this->builder->attribute('id', '1234'), + $this->builder->attribute('xmlns:ns1', 'http://some.namespace/and/more/stuff'), + $this->builder->eachLike( + $this->builder->examples(2), + $this->builder->name('ns1:project'), + $this->builder->attribute('id', $this->matcher->integerV3(1)), + $this->builder->attribute('type', 'activity'), + $this->builder->attribute('name', $this->matcher->string('Project 1')), + $this->builder->attribute('due', $this->matcher->datetime("yyyy-MM-dd'T'HH:mm:ss.SZ", '2016-02-11T09:46:56.023Z')), + $this->builder->contentLike('Project 1 description'), + $this->builder->add( + $this->builder->name('ns1:tasks'), + $this->builder->eachLike( + $this->builder->examples(5), + $this->builder->name('ns1:task'), + $this->builder->attribute('id', $this->matcher->integerV3(1)), + $this->builder->attribute('name', $this->matcher->string('Task 1')), + $this->builder->attribute('done', $this->matcher->boolean()), + $this->builder->contentLike('Task 1 description'), + ), ), - $builder->add( - $builder->name('Third Parent'), - $builder->eachLike( - $builder->name('Child') - ) - ), - ), - $builder->add( - $builder->name('First Child Third Element'), ), - ) - ; + ); $expectedArray = [ 'version' => '1.0', 'charset' => 'UTF-8', 'root' => [ - 'name' => 'Root', + 'name' => 'ns1:projects', 'children' => [ [ - 'name' => 'FirstChildSecondElement', - 'children' => [ - [ - 'content' => 'Example Test', - 'matcher' => ['pact:matcher:type' => 'type'], - ], - ], - 'attributes' => [], - ], - [ - 'name' => 'SecondParent', - 'children' => [ - [ - 'name' => 'Secondchild1', - 'children' => [], - 'attributes' => ['myAttr' => 'Attr Value'], - ], - [ - 'name' => 'Secondchild2', - 'children' => [['content' => 'Test']], - 'attributes' => [], - ], - [ - 'name' => 'ThirdParent', - 'children' => [ - [ - 'pact:matcher:type' => 'type', - 'value' => [ - 'name' => 'Child', - 'children' => [], - 'attributes' => [], + 'pact:matcher:type' => 'type', + 'value' => [ + 'name' => 'ns1:project', + 'children' => [ + [ + 'name' => 'ns1:tasks', + 'children' => [ + [ + 'pact:matcher:type' => 'type', + 'value' => [ + 'name' => 'ns1:task', + 'children' => [ + [ + 'content' => + 'Task 1 description', + 'matcher' => [ + 'pact:matcher:type' => + 'type', + ], + ], + ], + 'attributes' => [ + 'id' => [ + 'pact:matcher:type' => + 'integer', + 'value' => 1, + ], + 'name' => [ + 'pact:matcher:type' => 'type', + 'value' => 'Task 1', + ], + 'done' => [ + 'pact:matcher:type' => 'type', + 'value' => true, + ], + ], + ], + 'examples' => 5, ], ], + 'attributes' => [], + ], + [ + 'content' => 'Project 1 description', + 'matcher' => ['pact:matcher:type' => 'type'], + ], + ], + 'attributes' => [ + 'id' => [ + 'pact:matcher:type' => 'integer', + 'value' => 1, + ], + 'type' => 'activity', + 'name' => [ + 'pact:matcher:type' => 'type', + 'value' => 'Project 1', + ], + 'due' => [ + 'pact:matcher:type' => 'datetime', + 'format' => "yyyy-MM-dd'T'HH:mm:ss.SZ", + 'value' => '2016-02-11T09:46:56.023Z', ], - 'attributes' => [], ], ], - 'attributes' => [], - ], - [ - 'name' => 'FirstChildThirdElement', - 'children' => [], - 'attributes' => [], + 'examples' => 2, ], ], - 'attributes' => [], + 'attributes' => [ + 'id' => '1234', + 'xmlns:ns1' => 'http://some.namespace/and/more/stuff', + ], ], ]; - $this->assertSame(json_encode($expectedArray), json_encode($builder)); + $this->assertSame(json_encode($expectedArray), json_encode($this->builder)); } } From 72caf79ae04f0c914241108bef4282ab83f4e686 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 1 Jan 2024 14:13:31 +0700 Subject: [PATCH 184/298] test: Update xml example --- .../src/Service/HttpClientService.php | 2 +- .../tests/Service/HttpClientServiceTest.php | 53 ++++++++++++------- .../xml/pacts/xmlConsumer-xmlProvider.json | 41 +++++++++++--- example/xml/provider/public/index.php | 2 +- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/example/xml/consumer/src/Service/HttpClientService.php b/example/xml/consumer/src/Service/HttpClientService.php index a2193e03..f13a2e16 100644 --- a/example/xml/consumer/src/Service/HttpClientService.php +++ b/example/xml/consumer/src/Service/HttpClientService.php @@ -20,7 +20,7 @@ public function __construct(string $baseUri) public function getMovies(): string { $response = $this->httpClient->get(new Uri("{$this->baseUri}/movies"), [ - 'headers' => ['Accept' => 'text/xml; charset=UTF8'] + 'headers' => ['Accept' => 'application/xml'] ]); return $response->getBody(); diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index 94563c7f..62d691a6 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -2,6 +2,7 @@ namespace XmlConsumer\Tests\Service; +use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Xml\XmlBuilder; use XmlConsumer\Service\HttpClientService; use PhpPact\Consumer\InteractionBuilder; @@ -14,11 +15,13 @@ class HttpClientServiceTest extends TestCase { public function testGetMovies() { + $matcher = new Matcher(); + $request = new ConsumerRequest(); $request ->setMethod('GET') ->setPath('/movies') - ->addHeader('Accept', 'text/xml; charset=UTF8'); + ->addHeader('Accept', $matcher->regex('application/xml', 'application\/.*xml')); $xmlBuilder = new XmlBuilder('1.0', 'UTF-8'); $xmlBuilder @@ -29,7 +32,7 @@ public function testGetMovies() $xmlBuilder->name('movie'), $xmlBuilder->add( $xmlBuilder->name('title'), - $xmlBuilder->contentLike('PHP: Behind the Parser'), + $xmlBuilder->contentLike('Big Buck Bunny'), ), $xmlBuilder->add( $xmlBuilder->name('characters'), @@ -38,21 +41,20 @@ public function testGetMovies() $xmlBuilder->name('character'), $xmlBuilder->add( $xmlBuilder->name('name'), - $xmlBuilder->contentLike('Ms. Coder'), + $xmlBuilder->contentLike('Big Buck Bunny'), ), $xmlBuilder->add( $xmlBuilder->name('actor'), - $xmlBuilder->contentLike('Onlivia Actora'), + $xmlBuilder->contentLike('Jan Morgenstern'), ), ), ), $xmlBuilder->add( $xmlBuilder->name('plot'), $xmlBuilder->contentLike( - <<name('great-lines'), $xmlBuilder->eachLike( $xmlBuilder->name('line'), - $xmlBuilder->contentLike('PHP solves all my web problems'), + $xmlBuilder->contentLike('Open source movie'), ), ), $xmlBuilder->add( $xmlBuilder->name('rating'), - $xmlBuilder->attribute('type', 'thumbs'), - $xmlBuilder->contentLike(7), - ), - $xmlBuilder->add( - $xmlBuilder->name('rating'), - $xmlBuilder->attribute('type', 'stars'), - $xmlBuilder->contentLike(5), + $xmlBuilder->attribute('type', $matcher->regex('stars', 'stars|thumbs')), + $xmlBuilder->contentLike(6), ), + // TODO: implement XML generators + // $xmlBuilder->add( + // $xmlBuilder->name('release-date'), + // $xmlBuilder->content($matcher->date('dd-MM-yyyy')), + // ), ), ); $response = new ProviderResponse(); $response ->setStatus(200) - ->addHeader('Content-Type', 'text/xml') + ->addHeader('Content-Type', $matcher->regex('application/xml', 'application\/.*xml')) ->setBody( json_encode($xmlBuilder) ); @@ -100,10 +102,23 @@ public function testGetMovies() ->willRespondWith($response); $service = new HttpClientService($config->getBaseUri()); - $moviesResult = new \SimpleXMLElement($service->getMovies()); + $movies = new \SimpleXMLElement($service->getMovies()); $verifyResult = $builder->verify(); $this->assertTrue($verifyResult); - $this->assertCount(1, $moviesResult); + $this->assertCount(1, $movies->movie); + $this->assertEquals('Big Buck Bunny', $movies->movie[0]->title); + // TODO: investigate why mock server replace "\r\n" by "\n" on Windows + $this->assertEquals(str_replace("\r\n", "\n", $plot), $movies->movie[0]->plot); + $this->assertCount(1, $movies->movie[0]->{'great-lines'}->line); + $this->assertEquals('Open source movie', $movies->movie[0]->{'great-lines'}->line[0]); + $this->assertEquals('6', $movies->movie[0]->rating); + $this->assertCount(2, $movies->movie[0]->characters->character); + $this->assertEquals('Big Buck Bunny', $movies->movie[0]->characters->character[0]->name); + $this->assertEquals('Jan Morgenstern', $movies->movie[0]->characters->character[0]->actor); + // TODO: implement XML generators + $this->assertEquals('', $movies->movie[0]->characters->character[1]->name); + $this->assertEquals('', $movies->movie[0]->characters->character[1]->actor); + //$this->assertEquals('', $movies->movie[0]->{'release-date'}[0]); } } diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index da5c5acb..21cc297b 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -12,15 +12,28 @@ ], "request": { "headers": { - "Accept": "text/xml; charset=UTF8" + "Accept": "application/xml" + }, + "matchingRules": { + "header": { + "$.Accept[0]": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "application\\/.*xml" + } + ] + } + } }, "method": "GET", "path": "/movies" }, "response": { - "body": "PHP: Behind the ParserMs. CoderOnlivia ActoraSo, this language. It's like, a programming language. Or is it a\nscripting language? All is revealed in this thrilling horror spoof\nof a documentary.PHP solves all my web problems75", + "body": "Big Buck BunnyBig Buck BunnyJan MorgensternThe plot follows a day in the life of Big Buck Bunny, during which time he meets three bullying rodents: the leader, Frank the flying squirrel, and his sidekicks Rinky the red squirrel and Gimera the chinchilla.\nThe rodents amuse themselves by harassing helpless creatures of the forest by throwing fruits, nuts, and rocks at them.Open source movie6", "headers": { - "Content-Type": "text/xml" + "Content-Type": "application/xml" }, "matchingRules": { "body": { @@ -85,9 +98,15 @@ "matchers": [ { "match": "type" - }, + } + ] + }, + "$.movies.movie.rating['@type']": { + "combine": "AND", + "matchers": [ { - "match": "type" + "match": "regex", + "regex": "stars|thumbs" } ] }, @@ -100,7 +119,17 @@ ] } }, - "header": {}, + "header": { + "$['Content-Type'][0]": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "application\\/.*xml" + } + ] + } + }, "status": {} }, "status": 200 diff --git a/example/xml/provider/public/index.php b/example/xml/provider/public/index.php index c48aec96..b3a81eb7 100644 --- a/example/xml/provider/public/index.php +++ b/example/xml/provider/public/index.php @@ -40,7 +40,7 @@ XML ); - return $response->withHeader('Content-Type', 'text/xml'); + return $response->withHeader('Content-Type', 'application/movies+xml'); }); $app->post('/pact-change-state', function (Request $request, Response $response) { From a95c47c6f807483b905ce0dab0fd2a605b396c5e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 7 Jan 2024 08:10:32 +0700 Subject: [PATCH 185/298] refactor!: Rename class to Plugin\AbstractPluginBodyRegistry --- .../Driver/Pact/AbstractPluginPactDriver.php | 4 +- .../PluginBodyNotAddedException.php} | 4 +- ...inNotSupportedBySpecificationException.php | 2 +- .../Body/AbstractPluginBodyRegistry.php | 44 +++++++++++++++++++ .../Contents/AbstractContentsRegistry.php | 30 ------------- 5 files changed, 49 insertions(+), 35 deletions(-) rename src/PhpPact/{SyncMessage => Plugin}/Driver/Pact/AbstractPluginPactDriver.php (88%) rename src/PhpPact/{SyncMessage/Exception/InteractionContentNotAddedException.php => Plugin/Exception/PluginBodyNotAddedException.php} (84%) rename src/PhpPact/{SyncMessage => Plugin}/Exception/PluginNotSupportedBySpecificationException.php (88%) create mode 100644 src/PhpPact/Plugin/Registry/Interaction/Body/AbstractPluginBodyRegistry.php delete mode 100644 src/PhpPact/SyncMessage/Registry/Interaction/Contents/AbstractContentsRegistry.php diff --git a/src/PhpPact/SyncMessage/Driver/Pact/AbstractPluginPactDriver.php b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php similarity index 88% rename from src/PhpPact/SyncMessage/Driver/Pact/AbstractPluginPactDriver.php rename to src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php index 209b4ac0..75d490d7 100644 --- a/src/PhpPact/SyncMessage/Driver/Pact/AbstractPluginPactDriver.php +++ b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php @@ -1,9 +1,9 @@ getContents()); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BodyNotSupportedException('Plugin does not support non-json body contents'); + } + $error = $this->client->call('pactffi_interaction_contents', $this->getInteractionId(), $this->getPart(), $body->getContentType(), $body->getContents()); + if ($error) { + throw new PluginBodyNotAddedException($error); + } + break; + + case $body instanceof Multipart: + throw new BodyNotSupportedException('Plugin does not support multipart body'); + }; + } + + abstract protected function getInteractionId(): int; + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/SyncMessage/Registry/Interaction/Contents/AbstractContentsRegistry.php b/src/PhpPact/SyncMessage/Registry/Interaction/Contents/AbstractContentsRegistry.php deleted file mode 100644 index bf27fe92..00000000 --- a/src/PhpPact/SyncMessage/Registry/Interaction/Contents/AbstractContentsRegistry.php +++ /dev/null @@ -1,30 +0,0 @@ -client->call('pactffi_interaction_contents', $this->getInteractionId(), $this->getPart(), $contentType, $contents); - if ($error) { - throw new InteractionContentNotAddedException($error); - } - } - - abstract protected function getInteractionId(): int; - - abstract protected function getPart(): int; -} From be1a80f4370892521de6523a1d56117fe140692e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 7 Jan 2024 22:59:34 +0700 Subject: [PATCH 186/298] refactor!: Move protobuf code to core --- .../Driver/Pact/ProtobufPactDriver.php | 13 +++++++++ .../Factory/ProtobufMessageDriverFactory.php | 25 +++++++++++++++++ .../ProtobufSyncMessageDriverFactory.php | 27 +++++++++++++++++++ .../Body/ProtobufMessageContentsRegistry.php | 25 +++++++++++++++++ .../Interaction/ProtobufMessageRegistry.php | 20 ++++++++++++++ .../ProtobufSyncMessageRegistry.php | 20 ++++++++++++++ .../Protobuf/Service/GrpcMockServer.php | 13 +++++++++ 7 files changed, 143 insertions(+) create mode 100644 src/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriver.php create mode 100644 src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php create mode 100644 src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php create mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/Body/ProtobufMessageContentsRegistry.php create mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php create mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php create mode 100644 src/PhpPact/Plugins/Protobuf/Service/GrpcMockServer.php diff --git a/src/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriver.php b/src/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriver.php new file mode 100644 index 00000000..176486a9 --- /dev/null +++ b/src/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriver.php @@ -0,0 +1,13 @@ +messageRegistry->getId(); + } +} diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php new file mode 100644 index 00000000..e8134b81 --- /dev/null +++ b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php @@ -0,0 +1,20 @@ + Date: Tue, 9 Jan 2024 02:52:46 +0700 Subject: [PATCH 187/298] chore: Add protobuf sync message example --- .cirrus.yml | 11 +- .github/workflows/build.yml | 20 +++- .php-cs-fixer.php | 1 + composer.json | 23 +++- .../consumer/phpunit.xml | 11 ++ .../consumer/src/CalculatorClient.php | 23 ++++ .../consumer/src/ProtobufClient.php | 23 ++++ .../consumer/tests/ProtobufClientTest.php | 61 ++++++++++ .../library/proto/area_calculator.proto | 47 ++++++++ .../library/src/.gitignore | 1 + ...eConsumer-protobufSyncMessageProvider.json | 104 ++++++++++++++++++ .../protobuf-sync-message/provider/.rr.yaml | 11 ++ .../provider/composer.json | 30 +++++ .../provider/phpunit.xml | 11 ++ .../provider/src/Service/Calculator.php | 71 ++++++++++++ .../src/Service/CalculatorInterface.php | 15 +++ .../provider/tests/PactVerifyTest.php | 54 +++++++++ .../protobuf-sync-message/provider/worker.php | 17 +++ phpunit.xml | 6 + 19 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 example/protobuf-sync-message/consumer/phpunit.xml create mode 100644 example/protobuf-sync-message/consumer/src/CalculatorClient.php create mode 100644 example/protobuf-sync-message/consumer/src/ProtobufClient.php create mode 100644 example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php create mode 100644 example/protobuf-sync-message/library/proto/area_calculator.proto create mode 100644 example/protobuf-sync-message/library/src/.gitignore create mode 100644 example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json create mode 100644 example/protobuf-sync-message/provider/.rr.yaml create mode 100644 example/protobuf-sync-message/provider/composer.json create mode 100644 example/protobuf-sync-message/provider/phpunit.xml create mode 100644 example/protobuf-sync-message/provider/src/Service/Calculator.php create mode 100644 example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php create mode 100644 example/protobuf-sync-message/provider/tests/PactVerifyTest.php create mode 100644 example/protobuf-sync-message/provider/worker.php diff --git a/.cirrus.yml b/.cirrus.yml index 2a3a7c07..cba18c81 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -14,6 +14,8 @@ BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE - composer run lint static_analysis_script: - composer run static-code-analysis + generate_library_script: + - composer gen-lib test_script: - composer test @@ -26,12 +28,16 @@ linux_arm64_task: - VERSION: 8.0 arm_container: image: php:$VERSION + cpu: 4 + memory: 12G pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev + - apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - docker-php-ext-install sockets - docker-php-ext-install ffi + - MAKEFLAGS=" -j4" pecl install grpc + - echo 'extension=grpc.so' >> /usr/local/etc/php/conf.d/grpc.ini version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE @@ -46,7 +52,8 @@ macos_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest pre_req_script: - - brew install php@$VERSION composer + - brew install php@$VERSION composer protobuf + - MAKEFLAGS=" -j4" pecl install grpc version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7ebeac3..599bf339 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,15 +60,33 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: sockets, curl, zip, ffi + extensions: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} php-version: ${{ matrix.php }} coverage: none ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install gRPC Extension (for PHP 8.2 on Windows) + run: | + cd C:\tools\php + Invoke-WebRequest -Uri https://phpdev.toolsforresearch.com/php-8.2.7-nts-Win32-vs16-x64-grpc-protobuf.zip -OutFile php8.2.zip + unzip php8.2.zip ext/php_grpc.dll + rm php8.2.zip + echo "extension=php_grpc.dll" >> php.ini + if: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' }} + shell: pwsh + - name: Composer install uses: ramsey/composer-install@v2 with: dependency-versions: ${{ matrix.dependencies }} + - name: Generate Library + run: composer gen-lib + - name: Composer test run: composer test diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 43a2591d..3bb3d850 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -5,6 +5,7 @@ ->in(__DIR__ . '/tests') ->in(__DIR__ . '/example') ->in(__DIR__ . '/compatibility-suite/tests') + ->exclude('library/src') ->name('*.php'); $config = new PhpCsFixer\Config(); diff --git a/composer.json b/composer.json index 577c5b37..af8416bd 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", - "ramsey/uuid": "^4.7" + "ramsey/uuid": "^4.7", + "pact-foundation/example-protobuf-sync-message-provider": "@dev" }, "autoload": { "psr-4": { @@ -72,7 +73,14 @@ "MatchersProvider\\Tests\\": "example/matchers/provider/tests", "GeneratorsConsumer\\": "example/generators/consumer/src", "GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests", - "GeneratorsProvider\\Tests\\": "example/generators/provider/tests" + "GeneratorsProvider\\Tests\\": "example/generators/provider/tests", + "": [ + "example/protobuf-sync-message/library/src" + ], + "ProtobufSyncMessageConsumer\\": "example/protobuf-sync-message/consumer/src", + "ProtobufSyncMessageConsumer\\Tests\\": "example/protobuf-sync-message/consumer/tests", + "ProtobufSyncMessageProvider\\": "example/protobuf-sync-message/provider/src", + "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests" } }, "scripts": { @@ -83,6 +91,9 @@ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", "phpunit --debug" ], + "gen-lib": [ + "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto" + ], "check-compatibility": "behat" }, "extra": { @@ -120,5 +131,11 @@ "allow-plugins": { "tienvx/composer-downloads-plugin": true } - } + }, + "repositories": [ + { + "type": "path", + "url": "example/protobuf-sync-message/provider" + } + ] } diff --git a/example/protobuf-sync-message/consumer/phpunit.xml b/example/protobuf-sync-message/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/protobuf-sync-message/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-sync-message/consumer/src/CalculatorClient.php b/example/protobuf-sync-message/consumer/src/CalculatorClient.php new file mode 100644 index 00000000..4ce7497a --- /dev/null +++ b/example/protobuf-sync-message/consumer/src/CalculatorClient.php @@ -0,0 +1,23 @@ +_simpleRequest( + '/plugins.Calculator/calculate', + $request, + [AreaResponse::class, 'decode'], + $metadata, + [] + )->wait(); + + return $response; + } +} diff --git a/example/protobuf-sync-message/consumer/src/ProtobufClient.php b/example/protobuf-sync-message/consumer/src/ProtobufClient.php new file mode 100644 index 00000000..44602d6e --- /dev/null +++ b/example/protobuf-sync-message/consumer/src/ProtobufClient.php @@ -0,0 +1,23 @@ +baseUrl, [ + 'credentials' => ChannelCredentials::createInsecure(), + ]); + + return $client->calculate($shapeMessage); + } +} diff --git a/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php new file mode 100644 index 00000000..55cde3cc --- /dev/null +++ b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php @@ -0,0 +1,61 @@ +setConsumer('protobufSyncMessageConsumer'); + $config->setProvider('protobufSyncMessageProvider'); + $config->setPactSpecificationVersion('4.0.0'); + $config->setPactDir(__DIR__.'/../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $config->setHost('127.0.0.1'); + $builder = new SyncMessageBuilder($config, new ProtobufSyncMessageDriverFactory()); + $builder + ->expectsToReceive('request for calculate shape area') + ->withMetadata([]) + ->withContent(new Text( + json_encode([ + 'pact:proto' => $protoPath, + 'pact:content-type' => 'application/grpc', + 'pact:proto-service' => 'Calculator/calculate', + + 'request' => [ + 'rectangle' => [ + 'length' => 'matching(number, 3)', + 'width' => 'matching(number, 4)', + ], + ], + 'response' => [ + 'value' => 'matching(number, 12)', + ] + ]), + 'application/grpc' + )); + $builder->registerMessage(); + + $service = new ProtobufClient("{$config->getHost()}:{$config->getPort()}"); + $rectangle = (new Rectangle())->setLength(3)->setWidth(4); + $message = (new ShapeMessage())->setRectangle($rectangle); + $response = $service->calculate($message); + + $this->assertTrue($builder->verify()); + $this->assertEquals(3 * 4, $response->getValue()); + } +} diff --git a/example/protobuf-sync-message/library/proto/area_calculator.proto b/example/protobuf-sync-message/library/proto/area_calculator.proto new file mode 100644 index 00000000..8bce6c99 --- /dev/null +++ b/example/protobuf-sync-message/library/proto/area_calculator.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package plugins; + +option php_generic_services = true; + +service Calculator { + rpc calculate (ShapeMessage) returns (AreaResponse) {} +} + +message ShapeMessage { + oneof shape { + Square square = 1; + Rectangle rectangle = 2; + Circle circle = 3; + Triangle triangle = 4; + Parallelogram parallelogram = 5; + } +} + +message Square { + float edge_length = 1; +} + +message Rectangle { + float length = 1; + float width = 2; +} + +message Circle { + float radius = 1; +} + +message Triangle { + float edge_a = 1; + float edge_b = 2; + float edge_c = 3; +} + +message Parallelogram { + float base_length = 1; + float height = 2; +} + +message AreaResponse { + float value = 1; +} diff --git a/example/protobuf-sync-message/library/src/.gitignore b/example/protobuf-sync-message/library/src/.gitignore new file mode 100644 index 00000000..cde8069e --- /dev/null +++ b/example/protobuf-sync-message/library/src/.gitignore @@ -0,0 +1 @@ +*.php diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json new file mode 100644 index 00000000..ab308e25 --- /dev/null +++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json @@ -0,0 +1,104 @@ +{ + "consumer": { + "name": "protobufSyncMessageConsumer" + }, + "interactions": [ + { + "description": "request for calculate shape area", + "interactionMarkup": { + "markup": "```protobuf\nmessage AreaResponse {\n float value = 1;\n}\n```\n", + "markupType": "COMMON_MARK" + }, + "pending": false, + "pluginConfiguration": { + "protobuf": { + "descriptorKey": "6b90c212dfe22dc3c119d1c3fe42b5e1", + "service": "Calculator/calculate" + } + }, + "request": { + "contents": { + "content": "EgoNAABAQBUAAIBA", + "contentType": "application/protobuf;message=ShapeMessage", + "contentTypeHint": "BINARY", + "encoded": "base64" + }, + "matchingRules": { + "body": { + "$.rectangle.length": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + }, + "$.rectangle.width": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + } + } + }, + "metadata": { + "contentType": "application/protobuf;message=ShapeMessage" + } + }, + "response": [ + { + "contents": { + "content": "DQAAQEE=", + "contentType": "application/protobuf;message=AreaResponse", + "contentTypeHint": "BINARY", + "encoded": "base64" + }, + "matchingRules": { + "body": { + "$.value": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + } + } + }, + "metadata": { + "contentType": "application/protobuf;message=AreaResponse" + } + } + ], + "transport": "grpc", + "type": "Synchronous/Messages" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.11", + "mockserver": "1.2.4", + "models": "1.1.12" + }, + "pactSpecification": { + "version": "4.0" + }, + "plugins": [ + { + "configuration": { + "6b90c212dfe22dc3c119d1c3fe42b5e1": { + "protoDescriptors": "CtYFChVhcmVhX2NhbGN1bGF0b3IucHJvdG8SB3BsdWdpbnMikgIKDFNoYXBlTWVzc2FnZRIpCgZzcXVhcmUYASABKAsyDy5wbHVnaW5zLlNxdWFyZUgAUgZzcXVhcmUSMgoJcmVjdGFuZ2xlGAIgASgLMhIucGx1Z2lucy5SZWN0YW5nbGVIAFIJcmVjdGFuZ2xlEikKBmNpcmNsZRgDIAEoCzIPLnBsdWdpbnMuQ2lyY2xlSABSBmNpcmNsZRIvCgh0cmlhbmdsZRgEIAEoCzIRLnBsdWdpbnMuVHJpYW5nbGVIAFIIdHJpYW5nbGUSPgoNcGFyYWxsZWxvZ3JhbRgFIAEoCzIWLnBsdWdpbnMuUGFyYWxsZWxvZ3JhbUgAUg1wYXJhbGxlbG9ncmFtQgcKBXNoYXBlIikKBlNxdWFyZRIfCgtlZGdlX2xlbmd0aBgBIAEoAlIKZWRnZUxlbmd0aCI5CglSZWN0YW5nbGUSFgoGbGVuZ3RoGAEgASgCUgZsZW5ndGgSFAoFd2lkdGgYAiABKAJSBXdpZHRoIiAKBkNpcmNsZRIWCgZyYWRpdXMYASABKAJSBnJhZGl1cyJPCghUcmlhbmdsZRIVCgZlZGdlX2EYASABKAJSBWVkZ2VBEhUKBmVkZ2VfYhgCIAEoAlIFZWRnZUISFQoGZWRnZV9jGAMgASgCUgVlZGdlQyJICg1QYXJhbGxlbG9ncmFtEh8KC2Jhc2VfbGVuZ3RoGAEgASgCUgpiYXNlTGVuZ3RoEhYKBmhlaWdodBgCIAEoAlIGaGVpZ2h0IiQKDEFyZWFSZXNwb25zZRIUCgV2YWx1ZRgBIAEoAlIFdmFsdWUySQoKQ2FsY3VsYXRvchI7CgljYWxjdWxhdGUSFS5wbHVnaW5zLlNoYXBlTWVzc2FnZRoVLnBsdWdpbnMuQXJlYVJlc3BvbnNlIgBCA9ACAWIGcHJvdG8z", + "protoFile": "syntax = \"proto3\";\n\npackage plugins;\n\noption php_generic_services = true;\n\nservice Calculator {\n rpc calculate (ShapeMessage) returns (AreaResponse) {}\n}\n\nmessage ShapeMessage {\n oneof shape {\n Square square = 1;\n Rectangle rectangle = 2;\n Circle circle = 3;\n Triangle triangle = 4;\n Parallelogram parallelogram = 5;\n }\n}\n\nmessage Square {\n float edge_length = 1;\n}\n\nmessage Rectangle {\n float length = 1;\n float width = 2;\n}\n\nmessage Circle {\n float radius = 1;\n}\n\nmessage Triangle {\n float edge_a = 1;\n float edge_b = 2;\n float edge_c = 3;\n}\n\nmessage Parallelogram {\n float base_length = 1;\n float height = 2;\n}\n\nmessage AreaResponse {\n float value = 1;\n}\n" + } + }, + "name": "protobuf", + "version": "0.3.8" + } + ] + }, + "provider": { + "name": "protobufSyncMessageProvider" + } +} \ No newline at end of file diff --git a/example/protobuf-sync-message/provider/.rr.yaml b/example/protobuf-sync-message/provider/.rr.yaml new file mode 100644 index 00000000..25f94af2 --- /dev/null +++ b/example/protobuf-sync-message/provider/.rr.yaml @@ -0,0 +1,11 @@ +version: "2.7" + +server: + command: "php worker.php" + +grpc: + listen: "tcp://127.0.0.1:9001" + proto: + - "../library/proto/area_calculator.proto" + pool: + num_workers: 1 diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json new file mode 100644 index 00000000..e75c3542 --- /dev/null +++ b/example/protobuf-sync-message/provider/composer.json @@ -0,0 +1,30 @@ +{ + "name": "pact-foundation/example-protobuf-sync-message-provider", + "require": { + "grpc/grpc": "^1.57", + "spiral/roadrunner-grpc": "^2.0", + "tienvx/composer-downloads-plugin": "^1.2" + }, + "require-dev": { + "ext-grpc": "*" + }, + "extra": { + "downloads": { + "rr": { + "version": "2023.3.9", + "variables": { + "{$os}": "strtolower(PHP_OS_FAMILY)", + "{$architecture}": "in_array(php_uname('m'), ['x86_64', 'AMD64']) ? 'amd64' : 'arm64'", + "{$extension}": "(PHP_OS_FAMILY === 'Windows' || (PHP_OS_FAMILY === 'Darwin' && php_uname('m') === 'x86_64')) ? 'zip' : 'tar.gz'" + }, + "url": "https://github.com/roadrunner-server/roadrunner/releases/download/v{$version}/roadrunner-{$version}-{$os}-{$architecture}.{$extension}", + "path": "bin/roadrunner" + } + } + }, + "config": { + "allow-plugins": { + "tienvx/composer-downloads-plugin": true + } + } +} diff --git a/example/protobuf-sync-message/provider/phpunit.xml b/example/protobuf-sync-message/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/protobuf-sync-message/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-sync-message/provider/src/Service/Calculator.php b/example/protobuf-sync-message/provider/src/Service/Calculator.php new file mode 100644 index 00000000..6ba7f07a --- /dev/null +++ b/example/protobuf-sync-message/provider/src/Service/Calculator.php @@ -0,0 +1,71 @@ +getShape()) { + case 'square': + $area = $this->calculateSquareArea($request->getSquare()); + break; + case 'rectangle': + $area = $this->calculateRectangleArea($request->getRectangle()); + break; + case 'circle': + $area = $this->calculateCircleArea($request->getCircle()); + break; + case 'triangle': + $area = $this->calculateTriangleArea($request->getTriangle()); + break; + case 'parallelogram': + $area = $this->calculateParallelogramArea($request->getParallelogram()); + break; + default: + throw new Exception(sprintf('Shape %s is not supported', $request->getShape())); + } + + return new AreaResponse(['value' => $area]); + } + + private function calculateSquareArea(Square $square): float + { + return pow($square->getEdgeLength(), 2); + } + + private function calculateRectangleArea(Rectangle $rectangle): float + { + return $rectangle->getWidth() * $rectangle->getLength(); + } + + private function calculateCircleArea(Circle $circle): float + { + return pi() * pow($circle->getRadius(), 2); + } + + /** + * Use Heron's formula. + */ + private function calculateTriangleArea(Triangle $triangle): float + { + $p = ($triangle->getEdgeA() + $triangle->getEdgeB() + $triangle->getEdgeC()) / 2; + + return sqrt($p * ($p - $triangle->getEdgeA()) * ($p - $triangle->getEdgeB()) * ($p - $triangle->getEdgeC())); + } + + private function calculateParallelogramArea(Parallelogram $parallelogram): float + { + return $parallelogram->getBaseLength() * $parallelogram->getHeight(); + } +} diff --git a/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php new file mode 100644 index 00000000..990b455a --- /dev/null +++ b/example/protobuf-sync-message/provider/src/Service/CalculatorInterface.php @@ -0,0 +1,15 @@ +process = new Process([__DIR__ . '/../bin/roadrunner/rr', 'serve', '-w', __DIR__ . '/..']); + $this->process->setTimeout(120); + + $this->process->start(function (string $type, string $buffer): void { + echo "\n$type > $buffer"; + }); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 9001))); + } + + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer(): void + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('protobufSyncMessageProvider') + ->setHost('127.0.0.1'); + $providerTransport = new ProviderTransport(); + $providerTransport + ->setProtocol('grpc') + ->setScheme('tcp') + ->setPort(9001) + ->setPath('/') + ; + $config->addProviderTransport($providerTransport); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json'); + + $this->assertTrue($verifier->verify()); + } +} diff --git a/example/protobuf-sync-message/provider/worker.php b/example/protobuf-sync-message/provider/worker.php new file mode 100644 index 00000000..55c59fb3 --- /dev/null +++ b/example/protobuf-sync-message/provider/worker.php @@ -0,0 +1,17 @@ + false, // optional (default: false) +]); + +$server->registerService(CalculatorInterface::class, new Calculator()); + +$server->serve(Worker::create()); diff --git a/phpunit.xml b/phpunit.xml index e300d5a8..b4e7bf88 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -55,6 +55,12 @@ ./example/generators/provider/tests + + ./example/protobuf-sync-message/consumer/tests + + + ./example/protobuf-sync-message/provider/tests + From 908b8fe6d610fe02e3f592951f53ab03638515a9 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 13 Jan 2024 17:26:52 +0700 Subject: [PATCH 188/298] chore: Suggest install grpc extension for protobuf sync message example --- example/protobuf-sync-message/provider/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json index e75c3542..85598def 100644 --- a/example/protobuf-sync-message/provider/composer.json +++ b/example/protobuf-sync-message/provider/composer.json @@ -5,8 +5,8 @@ "spiral/roadrunner-grpc": "^2.0", "tienvx/composer-downloads-plugin": "^1.2" }, - "require-dev": { - "ext-grpc": "*" + "suggest": { + "ext-grpc": "If you want to use gRPC then you must install and use this" }, "extra": { "downloads": { From 90b84616bbe52f2e8994b3f73e62e07e1a2d6934 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 13 Jan 2024 17:58:21 +0700 Subject: [PATCH 189/298] chore: Revert require-dev's ext-grpc --- example/protobuf-sync-message/provider/composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json index 85598def..e864a0f1 100644 --- a/example/protobuf-sync-message/provider/composer.json +++ b/example/protobuf-sync-message/provider/composer.json @@ -5,6 +5,9 @@ "spiral/roadrunner-grpc": "^2.0", "tienvx/composer-downloads-plugin": "^1.2" }, + "require-dev": { + "ext-grpc": "*" + }, "suggest": { "ext-grpc": "If you want to use gRPC then you must install and use this" }, From b5caced82e9408a7f3f808d757ccc701dc7296b7 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 13 Jan 2024 09:36:32 +0700 Subject: [PATCH 190/298] ci(test): Compile grpc and cache docker image on Cirrus CI --- .cirrus.yml | 14 +++----------- .cirrus/linux_arm64/Dockerfile | 8 ++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 .cirrus/linux_arm64/Dockerfile diff --git a/.cirrus.yml b/.cirrus.yml index cba18c81..508d4ef9 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -27,17 +27,9 @@ linux_arm64_task: - VERSION: 8.1 - VERSION: 8.0 arm_container: - image: php:$VERSION - cpu: 4 - memory: 12G - pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler - - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - - docker-php-ext-install sockets - - docker-php-ext-install ffi - - MAKEFLAGS=" -j4" pecl install grpc - - echo 'extension=grpc.so' >> /usr/local/etc/php/conf.d/grpc.ini + dockerfile: .cirrus/linux_arm64/Dockerfile + docker_arguments: + VERSION: $VERSION version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/.cirrus/linux_arm64/Dockerfile b/.cirrus/linux_arm64/Dockerfile new file mode 100644 index 00000000..f3c4e43a --- /dev/null +++ b/.cirrus/linux_arm64/Dockerfile @@ -0,0 +1,8 @@ +ARG VERSION +FROM php:${VERSION} +RUN apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler +COPY --from=composer/composer:2-bin /composer /usr/local/bin/composer +RUN docker-php-ext-install sockets +RUN docker-php-ext-install ffi +RUN pecl install grpc +RUN echo 'extension=grpc.so' >> /usr/local/etc/php/conf.d/grpc.ini From af27c1835080db72e0dc039e32f629ad646c5c5c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 2 Jan 2024 01:25:26 +0700 Subject: [PATCH 191/298] refactor!: Move csv to core --- composer.json | 4 + example/csv/consumer/phpunit.xml | 11 ++ .../src/Service/HttpClientService.php | 28 +++++ .../tests/Service/HttpClientServiceTest.php | 63 ++++++++++ .../csv/pacts/csvConsumer-csvProvider.json | 108 ++++++++++++++++++ example/csv/provider/phpunit.xml | 11 ++ example/csv/provider/public/index.php | 15 +++ example/csv/provider/public/report.csv | 11 ++ example/csv/provider/tests/PactVerifyTest.php | 53 +++++++++ example/xml/provider/tests/PactVerifyTest.php | 2 +- phpunit.xml | 6 + .../Plugins/Csv/Driver/Pact/CsvPactDriver.php | 13 +++ .../Factory/CsvInteractionDriverFactory.php | 27 +++++ .../Body/CsvResponseBodyRegistry.php | 25 ++++ .../Interaction/CsvInteractionRegistry.php | 24 ++++ 15 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 example/csv/consumer/phpunit.xml create mode 100644 example/csv/consumer/src/Service/HttpClientService.php create mode 100644 example/csv/consumer/tests/Service/HttpClientServiceTest.php create mode 100644 example/csv/pacts/csvConsumer-csvProvider.json create mode 100644 example/csv/provider/phpunit.xml create mode 100644 example/csv/provider/public/index.php create mode 100644 example/csv/provider/public/report.csv create mode 100644 example/csv/provider/tests/PactVerifyTest.php create mode 100644 src/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriver.php create mode 100644 src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php create mode 100644 src/PhpPact/Plugins/Csv/Registry/Interaction/Body/CsvResponseBodyRegistry.php create mode 100644 src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php diff --git a/composer.json b/composer.json index af8416bd..c83557c9 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,10 @@ "GeneratorsConsumer\\": "example/generators/consumer/src", "GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests", "GeneratorsProvider\\Tests\\": "example/generators/provider/tests", + "CsvConsumer\\": "example/csv/consumer/src", + "CsvConsumer\\Tests\\": "example/csv/consumer/tests", + "CsvProvider\\": "example/csv/provider/src", + "CsvProvider\\Tests\\": "example/csv/provider/tests", "": [ "example/protobuf-sync-message/library/src" ], diff --git a/example/csv/consumer/phpunit.xml b/example/csv/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/csv/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/csv/consumer/src/Service/HttpClientService.php b/example/csv/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..8052090f --- /dev/null +++ b/example/csv/consumer/src/Service/HttpClientService.php @@ -0,0 +1,28 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getReport(): array + { + $response = $this->httpClient->get(new Uri("{$this->baseUri}/report.csv"), [ + 'headers' => ['Accept' => 'text/csv'] + ]); + + return str_getcsv($response->getBody()); + } +} diff --git a/example/csv/consumer/tests/Service/HttpClientServiceTest.php b/example/csv/consumer/tests/Service/HttpClientServiceTest.php new file mode 100644 index 00000000..57373e75 --- /dev/null +++ b/example/csv/consumer/tests/Service/HttpClientServiceTest.php @@ -0,0 +1,63 @@ +setMethod('GET') + ->setPath('/report.csv') + ->addHeader('Accept', 'text/csv') + ; + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->setBody(new Text( + json_encode([ + 'csvHeaders' => false, + 'column:1' => "matching(type,'Name')", + 'column:2' => 'matching(number,100)', + 'column:3' => "matching(datetime, 'yyyy-MM-dd','2000-01-01')", + ]), + 'text/csv' + )) + ; + + $config = new MockServerConfig(); + $config + ->setConsumer('csvConsumer') + ->setProvider('csvProvider') + ->setPactSpecificationVersion('4.0.0') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config, new CsvInteractionDriverFactory()); + $builder + ->given('report.csv file exist') + ->uponReceiving('request for a report.csv') + ->with($request) + ->willRespondWith($response) + ; + + $service = new HttpClientService($config->getBaseUri()); + $columns = $service->getReport(); + + $this->assertTrue($builder->verify()); + $this->assertCount(3, $columns); + $this->assertSame(['Name', '100', '2000-01-01'], $columns); + } +} diff --git a/example/csv/pacts/csvConsumer-csvProvider.json b/example/csv/pacts/csvConsumer-csvProvider.json new file mode 100644 index 00000000..8d001bc1 --- /dev/null +++ b/example/csv/pacts/csvConsumer-csvProvider.json @@ -0,0 +1,108 @@ +{ + "consumer": { + "name": "csvConsumer" + }, + "interactions": [ + { + "description": "request for a report.csv", + "interactionMarkup": { + "markup": "# Data\n\n|Name|100|2000-01-01|\n", + "markupType": "COMMON_MARK" + }, + "pending": false, + "pluginConfiguration": { + "csv": { + "csvHeaders": false + } + }, + "providerStates": [ + { + "name": "report.csv file exist" + } + ], + "request": { + "headers": { + "Accept": [ + "text/csv" + ] + }, + "method": "GET", + "path": "/report.csv" + }, + "response": { + "body": { + "content": "Name,100,2000-01-01\n", + "contentType": "text/csv;charset=utf-8", + "contentTypeHint": "DEFAULT", + "encoded": false + }, + "generators": { + "body": { + "column:3": { + "format": "yyyy-MM-dd", + "type": "DateTime" + } + } + }, + "headers": { + "content-type": [ + "text/csv" + ] + }, + "matchingRules": { + "body": { + "column:1": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "column:2": { + "combine": "AND", + "matchers": [ + { + "match": "number" + } + ] + }, + "column:3": { + "combine": "AND", + "matchers": [ + { + "format": "yyyy-MM-dd", + "match": "datetime" + } + ] + } + }, + "status": {} + }, + "status": 200 + }, + "transport": "http", + "type": "Synchronous/HTTP" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.11", + "mockserver": "1.2.4", + "models": "1.1.12" + }, + "pactSpecification": { + "version": "4.0" + }, + "plugins": [ + { + "configuration": {}, + "name": "csv", + "version": "0.0.3" + } + ] + }, + "provider": { + "name": "csvProvider" + } +} \ No newline at end of file diff --git a/example/csv/provider/phpunit.xml b/example/csv/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/csv/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/csv/provider/public/index.php b/example/csv/provider/public/index.php new file mode 100644 index 00000000..bfebea0d --- /dev/null +++ b/example/csv/provider/public/index.php @@ -0,0 +1,15 @@ +post('/pact-change-state', function (Request $request, Response $response) { + return $response; +}); + +$app->run(); diff --git a/example/csv/provider/public/report.csv b/example/csv/provider/public/report.csv new file mode 100644 index 00000000..17364fa4 --- /dev/null +++ b/example/csv/provider/public/report.csv @@ -0,0 +1,11 @@ +"Mack Greenholt Jr.",943,2003-02-05 +"Luigi Ruecker",654,1980-09-11 +"Mr. Unique Zieme",79,2020-04-30 +"Leif Price",367,2013-04-07 +"Destiny Rodriguez",995,1983-03-20 +"Lavinia Carroll",344,2010-08-01 +"Reta Schoen",113,2001-01-26 +"Mrs. Carolina Swift",494,1982-01-20 +"Gene Crona PhD",852,1992-11-16 +"Dr. Santino Koepp",79,1984-05-05 +"Luigi Sanford",969,1995-07-02 diff --git a/example/csv/provider/tests/PactVerifyTest.php b/example/csv/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..93ae369d --- /dev/null +++ b/example/csv/provider/tests/PactVerifyTest.php @@ -0,0 +1,53 @@ +process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process->start(); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer(): void + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('csvProvider') + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/csvConsumer-csvProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/example/xml/provider/tests/PactVerifyTest.php b/example/xml/provider/tests/PactVerifyTest.php index 925afc4c..eedbff1d 100644 --- a/example/xml/provider/tests/PactVerifyTest.php +++ b/example/xml/provider/tests/PactVerifyTest.php @@ -1,6 +1,6 @@ ./example/generators/provider/tests + + ./example/csv/consumer/tests + + + ./example/csv/provider/tests + ./example/protobuf-sync-message/consumer/tests diff --git a/src/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriver.php b/src/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriver.php new file mode 100644 index 00000000..045fe996 --- /dev/null +++ b/src/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriver.php @@ -0,0 +1,13 @@ +interactionRegistry->getId(); + } +} diff --git a/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php b/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php new file mode 100644 index 00000000..615d50c7 --- /dev/null +++ b/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php @@ -0,0 +1,24 @@ + Date: Mon, 8 Jan 2024 14:24:49 +0700 Subject: [PATCH 192/298] chore: Add protobuf async message example --- composer.json | 11 ++- .../consumer/phpunit.xml | 11 +++ .../MessageHandler/PersonMessageHandler.php | 18 ++++ .../consumer/src/Service/SayHelloService.php | 11 +++ .../PersonMessageHandlerTest.php | 73 +++++++++++++++ .../library/proto/say_hello.proto | 13 +++ .../library/src/.gitignore | 1 + ...Consumer-protobufAsyncMessageProvider.json | 93 +++++++++++++++++++ .../provider/phpunit.xml | 11 +++ .../provider/public/index.php | 50 ++++++++++ .../provider/tests/PactVerifyTest.php | 53 +++++++++++ phpunit.xml | 6 ++ 12 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 example/protobuf-async-message/consumer/phpunit.xml create mode 100644 example/protobuf-async-message/consumer/src/MessageHandler/PersonMessageHandler.php create mode 100644 example/protobuf-async-message/consumer/src/Service/SayHelloService.php create mode 100644 example/protobuf-async-message/consumer/tests/MessageHandler/PersonMessageHandlerTest.php create mode 100644 example/protobuf-async-message/library/proto/say_hello.proto create mode 100644 example/protobuf-async-message/library/src/.gitignore create mode 100644 example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json create mode 100644 example/protobuf-async-message/provider/phpunit.xml create mode 100644 example/protobuf-async-message/provider/public/index.php create mode 100644 example/protobuf-async-message/provider/tests/PactVerifyTest.php diff --git a/composer.json b/composer.json index af8416bd..f0823d9d 100644 --- a/composer.json +++ b/composer.json @@ -75,12 +75,16 @@ "GeneratorsConsumer\\Tests\\": "example/generators/consumer/tests", "GeneratorsProvider\\Tests\\": "example/generators/provider/tests", "": [ - "example/protobuf-sync-message/library/src" + "example/protobuf-sync-message/library/src", + "example/protobuf-async-message/library/src" ], "ProtobufSyncMessageConsumer\\": "example/protobuf-sync-message/consumer/src", "ProtobufSyncMessageConsumer\\Tests\\": "example/protobuf-sync-message/consumer/tests", "ProtobufSyncMessageProvider\\": "example/protobuf-sync-message/provider/src", - "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests" + "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests", + "ProtobufAsyncMessageConsumer\\": "example/protobuf-async-message/consumer/src", + "ProtobufAsyncMessageConsumer\\Tests\\": "example/protobuf-async-message/consumer/tests", + "ProtobufAsyncMessageProvider\\Tests\\": "example/protobuf-async-message/provider/tests" } }, "scripts": { @@ -92,7 +96,8 @@ "phpunit --debug" ], "gen-lib": [ - "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto" + "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto", + "protoc --php_out=example/protobuf-async-message/library/src example/protobuf-async-message/library/proto/say_hello.proto" ], "check-compatibility": "behat" }, diff --git a/example/protobuf-async-message/consumer/phpunit.xml b/example/protobuf-async-message/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/protobuf-async-message/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-async-message/consumer/src/MessageHandler/PersonMessageHandler.php b/example/protobuf-async-message/consumer/src/MessageHandler/PersonMessageHandler.php new file mode 100644 index 00000000..26c51aad --- /dev/null +++ b/example/protobuf-async-message/consumer/src/MessageHandler/PersonMessageHandler.php @@ -0,0 +1,18 @@ +service->sayHello($person->getName()->getGiven(), $person->getName()->getSurname()); + } +} diff --git a/example/protobuf-async-message/consumer/src/Service/SayHelloService.php b/example/protobuf-async-message/consumer/src/Service/SayHelloService.php new file mode 100644 index 00000000..a0939825 --- /dev/null +++ b/example/protobuf-async-message/consumer/src/Service/SayHelloService.php @@ -0,0 +1,11 @@ +createMock(SayHelloService::class); + $service + ->expects($this->once()) + ->method('sayHello') + ->with($this->given, $this->surname); + $this->service = $service; + } + + public function testInvoke(): void + { + $id = 'd1f077b5-0f91-40aa-b8f9-568b50ee4dd9'; + + $config = (new PactMessageConfig()) + ->setConsumer('protobufAsyncMessageConsumer') + ->setProvider('protobufAsyncMessageProvider') + ->setPactSpecificationVersion('4.0.0') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + + $builder = new MessageBuilder($config, new ProtobufMessageDriverFactory()); + + $builder + ->given('A person with fixed id exists', ['id' => $id, 'reuse' => '0']) + ->expectsToReceive('Person message sent') + ->withContent(new Text( + json_encode([ + 'pact:proto' => __DIR__ . '/../../../library/proto/say_hello.proto', + 'pact:message-type' => 'Person', + 'pact:content-type' => 'application/protobuf', + 'id' => "matching(regex, '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', '{$id}')", + 'name' => [ + 'given' => "matching(type, '{$this->given}')", + 'surname' => "matching(type, '{$this->surname}')" + ], + ]), + 'application/protobuf' + )); + + $builder->setCallback(function (string $pactJson): void { + $message = \json_decode($pactJson); + $person = new Person(); + $decoded = base64_decode($message->contents->content); + $person->mergeFromString($decoded); + $handler = new PersonMessageHandler($this->service); + $handler($person); + }); + + $this->assertTrue($builder->verify()); + } +} diff --git a/example/protobuf-async-message/library/proto/say_hello.proto b/example/protobuf-async-message/library/proto/say_hello.proto new file mode 100644 index 00000000..7db28ef6 --- /dev/null +++ b/example/protobuf-async-message/library/proto/say_hello.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package library; + +message Person { + string id = 1; + Name name = 2; +} + +message Name { + string given = 1; + string surname = 2; +} diff --git a/example/protobuf-async-message/library/src/.gitignore b/example/protobuf-async-message/library/src/.gitignore new file mode 100644 index 00000000..cde8069e --- /dev/null +++ b/example/protobuf-async-message/library/src/.gitignore @@ -0,0 +1 @@ +*.php diff --git a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json new file mode 100644 index 00000000..4fd83ff1 --- /dev/null +++ b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json @@ -0,0 +1,93 @@ +{ + "consumer": { + "name": "protobufAsyncMessageConsumer" + }, + "interactions": [ + { + "contents": { + "content": "CiRkMWYwNzdiNS0wZjkxLTQwYWEtYjhmOS01NjhiNTBlZTRkZDkSEAoFR2l2ZW4SB1N1cm5hbWU=", + "contentType": "application/protobuf;message=Person", + "contentTypeHint": "BINARY", + "encoded": "base64" + }, + "description": "Person message sent", + "interactionMarkup": { + "markup": "```protobuf\nmessage Person {\n string id = 1;\n message .library.Name name = 2;\n}\n```\n", + "markupType": "COMMON_MARK" + }, + "matchingRules": { + "body": { + "$.id": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" + } + ] + }, + "$.name.given": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.name.surname": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + } + }, + "metadata": { + "contentType": "application/protobuf;message=Person" + }, + "pending": false, + "pluginConfiguration": { + "protobuf": { + "descriptorKey": "f77f40284a5ed1f38188ed943aca6938", + "message": "Person" + } + }, + "providerStates": [ + { + "name": "A person with fixed id exists", + "params": { + "id": "d1f077b5-0f91-40aa-b8f9-568b50ee4dd9", + "reuse": 0 + } + } + ], + "type": "Asynchronous/Messages" + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.11", + "models": "1.1.12" + }, + "pactSpecification": { + "version": "4.0" + }, + "plugins": [ + { + "configuration": { + "f77f40284a5ed1f38188ed943aca6938": { + "protoDescriptors": "CpcBCg9zYXlfaGVsbG8ucHJvdG8SB2xpYnJhcnkiOwoGUGVyc29uEg4KAmlkGAEgASgJUgJpZBIhCgRuYW1lGAIgASgLMg0ubGlicmFyeS5OYW1lUgRuYW1lIjYKBE5hbWUSFAoFZ2l2ZW4YASABKAlSBWdpdmVuEhgKB3N1cm5hbWUYAiABKAlSB3N1cm5hbWViBnByb3RvMw==", + "protoFile": "syntax = \"proto3\";\n\npackage library;\n\nmessage Person {\n string id = 1;\n Name name = 2;\n}\n\nmessage Name {\n string given = 1;\n string surname = 2;\n}\n" + } + }, + "name": "protobuf", + "version": "0.3.8" + } + ] + }, + "provider": { + "name": "protobufAsyncMessageProvider" + } +} \ No newline at end of file diff --git a/example/protobuf-async-message/provider/phpunit.xml b/example/protobuf-async-message/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/protobuf-async-message/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/protobuf-async-message/provider/public/index.php b/example/protobuf-async-message/provider/public/index.php new file mode 100644 index 00000000..cea0e98d --- /dev/null +++ b/example/protobuf-async-message/provider/public/index.php @@ -0,0 +1,50 @@ +addBodyParsingMiddleware(); + +$app->post('/', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + if ($body['description'] === 'Person message sent') { + $person = new Person(); + $person->setId('2d5554cd-22da-43ce-8842-2b42cf20661d'); + $name = new Name(); + $name->setGiven('Hettie'); + $name->setSurname('Toy'); + $person->setName($name); + $response->getBody()->write($person->serializeToString()); + + return $response + ->withHeader('Content-Type', 'application/protobuf;message=Person') + ->withHeader('Pact-Message-Metadata', \base64_encode(\json_encode([]))); + } + + $response->getBody()->write('Hello world!'); + + return $response + ->withHeader('Content-Type', 'text/plain') + ; +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + $response->getBody()->write(sprintf('State changed: %s', \json_encode([ + 'action' => $body['action'], + 'state' => $body['state'], + 'params' => $body['params'], + ]))); + + return $response + ->withHeader('Content-Type', 'text/plain') + ; +}); + +$app->run(); diff --git a/example/protobuf-async-message/provider/tests/PactVerifyTest.php b/example/protobuf-async-message/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..d9eff681 --- /dev/null +++ b/example/protobuf-async-message/provider/tests/PactVerifyTest.php @@ -0,0 +1,53 @@ +process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process->start(); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + public function testPactVerifyConsumer(): void + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('protobufAsyncMessageProvider') + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri("http://localhost:7202/pact-change-state")) + ->setStateChangeTeardown(true) + ->setStateChangeAsBody(true) + ; + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json'); + + $this->assertTrue($verifier->verify()); + } +} diff --git a/phpunit.xml b/phpunit.xml index b4e7bf88..eae4f870 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -61,6 +61,12 @@ ./example/protobuf-sync-message/provider/tests + + ./example/protobuf-async-message/consumer/tests + + + ./example/protobuf-async-message/provider/tests + From 12120e2daa8664fd64e497bdddf5ff537d804218 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 16 Jan 2024 20:07:39 +0700 Subject: [PATCH 193/298] refactor: Add PluginFormatter --- .../tests/Service/HttpClientServiceTest.php | 10 +- .../consumer/tests/ProtobufClientTest.php | 9 +- .../Exception/MatchingExpressionException.php | 7 + .../Matcher/Formatters/MinimalFormatter.php | 3 +- .../Matcher/Formatters/PluginFormatter.php | 104 ++++++++++++ .../Formatters/ValueOptionalFormatter.php | 10 +- .../Formatters/ValueRequiredFormatter.php | 5 +- .../Formatters/XmlContentFormatter.php | 7 +- .../Formatters/XmlElementFormatter.php | 6 +- src/PhpPact/Consumer/Matcher/Matcher.php | 158 ++++++++++-------- .../Matcher/Matchers/AbstractDateTime.php | 7 +- .../Matcher/Matchers/AbstractMatcher.php | 11 +- .../Matcher/Matchers/ArrayContains.php | 2 +- .../Consumer/Matcher/Matchers/Boolean.php | 2 +- .../Consumer/Matcher/Matchers/ContentType.php | 2 +- .../Consumer/Matcher/Matchers/Decimal.php | 2 +- .../Consumer/Matcher/Matchers/EachKey.php | 10 +- .../Consumer/Matcher/Matchers/EachValue.php | 10 +- .../Consumer/Matcher/Matchers/Equality.php | 2 +- .../Matchers/GeneratorAwareMatcher.php | 8 +- .../Consumer/Matcher/Matchers/Includes.php | 2 +- .../Consumer/Matcher/Matchers/Integer.php | 2 +- .../Matcher/Matchers/MatchingField.php | 44 +++++ .../Consumer/Matcher/Matchers/MaxType.php | 2 +- .../Consumer/Matcher/Matchers/MinMaxType.php | 2 +- .../Consumer/Matcher/Matchers/MinType.php | 10 +- .../Consumer/Matcher/Matchers/NotEmpty.php | 2 +- .../Consumer/Matcher/Matchers/NullValue.php | 2 +- .../Consumer/Matcher/Matchers/Number.php | 2 +- .../Consumer/Matcher/Matchers/Regex.php | 11 +- .../Consumer/Matcher/Matchers/Semver.php | 2 +- .../Consumer/Matcher/Matchers/StatusCode.php | 2 +- .../Consumer/Matcher/Matchers/StringValue.php | 8 +- .../Consumer/Matcher/Matchers/Type.php | 2 +- .../Consumer/Matcher/Matchers/Values.php | 2 +- .../Matcher/Model/FormatterInterface.php | 4 +- .../Matcher/Model/MatcherInterface.php | 4 +- .../Formatters/PluginFormatterTest.php | 148 ++++++++++++++++ .../Formatters/ValueOptionalFormatterTest.php | 5 +- .../Formatters/ValueRequiredFormatterTest.php | 5 +- .../Formatters/XmlContentFormatterTest.php | 5 +- .../Formatters/XmlElementFormatterTest.php | 8 +- .../PhpPact/Consumer/Matcher/MatcherTest.php | 18 ++ .../Matcher/Matchers/MatchingFieldTest.php | 53 ++++++ 44 files changed, 572 insertions(+), 148 deletions(-) create mode 100644 src/PhpPact/Consumer/Matcher/Exception/MatchingExpressionException.php create mode 100644 src/PhpPact/Consumer/Matcher/Formatters/PluginFormatter.php create mode 100644 src/PhpPact/Consumer/Matcher/Matchers/MatchingField.php create mode 100644 tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php create mode 100644 tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php diff --git a/example/csv/consumer/tests/Service/HttpClientServiceTest.php b/example/csv/consumer/tests/Service/HttpClientServiceTest.php index 57373e75..7f7a135c 100644 --- a/example/csv/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/csv/consumer/tests/Service/HttpClientServiceTest.php @@ -4,6 +4,8 @@ use CsvConsumer\Service\HttpClientService; use PhpPact\Consumer\InteractionBuilder; +use PhpPact\Consumer\Matcher\Formatters\PluginFormatter; +use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; @@ -15,6 +17,8 @@ class HttpClientServiceTest extends TestCase { public function testGetCsvFile(): void { + $matcher = new Matcher(new PluginFormatter()); + $request = new ConsumerRequest(); $request ->setMethod('GET') @@ -28,9 +32,9 @@ public function testGetCsvFile(): void ->setBody(new Text( json_encode([ 'csvHeaders' => false, - 'column:1' => "matching(type,'Name')", - 'column:2' => 'matching(number,100)', - 'column:3' => "matching(datetime, 'yyyy-MM-dd','2000-01-01')", + 'column:1' => $matcher->like('Name'), + 'column:2' => $matcher->number(100), + 'column:3' => $matcher->datetime('yyyy-MM-dd', '2000-01-01'), ]), 'text/csv' )) diff --git a/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php index 55cde3cc..5bf4784c 100644 --- a/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php +++ b/example/protobuf-sync-message/consumer/tests/ProtobufClientTest.php @@ -2,6 +2,8 @@ namespace ProtobufSyncMessageConsumer\Tests; +use PhpPact\Consumer\Matcher\Formatters\PluginFormatter; +use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Plugins\Protobuf\Factory\ProtobufSyncMessageDriverFactory; use PhpPact\Standalone\MockService\MockServerConfig; @@ -15,6 +17,7 @@ class ProtobufClientTest extends TestCase { public function testCalculateArea(): void { + $matcher = new Matcher(new PluginFormatter()); $protoPath = __DIR__ . '/../../library/proto/area_calculator.proto'; $config = new MockServerConfig(); @@ -38,12 +41,12 @@ public function testCalculateArea(): void 'request' => [ 'rectangle' => [ - 'length' => 'matching(number, 3)', - 'width' => 'matching(number, 4)', + 'length' => $matcher->number(3), + 'width' => $matcher->number(4), ], ], 'response' => [ - 'value' => 'matching(number, 12)', + 'value' => $matcher->number(12), ] ]), 'application/grpc' diff --git a/src/PhpPact/Consumer/Matcher/Exception/MatchingExpressionException.php b/src/PhpPact/Consumer/Matcher/Exception/MatchingExpressionException.php new file mode 100644 index 00000000..67f8df51 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Exception/MatchingExpressionException.php @@ -0,0 +1,7 @@ + */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + public function format(MatcherInterface $matcher): array { return [ 'pact:matcher:type' => $matcher->getType(), diff --git a/src/PhpPact/Consumer/Matcher/Formatters/PluginFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/PluginFormatter.php new file mode 100644 index 00000000..4552bacf --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Formatters/PluginFormatter.php @@ -0,0 +1,104 @@ +getGenerator()) { + throw new GeneratorNotRequiredException('Generator is not support in plugin'); + } + + if ($matcher instanceof MatchingField) { + return $this->formatMatchingFieldMatcher($matcher); + } + if ($matcher instanceof NotEmpty) { + return $this->formatNotEmptyMatcher($matcher); + } + if ($matcher instanceof EachKey || $matcher instanceof EachValue) { + return $this->formatEachKeyAndEachValueMatchers($matcher); + } + if ($matcher instanceof NullValue) { + return $this->formatMatchersWithoutConfig(new Type(null)); + } + + if (in_array($matcher->getType(), self::MATCHERS_WITHOUT_CONFIG)) { + return $this->formatMatchersWithoutConfig($matcher); + } + if ($matcher instanceof AbstractDateTime || $matcher instanceof Regex || $matcher instanceof ContentType) { + return $this->formatMatchersWithConfig($matcher); + } + + throw new MatcherNotSupportedException(sprintf("Matcher '%s' is not supported by plugin", $matcher->getType())); + } + + private function formatMatchingFieldMatcher(MatchingField $matcher): string + { + return sprintf("matching($%s)", $this->normalize($matcher->getFieldName())); + } + + private function formatMatchersWithoutConfig(MatcherInterface $matcher): string + { + $type = $matcher->getType() === 'equality' ? 'equalTo' : $matcher->getType(); + + return sprintf('matching(%s, %s)', $type, $this->normalize($matcher->getValue())); + } + + private function formatMatchersWithConfig(AbstractDateTime|Regex|ContentType $matcher): string + { + $config = match (true) { + $matcher instanceof AbstractDateTime => $matcher->getFormat(), + $matcher instanceof Regex => $matcher->getRegex(), + $matcher instanceof ContentType => $matcher->getValue(), + }; + + return sprintf("matching(%s, %s, %s)", $matcher->getType(), $this->normalize($config), $this->normalize($matcher->getValue())); + } + + private function formatNotEmptyMatcher(NotEmpty $matcher): string + { + return sprintf('notEmpty(%s)', $this->normalize($matcher->getValue())); + } + + private function formatEachKeyAndEachValueMatchers(EachKey|EachValue $matcher): string + { + $rules = $matcher->getRules(); + if (count($rules) === 0 || count($rules) > 1) { + throw new MatchingExpressionException(sprintf("Matcher '%s' only support 1 rule, %d provided", $matcher->getType(), count($rules))); + } + $rule = reset($rules); + + return sprintf('%s(%s)', $matcher->getType(), $this->format($rule)); + } + + private function normalize(mixed $value): string + { + return match (gettype($value)) { + 'string' => sprintf("'%s'", str_replace("'", "\\'", $value)), + 'boolean' => $value ? 'true' : 'false', + 'integer' => (string) $value, + 'double' => (string) $value, + 'NULL' => 'null', + default => throw new MatchingExpressionException(sprintf("Plugin formatter doesn't support value of type %s", gettype($value))), + }; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php index 30aea26d..d6a9e199 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php @@ -3,7 +3,7 @@ namespace PhpPact\Consumer\Matcher\Formatters; use PhpPact\Consumer\Matcher\Model\FormatterInterface; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; +use PhpPact\Consumer\Matcher\Model\GeneratorAwareInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; class ValueOptionalFormatter implements FormatterInterface @@ -11,16 +11,18 @@ class ValueOptionalFormatter implements FormatterInterface /** * @return array */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + public function format(MatcherInterface $matcher): array { $data = [ 'pact:matcher:type' => $matcher->getType(), ]; + $attributes = $matcher->getAttributes(); + $generator = $matcher instanceof GeneratorAwareInterface ? $matcher->getGenerator() : null; if ($generator) { - return $data + ['pact:generator:type' => $generator->getType()] + $matcher->getAttributes()->merge($generator->getAttributes())->getData(); + return $data + ['pact:generator:type' => $generator->getType()] + $attributes->merge($generator->getAttributes())->getData(); } - return $data + $matcher->getAttributes()->getData() + ['value' => $value]; + return $data + $attributes->getData() + ['value' => $matcher->getValue()]; } } diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php index a1285a6e..89dad973 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php @@ -2,7 +2,6 @@ namespace PhpPact\Consumer\Matcher\Formatters; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; class ValueRequiredFormatter extends ValueOptionalFormatter @@ -10,8 +9,8 @@ class ValueRequiredFormatter extends ValueOptionalFormatter /** * @return array */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + public function format(MatcherInterface $matcher): array { - return parent::format($matcher, $generator, $value) + ['value' => $value]; + return parent::format($matcher) + ['value' => $matcher->getValue()]; } } diff --git a/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php index f4f39b5a..efbdf0d0 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php @@ -3,7 +3,7 @@ namespace PhpPact\Consumer\Matcher\Formatters; use PhpPact\Consumer\Matcher\Model\Attributes; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; +use PhpPact\Consumer\Matcher\Model\GeneratorAwareInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; class XmlContentFormatter extends ValueOptionalFormatter @@ -11,10 +11,11 @@ class XmlContentFormatter extends ValueOptionalFormatter /** * @return array */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + public function format(MatcherInterface $matcher): array { + $generator = $matcher instanceof GeneratorAwareInterface ? $matcher->getGenerator() : null; $data = [ - 'content' => $value, + 'content' => $matcher->getValue(), 'matcher' => [ 'pact:matcher:type' => $matcher->getType(), ] + $matcher->getAttributes()->merge($generator ? $generator->getAttributes() : new Attributes($matcher))->getData(), diff --git a/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php index 924f7d13..3520b61a 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatter.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Matcher\Formatters; use PhpPact\Consumer\Matcher\Exception\InvalidValueException; -use PhpPact\Consumer\Matcher\Model\GeneratorInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; use PhpPact\Xml\XmlElement; @@ -12,13 +11,14 @@ class XmlElementFormatter extends ValueOptionalFormatter /** * @return array */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array + public function format(MatcherInterface $matcher): array { + $value = $matcher->getValue(); if (!$value instanceof XmlElement) { throw new InvalidValueException('Value must be xml element'); } - $result = parent::format($matcher, $generator, $value); + $result = parent::format($matcher); $examples = $value->getExamples(); if (null !== $examples) { diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index e8ff518c..3bcf2d2c 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -19,6 +19,7 @@ use PhpPact\Consumer\Matcher\Matchers\Equality; use PhpPact\Consumer\Matcher\Matchers\Includes; use PhpPact\Consumer\Matcher\Matchers\Integer; +use PhpPact\Consumer\Matcher\Matchers\MatchingField; use PhpPact\Consumer\Matcher\Matchers\MaxType; use PhpPact\Consumer\Matcher\Matchers\MinMaxType; use PhpPact\Consumer\Matcher\Matchers\MinType; @@ -32,6 +33,8 @@ use PhpPact\Consumer\Matcher\Matchers\Time; use PhpPact\Consumer\Matcher\Matchers\Type; use PhpPact\Consumer\Matcher\Matchers\Values; +use PhpPact\Consumer\Matcher\Model\FormatterAwareInterface; +use PhpPact\Consumer\Matcher\Model\FormatterInterface; use PhpPact\Consumer\Matcher\Model\GeneratorAwareInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; @@ -52,10 +55,14 @@ class Matcher public const IPV6_FORMAT = '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'; public const HEX_FORMAT = '^[0-9a-fA-F]+$'; + public function __construct(private ?FormatterInterface $formatter = null) + { + } + /** * Alias for the `like()` function. */ - public function somethingLike(mixed $value): Type + public function somethingLike(mixed $value): MatcherInterface { return $this->like($value); } @@ -63,15 +70,15 @@ public function somethingLike(mixed $value): Type /** * This executes a type based match against the values, that is, they are equal if they are the same type. */ - public function like(mixed $value): Type + public function like(mixed $value): MatcherInterface { - return new Type($value); + return $this->withFormatter(new Type($value)); } /** * Expect an array of similar data as the value passed in. */ - public function eachLike(mixed $value): MinType + public function eachLike(mixed $value): MatcherInterface { return $this->atLeastLike($value, 1); } @@ -80,14 +87,14 @@ public function eachLike(mixed $value): MinType * @param mixed $value example of what the expected data would be * @param int $min minimum number of objects to verify against */ - public function atLeastLike(mixed $value, int $min): MinType + public function atLeastLike(mixed $value, int $min): MatcherInterface { - return new MinType(array_fill(0, $min, $value), $min); + return $this->withFormatter(new MinType(array_fill(0, $min, $value), $min)); } - public function atMostLike(mixed $value, int $max): MaxType + public function atMostLike(mixed $value, int $max): MatcherInterface { - return new MaxType([$value], $max); + return $this->withFormatter(new MaxType([$value], $max)); } /** @@ -98,7 +105,7 @@ public function atMostLike(mixed $value, int $max): MaxType * * @throws MatcherException */ - public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $count = null): MinMaxType + public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $count = null): MatcherInterface { $elements = $count ?? $min; if ($count !== null) { @@ -115,7 +122,7 @@ public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $cou } } - return new MinMaxType(array_fill(0, $elements, $value), $min, $max); + return $this->withFormatter(new MinMaxType(array_fill(0, $elements, $value), $min, $max)); } /** @@ -125,9 +132,9 @@ public function constrainedArrayLike(mixed $value, int $min, int $max, ?int $cou * * @throws MatcherException */ - public function term(string|array|null $values, string $pattern): Regex + public function term(string|array|null $values, string $pattern): MatcherInterface { - return new Regex($pattern, $values); + return $this->withFormatter(new Regex($pattern, $values)); } /** @@ -137,7 +144,7 @@ public function term(string|array|null $values, string $pattern): Regex * * @throws MatcherException */ - public function regex(string|array|null $values, string $pattern): Regex + public function regex(string|array|null $values, string $pattern): MatcherInterface { return $this->term($values, $pattern); } @@ -149,7 +156,7 @@ public function regex(string|array|null $values, string $pattern): Regex * * @throws MatcherException */ - public function dateISO8601(string $value = '2013-02-01'): Regex + public function dateISO8601(string $value = '2013-02-01'): MatcherInterface { return $this->term($value, self::ISO8601_DATE_FORMAT); } @@ -161,7 +168,7 @@ public function dateISO8601(string $value = '2013-02-01'): Regex * * @throws MatcherException */ - public function timeISO8601(string $value = 'T22:44:30.652Z'): Regex + public function timeISO8601(string $value = 'T22:44:30.652Z'): MatcherInterface { return $this->term($value, self::ISO8601_TIME_FORMAT); } @@ -173,7 +180,7 @@ public function timeISO8601(string $value = 'T22:44:30.652Z'): Regex * * @throws MatcherException */ - public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): Regex + public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): MatcherInterface { return $this->term($value, self::ISO8601_DATETIME_FORMAT); } @@ -185,7 +192,7 @@ public function dateTimeISO8601(string $value = '2015-08-06T16:53:10+01:00'): Re * * @throws MatcherException */ - public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.123+01:00'): Regex + public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.123+01:00'): MatcherInterface { return $this->term($value, self::ISO8601_DATETIME_WITH_MILLIS_FORMAT); } @@ -197,45 +204,45 @@ public function dateTimeWithMillisISO8601(string $value = '2015-08-06T16:53:10.1 * * @throws MatcherException */ - public function timestampRFC3339(string $value = 'Mon, 31 Oct 2016 15:21:41 -0400'): Regex + public function timestampRFC3339(string $value = 'Mon, 31 Oct 2016 15:21:41 -0400'): MatcherInterface { return $this->term($value, self::RFC3339_TIMESTAMP_FORMAT); } - public function boolean(): Type + public function boolean(): MatcherInterface { return $this->like(true); } - public function integer(int $int = 13): Type + public function integer(int $int = 13): MatcherInterface { return $this->like($int); } - public function decimal(float $float = 13.01): Type + public function decimal(float $float = 13.01): MatcherInterface { return $this->like($float); } - public function booleanV3(?bool $value = null): Boolean + public function booleanV3(?bool $value = null): MatcherInterface { - return new Boolean($value); + return $this->withFormatter(new Boolean($value)); } - public function integerV3(?int $value = null): Integer + public function integerV3(?int $value = null): MatcherInterface { - return new Integer($value); + return $this->withFormatter(new Integer($value)); } - public function decimalV3(?float $value = null): Decimal + public function decimalV3(?float $value = null): MatcherInterface { - return new Decimal($value); + return $this->withFormatter(new Decimal($value)); } /** * @throws MatcherException */ - public function hexadecimal(?string $value = null): Regex + public function hexadecimal(?string $value = null): MatcherInterface { $matcher = new Regex(self::HEX_FORMAT, $value); @@ -243,13 +250,13 @@ public function hexadecimal(?string $value = null): Regex $matcher->setGenerator(new RandomHexadecimal()); } - return $matcher; + return $this->withFormatter($matcher); } /** * @throws MatcherException */ - public function uuid(?string $value = null): Regex + public function uuid(?string $value = null): MatcherInterface { $matcher = new Regex(self::UUID_V4_FORMAT, $value); @@ -257,20 +264,20 @@ public function uuid(?string $value = null): Regex $matcher->setGenerator(new Uuid()); } - return $matcher; + return $this->withFormatter($matcher); } - public function ipv4Address(?string $ip = '127.0.0.13'): Regex + public function ipv4Address(?string $ip = '127.0.0.13'): MatcherInterface { return $this->term($ip, self::IPV4_FORMAT); } - public function ipv6Address(?string $ip = '::ffff:192.0.2.128'): Regex + public function ipv6Address(?string $ip = '::ffff:192.0.2.128'): MatcherInterface { return $this->term($ip, self::IPV6_FORMAT); } - public function email(?string $email = 'hello@pact.io'): Regex + public function email(?string $email = 'hello@pact.io'): MatcherInterface { return $this->term($email, self::EMAIL_FORMAT); } @@ -279,9 +286,9 @@ public function email(?string $email = 'hello@pact.io'): Regex * Value that must be null. This will only match the JSON Null value. For other content types, it will * match if the attribute is missing. */ - public function nullValue(): NullValue + public function nullValue(): MatcherInterface { - return new NullValue(); + return $this->withFormatter(new NullValue()); } /** @@ -291,9 +298,9 @@ public function nullValue(): NullValue * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function date(string $format = 'yyyy-MM-dd', ?string $value = null): Date + public function date(string $format = 'yyyy-MM-dd', ?string $value = null): MatcherInterface { - return new Date($format, $value); + return $this->withFormatter(new Date($format, $value)); } /** @@ -303,9 +310,9 @@ public function date(string $format = 'yyyy-MM-dd', ?string $value = null): Date * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function time(string $format = 'HH:mm:ss', ?string $value = null): Time + public function time(string $format = 'HH:mm:ss', ?string $value = null): MatcherInterface { - return new Time($format, $value); + return $this->withFormatter(new Time($format, $value)); } /** @@ -315,14 +322,14 @@ public function time(string $format = 'HH:mm:ss', ?string $value = null): Time * For Java one, see https://www.digitalocean.com/community/tutorials/java-simpledateformat-java-date-format#patterns * For PHP one, see https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters */ - public function datetime(string $format = "yyyy-MM-dd'T'HH:mm:ss", ?string $value = null): DateTime + public function datetime(string $format = "yyyy-MM-dd'T'HH:mm:ss", ?string $value = null): MatcherInterface { - return new DateTime($format, $value); + return $this->withFormatter(new DateTime($format, $value)); } - public function string(?string $value = null): StringValue + public function string(?string $value = null): MatcherInterface { - return new StringValue($value); + return $this->withFormatter(new StringValue($value)); } /** @@ -344,17 +351,17 @@ public function fromProviderState(MatcherInterface $matcher, string $expression) /** * Value that must be equal to the example. This is mainly used to reset the matching rules which cascade. */ - public function equal(mixed $value): Equality + public function equal(mixed $value): MatcherInterface { - return new Equality($value); + return $this->withFormatter(new Equality($value)); } /** * Value that must include the example value as a substring. */ - public function includes(string $value): Includes + public function includes(string $value): MatcherInterface { - return new Includes($value); + return $this->withFormatter(new Includes($value)); } /** @@ -362,9 +369,9 @@ public function includes(string $value): Includes * * @param int|float|null $value Example value. If omitted a random integer value will be generated. */ - public function number(int|float|null $value = null): Number + public function number(int|float|null $value = null): MatcherInterface { - return new Number($value); + return $this->withFormatter(new Number($value)); } /** @@ -373,33 +380,33 @@ public function number(int|float|null $value = null): Number * * @param array $variants */ - public function arrayContaining(array $variants): ArrayContains + public function arrayContaining(array $variants): MatcherInterface { - return new ArrayContains($variants); + return $this->withFormatter(new ArrayContains($variants)); } /** * Value must be present and not empty (not null or the empty string or empty array or empty object) */ - public function notEmpty(mixed $value): NotEmpty + public function notEmpty(mixed $value): MatcherInterface { - return new NotEmpty($value); + return $this->withFormatter(new NotEmpty($value)); } /** * Value must be valid based on the semver specification */ - public function semver(string $value): Semver + public function semver(string $value): MatcherInterface { - return new Semver($value); + return $this->withFormatter(new Semver($value)); } /** * Matches the response status code. */ - public function statusCode(string $status, ?int $value = null): StatusCode + public function statusCode(string $status, ?int $value = null): MatcherInterface { - return new StatusCode($status, $value); + return $this->withFormatter(new StatusCode($status, $value)); } /** @@ -407,17 +414,17 @@ public function statusCode(string $status, ?int $value = null): StatusCode * * @param array $values */ - public function values(array $values): Values + public function values(array $values): MatcherInterface { - return new Values($values); + return $this->withFormatter(new Values($values)); } /** * Match binary data by its content type (magic file check) */ - public function contentType(string $contentType): ContentType + public function contentType(string $contentType): MatcherInterface { - return new ContentType($contentType); + return $this->withFormatter(new ContentType($contentType)); } /** @@ -426,9 +433,9 @@ public function contentType(string $contentType): ContentType * @param array $values * @param array $rules */ - public function eachKey(array $values, array $rules): EachKey + public function eachKey(array $values, array $rules): MatcherInterface { - return new EachKey($values, $rules); + return $this->withFormatter(new EachKey($values, $rules)); } /** @@ -437,15 +444,15 @@ public function eachKey(array $values, array $rules): EachKey * @param array $values * @param array $rules */ - public function eachValue(array $values, array $rules): EachValue + public function eachValue(array $values, array $rules): MatcherInterface { - return new EachValue($values, $rules); + return $this->withFormatter(new EachValue($values, $rules)); } /** * @throws MatcherException */ - public function url(string $url, string $regex, bool $useMockServerBasePath = true): Regex + public function url(string $url, string $regex, bool $useMockServerBasePath = true): MatcherInterface { $matcher = new Regex($regex, $useMockServerBasePath ? null : $url); @@ -453,6 +460,23 @@ public function url(string $url, string $regex, bool $useMockServerBasePath = tr $matcher->setGenerator(new MockServerURL($regex, $url)); } + return $this->withFormatter($matcher); + } + + /** + * Generates a value that is looked up from the provider state context using the given expression + */ + public function matchingField(string $fieldName): MatcherInterface + { + return $this->withFormatter(new MatchingField($fieldName)); + } + + private function withFormatter(MatcherInterface $matcher): MatcherInterface + { + if ($matcher instanceof FormatterAwareInterface && $this->formatter) { + $matcher->setFormatter($this->formatter); + } + return $matcher; } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php index c11dbad9..65ea9f50 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/AbstractDateTime.php @@ -9,6 +9,11 @@ public function __construct(protected string $format, private ?string $value = n parent::__construct(); } + public function getFormat(): string + { + return $this->format; + } + /** * @return array */ @@ -17,7 +22,7 @@ protected function getAttributesData(): array return ['format' => $this->format]; } - protected function getValue(): ?string + public function getValue(): ?string { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php b/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php index 021ae779..5326bc33 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/AbstractMatcher.php @@ -4,11 +4,10 @@ use PhpPact\Consumer\Matcher\Formatters\ValueOptionalFormatter; use PhpPact\Consumer\Matcher\Model\Attributes; -use PhpPact\Consumer\Matcher\Model\FormatterAwareInterface; use PhpPact\Consumer\Matcher\Model\FormatterInterface; use PhpPact\Consumer\Matcher\Model\MatcherInterface; -abstract class AbstractMatcher implements MatcherInterface, FormatterAwareInterface +abstract class AbstractMatcher implements MatcherInterface { private FormatterInterface $formatter; @@ -28,11 +27,11 @@ public function getFormatter(): FormatterInterface } /** - * @return array + * @return string|array */ - public function jsonSerialize(): array + public function jsonSerialize(): string|array { - return $this->getFormatter()->format($this, null, $this->getValue()); + return $this->getFormatter()->format($this); } public function getAttributes(): Attributes @@ -44,6 +43,4 @@ public function getAttributes(): Attributes * @return array */ abstract protected function getAttributesData(): array; - - abstract protected function getValue(): mixed; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php index f5f04df2..418d4005 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/ArrayContains.php @@ -28,7 +28,7 @@ protected function getAttributesData(): array /** * @todo Change return type to `null` */ - protected function getValue(): mixed + public function getValue(): mixed { return null; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php index 08e3e398..96f2a59b 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Boolean.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): ?bool + public function getValue(): ?bool { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php index a8d8635d..340a6875 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/ContentType.php @@ -17,7 +17,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): string + public function getValue(): string { return $this->contentType; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php index de994b2b..f1f00f82 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Decimal.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): ?float + public function getValue(): ?float { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php index 8a941234..028e1e8a 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachKey.php @@ -29,7 +29,7 @@ protected function getAttributesData(): array /** * @return array|object */ - protected function getValue(): object|array + public function getValue(): object|array { return $this->value; } @@ -38,4 +38,12 @@ public function getType(): string { return 'eachKey'; } + + /** + * @return MatcherInterface[] + */ + public function getRules(): array + { + return $this->rules; + } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php index 3b0c8bbd..08aecb8a 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/EachValue.php @@ -29,7 +29,7 @@ protected function getAttributesData(): array /** * @return array|object */ - protected function getValue(): object|array + public function getValue(): object|array { return $this->value; } @@ -38,4 +38,12 @@ public function getType(): string { return 'eachValue'; } + + /** + * @return MatcherInterface[] + */ + public function getRules(): array + { + return $this->rules; + } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php index e6d12c42..f0e1cf8b 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Equality.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Equality.php @@ -23,7 +23,7 @@ protected function getAttributesData(): array /** * @return object|array|string|float|int|bool|null */ - protected function getValue(): object|array|string|float|int|bool|null + public function getValue(): object|array|string|float|int|bool|null { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php b/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php index e3cd0174..41301f30 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcher.php @@ -22,20 +22,18 @@ public function getGenerator(): ?GeneratorInterface } /** - * @return array + * @return string|array */ - public function jsonSerialize(): array + public function jsonSerialize(): string|array { if (null === $this->getValue()) { if (!$this->generator) { throw new GeneratorRequiredException(sprintf("Generator is required for matcher '%s' when example value is not set", $this->getType())); } - - return $this->getFormatter()->format($this, $this->generator, null); } elseif ($this->generator) { throw new GeneratorNotRequiredException(sprintf("Generator '%s' is not required for matcher '%s' when example value is set", $this->generator->getType(), $this->getType())); } - return $this->getFormatter()->format($this, $this->generator, $this->getValue()); + return $this->getFormatter()->format($this); } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Includes.php b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php index 094d94a7..803d2682 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Includes.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Includes.php @@ -17,7 +17,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): string + public function getValue(): string { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Integer.php b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php index 3919854c..9fe9a8ff 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Integer.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Integer.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): ?int + public function getValue(): ?int { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MatchingField.php b/src/PhpPact/Consumer/Matcher/Matchers/MatchingField.php new file mode 100644 index 00000000..e2c11833 --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/Matchers/MatchingField.php @@ -0,0 +1,44 @@ +fieldName; + } + + public function getValue(): mixed + { + throw new MatcherNotSupportedException(self::MATCHER_NOT_SUPPORTED_EXCEPTION_MESSAGE); + } + + public function getType(): string + { + throw new MatcherNotSupportedException(self::MATCHER_NOT_SUPPORTED_EXCEPTION_MESSAGE); + } + + protected function getAttributesData(): array + { + throw new MatcherNotSupportedException(self::MATCHER_NOT_SUPPORTED_EXCEPTION_MESSAGE); + } + + public function jsonSerialize(): string + { + $result = parent::jsonSerialize(); + if (is_array($result)) { + throw new MatcherNotSupportedException(self::MATCHER_NOT_SUPPORTED_EXCEPTION_MESSAGE); + } + + return $result; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php index 50a925dc..dede0193 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MaxType.php @@ -29,7 +29,7 @@ protected function getAttributesData(): array /** * @return array */ - protected function getValue(): array + public function getValue(): array { return array_values($this->values); } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php index f87fb753..10a29238 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinMaxType.php @@ -33,7 +33,7 @@ protected function getAttributesData(): array /** * @return array */ - protected function getValue(): array + public function getValue(): array { return array_values($this->values); } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/MinType.php b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php index 4aabf493..4f709ad6 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/MinType.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/MinType.php @@ -18,14 +18,6 @@ public function __construct( parent::__construct(); } - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->getFormatter()->format($this, null, array_values($this->values)); - } - public function getType(): string { return 'type'; @@ -42,7 +34,7 @@ protected function getAttributesData(): array /** * @return array */ - protected function getValue(): array + public function getValue(): array { return array_values($this->values); } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php index 85aaf0eb..f044a5a7 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/NotEmpty.php @@ -23,7 +23,7 @@ protected function getAttributesData(): array /** * @return object|array|string|float|int|bool */ - protected function getValue(): object|array|string|float|int|bool + public function getValue(): object|array|string|float|int|bool { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php index 2aa4cf8d..82715007 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/NullValue.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array /** * @todo Change return type to `null` */ - protected function getValue(): mixed + public function getValue(): mixed { return null; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Number.php b/src/PhpPact/Consumer/Matcher/Matchers/Number.php index 9f45fb4f..83cac53b 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Number.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Number.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): int|float|null + public function getValue(): int|float|null { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Regex.php b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php index d16582ee..d7822e9a 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Regex.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Regex.php @@ -24,9 +24,9 @@ public function __construct( } /** - * @return array + * @return string|array */ - public function jsonSerialize(): array + public function jsonSerialize(): string|array { if (null !== $this->values) { $this->validateRegex(); @@ -56,7 +56,7 @@ public function getType(): string /** * @return string|string[]|null */ - protected function getValue(): string|array|null + public function getValue(): string|array|null { return $this->values; } @@ -68,4 +68,9 @@ protected function getAttributesData(): array { return ['regex' => $this->regex]; } + + public function getRegex(): string + { + return $this->regex; + } } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Semver.php b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php index 231c7f21..03521ce2 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Semver.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Semver.php @@ -27,7 +27,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): ?string + public function getValue(): ?string { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php b/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php index e1132733..15db5716 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/StatusCode.php @@ -47,7 +47,7 @@ protected function getAttributesData(): array return ['status' => $this->status]; } - protected function getValue(): ?int + public function getValue(): ?int { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php index da0fcc15..f0267bcf 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/StringValue.php @@ -26,11 +26,11 @@ public function getType(): string } /** - * @return array + * @return string|array */ - public function jsonSerialize(): array + public function jsonSerialize(): string|array { - return $this->getFormatter()->format($this, $this->getGenerator(), $this->getValue()); + return $this->getFormatter()->format($this); } protected function getAttributesData(): array @@ -38,7 +38,7 @@ protected function getAttributesData(): array return []; } - protected function getValue(): string + public function getValue(): string { return $this->value ?? self::DEFAULT_VALUE; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Type.php b/src/PhpPact/Consumer/Matcher/Matchers/Type.php index b8595330..46b9b51c 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Type.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Type.php @@ -23,7 +23,7 @@ protected function getAttributesData(): array /** * @return object|array|string|float|int|bool|null */ - protected function getValue(): object|array|string|float|int|bool|null + public function getValue(): object|array|string|float|int|bool|null { return $this->value; } diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Values.php b/src/PhpPact/Consumer/Matcher/Matchers/Values.php index 11feaf12..cfb6f5ec 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Values.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Values.php @@ -23,7 +23,7 @@ protected function getAttributesData(): array /** * @return array */ - protected function getValue(): array + public function getValue(): array { return $this->values; } diff --git a/src/PhpPact/Consumer/Matcher/Model/FormatterInterface.php b/src/PhpPact/Consumer/Matcher/Model/FormatterInterface.php index 4b39acc0..6eb47411 100644 --- a/src/PhpPact/Consumer/Matcher/Model/FormatterInterface.php +++ b/src/PhpPact/Consumer/Matcher/Model/FormatterInterface.php @@ -5,7 +5,7 @@ interface FormatterInterface { /** - * @return array + * @return string|array */ - public function format(MatcherInterface $matcher, ?GeneratorInterface $generator, mixed $value): array; + public function format(MatcherInterface $matcher): string|array; } diff --git a/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php index 1e4fb48f..f5a7672c 100644 --- a/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php +++ b/src/PhpPact/Consumer/Matcher/Model/MatcherInterface.php @@ -4,9 +4,11 @@ use JsonSerializable; -interface MatcherInterface extends JsonSerializable +interface MatcherInterface extends JsonSerializable, FormatterAwareInterface { public function getType(): string; public function getAttributes(): Attributes; + + public function getValue(): mixed; } diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php new file mode 100644 index 00000000..c9bf7723 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php @@ -0,0 +1,148 @@ +formatter = new PluginFormatter(); + } + + public function testFormatWithGenerator(): void + { + $this->expectException(GeneratorNotRequiredException::class); + $this->expectExceptionMessage('Generator is not support in plugin'); + $matcher = new StringValue('example value'); + $matcher->setGenerator(new RandomString()); + $this->formatter->format($matcher); + } + + /** + * @dataProvider invalidRulesProvider + */ + public function testInvalidRules(EachKey|EachValue $matcher): void + { + $this->expectException(MatchingExpressionException::class); + $this->expectExceptionMessage(sprintf("Matcher '%s' only support 1 rule, %d provided", $matcher->getType(), count($matcher->getRules()))); + $this->formatter->format($matcher); + } + + public function invalidRulesProvider(): array + { + return [ + [new EachKey(["doesn't matter"], [])], + [new EachValue(["doesn't matter"], [])], + [new EachKey(["doesn't matter"], [new Type(1), new Type(2)])], + [new EachValue(["doesn't matter"], [new Type(1), new Type(2), new Type(3)])], + ]; + } + + /** + * @dataProvider invalidValueProvider + */ + public function testInvalidValue(MatcherInterface $matcher, string $type): void + { + $this->expectException(MatchingExpressionException::class); + $this->expectExceptionMessage(sprintf("Plugin formatter doesn't support value of type %s", $type)); + $this->formatter->format($matcher); + } + + public function invalidValueProvider(): array + { + return [ + [new Type((object)['key' => 'value']), 'object'], + [new Type(['key' => 'value']), 'array'], + [new MinType(['Example value'], 1), 'array'], + [new MaxType(['Example value'], 2), 'array'], + [new MinMaxType(['Example value'], 1, 2), 'array'], + ]; + } + + /** + * @dataProvider notSupportedMatcherProvider + */ + public function testNotSupportedMatcher(MatcherInterface $matcher): void + { + $this->expectException(MatcherNotSupportedException::class); + $this->expectExceptionMessage(sprintf("Matcher '%s' is not supported by plugin", $matcher->getType())); + $this->formatter->format($matcher); + } + + public function notSupportedMatcherProvider(): array + { + return [ + [new Values([1, 2, 3])], + [new ArrayContains([new Equality(1)])], + [new StatusCode('clientError', 405)], + ]; + } + + /** + * @dataProvider matcherProvider + */ + public function testFormat(MatcherInterface $matcher, string $json): void + { + $this->assertSame($json, json_encode($this->formatter->format($matcher))); + } + + public function matcherProvider(): array + { + return [ + [new MatchingField('product'), '"matching($\'product\')"'], + [new NotEmpty('test'), '"notEmpty(\'test\')"'], + [new EachKey(["doesn't matter"], [new Regex('\$(\.\w+)+', '$.test.one')]), '"eachKey(matching(regex, \'\\\\$(\\\\.\\\\w+)+\', \'$.test.one\'))"'], + [new EachValue(["doesn't matter"], [new Type(100)]), '"eachValue(matching(type, 100))"'], + [new Equality('Example value'), '"matching(equalTo, \'Example value\')"'], + [new Type('Example value'), '"matching(type, \'Example value\')"'], + [new Number(100.09), '"matching(number, 100.09)"'], + [new Integer(100), '"matching(integer, 100)"'], + [new Decimal(100.01), '"matching(decimal, 100.01)"'], + [new Includes('testing'), '"matching(include, \'testing\')"'], + [new Boolean(true), '"matching(boolean, true)"'], + [new Semver('1.0.0'), '"matching(semver, \'1.0.0\')"'], + [new DateTime('yyyy-MM-dd HH:mm:ssZZZZZ', '2020-05-21 16:44:32+10:00'), '"matching(datetime, \'yyyy-MM-dd HH:mm:ssZZZZZ\', \'2020-05-21 16:44:32+10:00\')"'], + [new Date('yyyy-MM-dd', '2012-04-12'), '"matching(date, \'yyyy-MM-dd\', \'2012-04-12\')"'], + [new Time('HH:mm', '22:04'), '"matching(time, \'HH:mm\', \'22:04\')"'], + [new Regex('\\w{3}\\d+', 'abc123'), '"matching(regex, \'\\\\w{3}\\\\d+\', \'abc123\')"'], + [new ContentType('application/xml'), '"matching(contentType, \'application\/xml\', \'application\/xml\')"'], + [new NullValue(), '"matching(type, null)"'], + ]; + } +} diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php index bb1eaefc..dd3d6861 100644 --- a/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php +++ b/tests/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatterTest.php @@ -17,9 +17,10 @@ class ValueOptionalFormatterTest extends TestCase */ public function testFormat(bool $hasGenerator, ?string $value, array $result): void { - $matcher = new Date('yyyy-MM-dd', 5); + $matcher = new Date('yyyy-MM-dd', $value); $generator = $hasGenerator ? new RandomString(10) : null; + $matcher->setGenerator($generator); $formatter = new ValueOptionalFormatter(); - $this->assertSame($result, $formatter->format($matcher, $generator, $value)); + $this->assertSame($result, $formatter->format($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php index c8dbd387..3e092bc3 100644 --- a/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php +++ b/tests/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatterTest.php @@ -17,9 +17,10 @@ class ValueRequiredFormatterTest extends TestCase */ public function testFormat(bool $hasGenerator, ?string $value, array $result): void { - $matcher = new Date('yyyy-MM-dd', 5); + $matcher = new Date('yyyy-MM-dd', $value); $generator = $hasGenerator ? new RandomString(10) : null; + $matcher->setGenerator($generator); $formatter = new ValueRequiredFormatter(); - $this->assertSame($result, $formatter->format($matcher, $generator, $value)); + $this->assertSame($result, $formatter->format($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php index 1caaacfb..e2a10f67 100644 --- a/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php +++ b/tests/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatterTest.php @@ -15,9 +15,10 @@ class XmlContentFormatterTest extends TestCase */ public function testFormat(bool $hasGenerator, ?string $value, array $result): void { - $matcher = new Date('yyyy-MM-dd', 5); + $matcher = new Date('yyyy-MM-dd', $value); $generator = $hasGenerator ? new RandomString(10) : null; + $matcher->setGenerator($generator); $formatter = new XmlContentFormatter(); - $this->assertSame($result, $formatter->format($matcher, $generator, $value)); + $this->assertSame($result, $formatter->format($matcher)); } } diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php index b3657d3f..543862fb 100644 --- a/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php +++ b/tests/PhpPact/Consumer/Matcher/Formatters/XmlElementFormatterTest.php @@ -3,10 +3,8 @@ namespace PhpPactTest\Consumer\Matcher\Formatters; use PhpPact\Consumer\Matcher\Exception\InvalidValueException; -use PhpPact\Consumer\Matcher\Formatters\XmlContentFormatter; use PhpPact\Consumer\Matcher\Formatters\XmlElementFormatter; use PhpPact\Consumer\Matcher\Generators\RandomString; -use PhpPact\Consumer\Matcher\Matchers\Date; use PhpPact\Consumer\Matcher\Matchers\StringValue; use PhpPact\Consumer\Matcher\Matchers\Type; use PhpPact\Xml\XmlElement; @@ -18,8 +16,10 @@ public function testFormatInvalidValue(): void { $this->expectException(InvalidValueException::class); $this->expectExceptionMessage('Value must be xml element'); + $matcher = new StringValue('example value'); + $matcher->setGenerator(new RandomString()); $formatter = new XmlElementFormatter(); - $formatter->format(new StringValue(), new RandomString(), 'example value'); + $formatter->format($matcher); } /** @@ -34,6 +34,6 @@ public function testFormat(?int $examples, array $result): void ); $matcher = new Type($value); $formatter = new XmlElementFormatter(); - $this->assertSame(json_encode($result), json_encode($formatter->format($matcher, null, $value))); + $this->assertSame(json_encode($result), json_encode($formatter->format($matcher))); } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 76d6a370..5d79515c 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -4,6 +4,8 @@ use PhpPact\Consumer\Matcher\Exception\MatcherException; use PhpPact\Consumer\Matcher\Exception\MatcherNotSupportedException; +use PhpPact\Consumer\Matcher\Formatters\MinimalFormatter; +use PhpPact\Consumer\Matcher\Formatters\ValueOptionalFormatter; use PhpPact\Consumer\Matcher\Generators\MockServerURL; use PhpPact\Consumer\Matcher\Generators\ProviderState; use PhpPact\Consumer\Matcher\Generators\RandomHexadecimal; @@ -21,6 +23,7 @@ use PhpPact\Consumer\Matcher\Matchers\Equality; use PhpPact\Consumer\Matcher\Matchers\Includes; use PhpPact\Consumer\Matcher\Matchers\Integer; +use PhpPact\Consumer\Matcher\Matchers\MatchingField; use PhpPact\Consumer\Matcher\Matchers\MaxType; use PhpPact\Consumer\Matcher\Matchers\MinMaxType; use PhpPact\Consumer\Matcher\Matchers\MinType; @@ -298,6 +301,7 @@ public function testFromProviderStateMatcherNotSupport(): void public function testFromProviderState(): void { $uuid = $this->matcher->uuid(); + $this->assertInstanceOf(Regex::class, $uuid); $this->assertSame(Uuid::class, get_class($uuid->getGenerator())); $this->assertSame($uuid, $this->matcher->fromProviderState($uuid, '${id}')); $this->assertSame(ProviderState::class, get_class($uuid->getGenerator())); @@ -393,4 +397,18 @@ public function testUrl(bool $useMockServerBasePath, bool $hasGenerator): void $this->assertNull($url->getGenerator()); } } + + public function testMatchingField(): void + { + $this->assertInstanceOf(MatchingField::class, $this->matcher->matchingField('address')); + } + + public function testWithFormatter(): void + { + $uuid = $this->matcher->uuid(); + $this->assertInstanceOf(ValueOptionalFormatter::class, $uuid->getFormatter()); + $matcher = new Matcher($formatter = new MinimalFormatter()); + $uuid = $matcher->uuid(); + $this->assertSame($formatter, $uuid->getFormatter()); + } } diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php new file mode 100644 index 00000000..88c1c422 --- /dev/null +++ b/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php @@ -0,0 +1,53 @@ +setFormatter(new PluginFormatter()); + $this->assertSame( + $json, + json_encode($matcher) + ); + } + + /** + * @dataProvider formatterProvider + */ + public function testNotSupportedFormatter(string $formatterClassName): void + { + $this->expectException(MatcherNotSupportedException::class); + $this->expectExceptionMessage('MatchingField matcher only work with plugin'); + $matcher = new MatchingField('person'); + $matcher->setFormatter(new $formatterClassName()); + json_encode($matcher); + } + + public function formatterProvider(): array + { + return [ + [MinimalFormatter::class], + [ValueOptionalFormatter::class], + [ValueRequiredFormatter::class], + [XmlContentFormatter::class], + [XmlElementFormatter::class], + ]; + } +} From cb83c2e8007a3b755665814dd8fc6cf25fccaed0 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 17 Jan 2024 22:19:00 +0700 Subject: [PATCH 194/298] deps: Update ffi library to 0.4.13 --- composer.json | 4 ++-- .../pacts/matchersConsumer-matchersProvider.json | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index c83557c9..5f85541f 100644 --- a/composer.json +++ b/composer.json @@ -103,12 +103,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.11", + "version": "0.4.13", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.11", + "version": "0.4.13", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index a9f8dce8..8cd325d0 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -29,21 +29,21 @@ ] }, "query": { - "$.pages": { + "locales[]": { "combine": "AND", "matchers": [ { "match": "regex", - "regex": "\\d+" + "regex": "^[a-z]{2}-[A-Z]{2}$" } ] }, - "$['locales[]']": { + "pages": { "combine": "AND", "matchers": [ { "match": "regex", - "regex": "^[a-z]{2}-[A-Z]{2}$" + "regex": "\\d+" } ] } @@ -591,9 +591,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "4.0" From d08322d39215be1056296853bf0dfb8a67d8c456 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 19 Jan 2024 13:52:10 +0700 Subject: [PATCH 195/298] test(compatibility-suite): Fix error 'Matching rule JSON [] is not correctly formed' --- compatibility-suite/tests/Context/V3/Http/ProviderContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compatibility-suite/tests/Context/V3/Http/ProviderContext.php b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php index 85d6e460..4dfad0ae 100644 --- a/compatibility-suite/tests/Context/V3/Http/ProviderContext.php +++ b/compatibility-suite/tests/Context/V3/Http/ProviderContext.php @@ -28,9 +28,9 @@ public function __construct( public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingProviderStatesDefined(int $id, TableNode $table): void { $this->pactWriter->write($id, $this->pactPath); - $pact = json_decode(file_get_contents($this->pactPath), true); + $pact = json_decode(file_get_contents($this->pactPath)); $rows = $table->getHash(); - $pact['interactions'][0]['providerStates'] = array_map(fn (array $row): array => ['name' => $row['State Name'], 'params' => json_decode($row['Parameters'] ?? '{}', true)], $rows); + $pact->interactions[0]->providerStates = array_map(fn (array $row): array => ['name' => $row['State Name'], 'params' => json_decode($row['Parameters'] ?? '{}', true)], $rows); file_put_contents($this->pactPath, json_encode($pact)); $this->providerVerifier->addSource($this->pactPath); } From 59154e11d1ac6be298e51fdf16a38bd0d0abcff4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 23 Jan 2024 17:02:47 +0700 Subject: [PATCH 196/298] ci: Reduce test running time by setting PACT_DO_NOT_TRACK --- .cirrus.yml | 2 ++ .github/workflows/build.yml | 2 ++ .github/workflows/compatibility-suite.yml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 508d4ef9..16bafdd6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -22,6 +22,7 @@ BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE linux_arm64_task: env: COMPOSER_ALLOW_SUPERUSER: 1 + PACT_DO_NOT_TRACK: true matrix: - VERSION: 8.2 - VERSION: 8.1 @@ -37,6 +38,7 @@ linux_arm64_task: macos_arm64_task: # https://www.markhesketh.com/switching-multiple-php-versions-on-macos/ env: + PACT_DO_NOT_TRACK: true matrix: - VERSION: 8.2 - VERSION: 8.1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 599bf339..a36eb477 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,3 +90,5 @@ jobs: - name: Composer test run: composer test + env: + PACT_DO_NOT_TRACK: true diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index 8cc1ee7d..efcb7cd8 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -3,7 +3,7 @@ name: Pact-PHP Compatibility Suite on: [push, pull_request] env: - pact_do_not_track: true + PACT_DO_NOT_TRACK: true jobs: v1: From ecbc3bf9f066bac1b6ef979d2144b336443421d0 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 23 Jan 2024 23:03:42 +0700 Subject: [PATCH 197/298] chore: Reuse process handling code --- .../tests/Service/GeneratorServer.php | 13 ++---- .../tests/Service/ProviderStateServer.php | 17 +++----- composer.json | 1 + .../binary/provider/tests/PactVerifyTest.php | 14 ++----- example/csv/provider/tests/PactVerifyTest.php | 16 +++----- .../provider/tests/PactVerifyTest.php | 10 ++--- .../json/provider/tests/PactVerifyTest.php | 16 +++----- .../provider/tests/PactVerifyTest.php | 10 ++--- .../message/provider/tests/PactVerifyTest.php | 18 +++----- .../provider/tests/PactVerifyTest.php | 14 ++----- .../provider/tests/PactVerifyTest.php | 16 +++----- .../provider/tests/PactVerifyTest.php | 16 +++----- .../provider/tests/RoadRunnerProcess.php | 22 ++++++++++ example/xml/provider/tests/PactVerifyTest.php | 16 +++----- ...roviderProcess.php => AbstractProcess.php} | 20 ++++++--- .../Helper/Exception/HelperException.php | 9 ++++ .../Exception/NoPortAvailableException.php | 7 ++++ tests/PhpPact/Helper/PhpProcess.php | 41 +++++++++++++++++++ .../ProviderVerifier/VerifierTest.php | 14 ++----- 19 files changed, 157 insertions(+), 133 deletions(-) create mode 100644 example/protobuf-sync-message/provider/tests/RoadRunnerProcess.php rename tests/PhpPact/Helper/{ProviderProcess.php => AbstractProcess.php} (51%) create mode 100644 tests/PhpPact/Helper/Exception/HelperException.php create mode 100644 tests/PhpPact/Helper/Exception/NoPortAvailableException.php create mode 100644 tests/PhpPact/Helper/PhpProcess.php diff --git a/compatibility-suite/tests/Service/GeneratorServer.php b/compatibility-suite/tests/Service/GeneratorServer.php index 3a2e3d75..dcc950be 100644 --- a/compatibility-suite/tests/Service/GeneratorServer.php +++ b/compatibility-suite/tests/Service/GeneratorServer.php @@ -3,12 +3,11 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPactTest\CompatibilitySuite\Constant\Path; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; final class GeneratorServer implements GeneratorServerInterface { - private int $port = 0; - private ProviderProcess $process; + private PhpProcess $process; private string $bodyFile = Path::PUBLIC_PATH . '/generators/body.json'; private string $pathFile = Path::PUBLIC_PATH . '/generators/path.txt'; private string $headersFile = Path::PUBLIC_PATH . '/generators/headers.json'; @@ -19,22 +18,18 @@ public function start(): void foreach ([$this->bodyFile, $this->pathFile, $this->headersFile, $this->queryParamsFile] as $file) { @unlink($file); } - $socket = \socket_create_listen($this->port); - \socket_getsockname($socket, $addr, $this->port); - \socket_close($socket); - $this->process = new ProviderProcess(Path::PUBLIC_PATH . '/generators/', $this->port); + $this->process = new PhpProcess(Path::PUBLIC_PATH . '/generators/'); $this->process->start(); } public function stop(): void { - $this->port = 0; $this->process->stop(); } public function getPort(): int { - return $this->port; + return $this->process->getPort(); } public function getBody(): string diff --git a/compatibility-suite/tests/Service/ProviderStateServer.php b/compatibility-suite/tests/Service/ProviderStateServer.php index d28faf11..aabcd9cb 100644 --- a/compatibility-suite/tests/Service/ProviderStateServer.php +++ b/compatibility-suite/tests/Service/ProviderStateServer.php @@ -3,41 +3,36 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPactTest\CompatibilitySuite\Constant\Path; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; final class ProviderStateServer implements ProviderStateServerInterface { - private int $port = 0; - private ProviderProcess $process; + private PhpProcess $process; public function start(): void { @unlink(Path::PUBLIC_PATH . '/provider-states/provider-states.json'); - $socket = \socket_create_listen($this->port); - \socket_getsockname($socket, $addr, $this->port); - \socket_close($socket); - $this->process = new ProviderProcess(Path::PUBLIC_PATH . '/provider-states/', $this->port); + $this->process = new PhpProcess(Path::PUBLIC_PATH . '/provider-states/'); $this->process->start(); } public function stop(): void { - $this->port = 0; $this->process->stop(); } public function getPort(): int { - return $this->port; + return $this->process->getPort(); } public function hasAction(string $action): bool { - return file_get_contents("http://localhost:$this->port/has-action?action=" . urlencode($action)); + return file_get_contents(sprintf('http://localhost:%d/has-action?action=%s', $this->getPort(), urlencode($action))); } public function hasState(string $action, string $state, array $params = []): bool { - return file_get_contents("http://localhost:$this->port/has-state?action=" . urlencode($action) . '&state=' . urlencode($state) . ($params ? ('&' . http_build_query($params)) : '')); + return file_get_contents(sprintf('http://localhost:%d/has-state?action=%s&state=%s%s', $this->getPort(), urlencode($action), urlencode($state), $params ? ('&' . http_build_query($params)) : '')); } } diff --git a/composer.json b/composer.json index d7d6ac74..290a44eb 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "tienvx/composer-downloads-plugin": "^1.2.0" }, "require-dev": { + "ext-sockets": "*", "roave/security-advisories": "dev-latest", "slim/slim": "^4.6", "slim/psr7": "^1.2.0", diff --git a/example/binary/provider/tests/PactVerifyTest.php b/example/binary/provider/tests/PactVerifyTest.php index 77059516..5d2516bb 100644 --- a/example/binary/provider/tests/PactVerifyTest.php +++ b/example/binary/provider/tests/PactVerifyTest.php @@ -4,25 +4,19 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -37,7 +31,7 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('binaryProvider') // Providers name to fetch. ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); } diff --git a/example/csv/provider/tests/PactVerifyTest.php b/example/csv/provider/tests/PactVerifyTest.php index 93ae369d..0089941e 100644 --- a/example/csv/provider/tests/PactVerifyTest.php +++ b/example/csv/provider/tests/PactVerifyTest.php @@ -5,25 +5,19 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -35,9 +29,9 @@ public function testPactVerifyConsumer(): void $config->getProviderInfo() ->setName('csvProvider') ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; if ($logLevel = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($logLevel); diff --git a/example/generators/provider/tests/PactVerifyTest.php b/example/generators/provider/tests/PactVerifyTest.php index 86f8ea05..e1648885 100644 --- a/example/generators/provider/tests/PactVerifyTest.php +++ b/example/generators/provider/tests/PactVerifyTest.php @@ -5,16 +5,16 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } @@ -29,9 +29,9 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('generatorsProvider') ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); diff --git a/example/json/provider/tests/PactVerifyTest.php b/example/json/provider/tests/PactVerifyTest.php index 94ca1599..52eebcf7 100644 --- a/example/json/provider/tests/PactVerifyTest.php +++ b/example/json/provider/tests/PactVerifyTest.php @@ -5,25 +5,19 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -38,9 +32,9 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('jsonProvider') // Providers name to fetch. ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); diff --git a/example/matchers/provider/tests/PactVerifyTest.php b/example/matchers/provider/tests/PactVerifyTest.php index 8a823f5d..726d6d36 100644 --- a/example/matchers/provider/tests/PactVerifyTest.php +++ b/example/matchers/provider/tests/PactVerifyTest.php @@ -5,16 +5,16 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } @@ -29,9 +29,9 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('matchersProvider') ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); diff --git a/example/message/provider/tests/PactVerifyTest.php b/example/message/provider/tests/PactVerifyTest.php index bea7fca0..e496b138 100644 --- a/example/message/provider/tests/PactVerifyTest.php +++ b/example/message/provider/tests/PactVerifyTest.php @@ -6,25 +6,19 @@ use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -39,14 +33,14 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('messageProvider') // Providers name to fetch. ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; $config->addProviderTransport( (new ProviderTransport()) ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) - ->setPort(7202) + ->setPort($this->process->getPort()) ->setPath('/pact-messages') ->setScheme('http') ); diff --git a/example/multipart/provider/tests/PactVerifyTest.php b/example/multipart/provider/tests/PactVerifyTest.php index 439e2802..664012b0 100644 --- a/example/multipart/provider/tests/PactVerifyTest.php +++ b/example/multipart/provider/tests/PactVerifyTest.php @@ -4,25 +4,19 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -37,7 +31,7 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('multipartProvider') // Providers name to fetch. ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); } diff --git a/example/protobuf-async-message/provider/tests/PactVerifyTest.php b/example/protobuf-async-message/provider/tests/PactVerifyTest.php index d9eff681..21b52368 100644 --- a/example/protobuf-async-message/provider/tests/PactVerifyTest.php +++ b/example/protobuf-async-message/provider/tests/PactVerifyTest.php @@ -5,25 +5,19 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -35,9 +29,9 @@ public function testPactVerifyConsumer(): void $config->getProviderInfo() ->setName('protobufAsyncMessageProvider') ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri("http://localhost:7202/pact-change-state")) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ->setStateChangeTeardown(true) ->setStateChangeAsBody(true) ; diff --git a/example/protobuf-sync-message/provider/tests/PactVerifyTest.php b/example/protobuf-sync-message/provider/tests/PactVerifyTest.php index 1c5426c8..fb6a22f7 100644 --- a/example/protobuf-sync-message/provider/tests/PactVerifyTest.php +++ b/example/protobuf-sync-message/provider/tests/PactVerifyTest.php @@ -1,26 +1,20 @@ process = new Process([__DIR__ . '/../bin/roadrunner/rr', 'serve', '-w', __DIR__ . '/..']); - $this->process->setTimeout(120); - - $this->process->start(function (string $type, string $buffer): void { - echo "\n$type > $buffer"; - }); - $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 9001))); + $this->process = new RoadRunnerProcess(); + $this->process->start(); } protected function tearDown(): void @@ -38,7 +32,7 @@ public function testPactVerifyConsumer(): void $providerTransport ->setProtocol('grpc') ->setScheme('tcp') - ->setPort(9001) + ->setPort($this->process->getPort()) ->setPath('/') ; $config->addProviderTransport($providerTransport); diff --git a/example/protobuf-sync-message/provider/tests/RoadRunnerProcess.php b/example/protobuf-sync-message/provider/tests/RoadRunnerProcess.php new file mode 100644 index 00000000..acfa52b7 --- /dev/null +++ b/example/protobuf-sync-message/provider/tests/RoadRunnerProcess.php @@ -0,0 +1,22 @@ +setTimeout(120); + + return $process; + } +} diff --git a/example/xml/provider/tests/PactVerifyTest.php b/example/xml/provider/tests/PactVerifyTest.php index eedbff1d..6116e68c 100644 --- a/example/xml/provider/tests/PactVerifyTest.php +++ b/example/xml/provider/tests/PactVerifyTest.php @@ -5,25 +5,19 @@ use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class PactVerifyTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../public/'); + $this->process = new PhpProcess(__DIR__ . '/../public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -38,9 +32,9 @@ public function testPactVerifyConsumer() $config->getProviderInfo() ->setName('xmlProvider') // Providers name to fetch. ->setHost('localhost') - ->setPort(7202); + ->setPort($this->process->getPort()); $config->getProviderState() - ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ->setStateChangeUrl(new Uri(sprintf('http://localhost:%d/pact-change-state', $this->process->getPort()))) ; if ($level = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($level); diff --git a/tests/PhpPact/Helper/ProviderProcess.php b/tests/PhpPact/Helper/AbstractProcess.php similarity index 51% rename from tests/PhpPact/Helper/ProviderProcess.php rename to tests/PhpPact/Helper/AbstractProcess.php index 83e9a2c8..8c2630f9 100644 --- a/tests/PhpPact/Helper/ProviderProcess.php +++ b/tests/PhpPact/Helper/AbstractProcess.php @@ -2,27 +2,31 @@ namespace PhpPactTest\Helper; +use PhpPactTest\Helper\Exception\NoPortAvailableException; use Symfony\Component\Process\Process; -class ProviderProcess +abstract class AbstractProcess { private Process $process; - public function __construct(string $publicPath, private int $port = 7202) + public function __construct() { - $this->process = new Process(['php', '-S', "127.0.0.1:$port", '-t', $publicPath]); + $this->process = $this->getProcess(); } public function start(): void { + if ($this->process->isRunning()) { + return; + } $this->process->start(function (string $type, string $buffer): void { echo "\n$type > $buffer"; }); $this->process->waitUntil(function (): bool { - $fp = @fsockopen('127.0.0.1', $this->port); - $isOpen = is_resource($fp); + $fp = @fsockopen('127.0.0.1', $this->getPort()); + $isOpen = \is_resource($fp); if ($isOpen) { - fclose($fp); + \fclose($fp); } return $isOpen; @@ -33,4 +37,8 @@ public function stop(): void { $this->process->stop(); } + + abstract public function getPort(): int; + + abstract protected function getProcess(): Process; } diff --git a/tests/PhpPact/Helper/Exception/HelperException.php b/tests/PhpPact/Helper/Exception/HelperException.php new file mode 100644 index 00000000..630f6af3 --- /dev/null +++ b/tests/PhpPact/Helper/Exception/HelperException.php @@ -0,0 +1,9 @@ +port) { + $this->port = $this->findAvailablePort(); + } + + return $this->port; + } + + protected function getProcess(): Process + { + return new Process(['php', '-S', '127.0.0.1:' . $this->getPort(), '-t', $this->publicPath]); + } + + private function findAvailablePort(): int + { + $socket = \socket_create_listen(0); + \socket_getsockname($socket, $addr, $port); + \socket_close($socket); + + if (!$port) { + throw new NoPortAvailableException(); + } + + return $port; + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 507e03ee..c66fc75c 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -4,25 +4,19 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPactTest\Helper\ProviderProcess; +use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\TestCase; class VerifierTest extends TestCase { - private ProviderProcess $process; + private PhpProcess $process; - /** - * Run the PHP build-in web server. - */ protected function setUp(): void { - $this->process = new ProviderProcess(__DIR__ . '/../../../_public/'); + $this->process = new PhpProcess(__DIR__ . '/../../../_public/'); $this->process->start(); } - /** - * Stop the web server process once complete. - */ protected function tearDown(): void { $this->process->stop(); @@ -34,7 +28,7 @@ public function testVerify(): void $config->getProviderInfo() ->setName('someProvider') ->setHost('localhost') - ->setPort(7202) + ->setPort($this->process->getPort()) ->setScheme('http') ->setPath('/'); if ($level = \getenv('PACT_LOGLEVEL')) { From fc68c566e679095e16b7971c235729df322296ab Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 24 Jan 2024 10:15:38 +0700 Subject: [PATCH 198/298] chore: Update remaining pacts for 0.4.13 --- example/binary/pacts/binaryConsumer-binaryProvider.json | 4 ++-- example/csv/pacts/csvConsumer-csvProvider.json | 4 ++-- .../pacts/generatorsConsumer-generatorsProvider.json | 4 ++-- example/json/pacts/jsonConsumer-jsonProvider.json | 7 ++++--- .../message/pacts/messageConsumer-messageProvider.json | 4 ++-- .../pacts/multipartConsumer-multipartProvider.json | 9 +++++---- ...syncMessageConsumer-protobufAsyncMessageProvider.json | 4 ++-- ...fSyncMessageConsumer-protobufSyncMessageProvider.json | 4 ++-- example/xml/pacts/xmlConsumer-xmlProvider.json | 8 ++++---- 9 files changed, 25 insertions(+), 23 deletions(-) diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json index a8511d97..da895fcb 100644 --- a/example/binary/pacts/binaryConsumer-binaryProvider.json +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -43,9 +43,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/csv/pacts/csvConsumer-csvProvider.json b/example/csv/pacts/csvConsumer-csvProvider.json index 8d001bc1..f0f4adea 100644 --- a/example/csv/pacts/csvConsumer-csvProvider.json +++ b/example/csv/pacts/csvConsumer-csvProvider.json @@ -87,9 +87,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "4.0" diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json index 12125983..7a6e1a07 100644 --- a/example/generators/pacts/generatorsConsumer-generatorsProvider.json +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -261,9 +261,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "4.0" diff --git a/example/json/pacts/jsonConsumer-jsonProvider.json b/example/json/pacts/jsonConsumer-jsonProvider.json index a169f1f2..a37f611e 100644 --- a/example/json/pacts/jsonConsumer-jsonProvider.json +++ b/example/json/pacts/jsonConsumer-jsonProvider.json @@ -31,7 +31,8 @@ ] } }, - "header": {} + "header": {}, + "status": {} }, "status": 200 } @@ -63,9 +64,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.9", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.11" + "models": "1.1.16" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/message/pacts/messageConsumer-messageProvider.json b/example/message/pacts/messageConsumer-messageProvider.json index ea82586b..9f51d8d7 100644 --- a/example/message/pacts/messageConsumer-messageProvider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -63,8 +63,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", - "models": "1.1.12" + "ffi": "0.4.13", + "models": "1.1.16" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/multipart/pacts/multipartConsumer-multipartProvider.json b/example/multipart/pacts/multipartConsumer-multipartProvider.json index 69e198d3..5bfc8331 100644 --- a/example/multipart/pacts/multipartConsumer-multipartProvider.json +++ b/example/multipart/pacts/multipartConsumer-multipartProvider.json @@ -48,7 +48,7 @@ } }, "header": { - "$.Authorization[0]": { + "Authorization": { "combine": "AND", "matchers": [ { @@ -107,7 +107,8 @@ ] } }, - "header": {} + "header": {}, + "status": {} }, "status": 200 } @@ -115,9 +116,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.9", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.11" + "models": "1.1.16" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json index 4fd83ff1..ba9383a5 100644 --- a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json +++ b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json @@ -68,8 +68,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", - "models": "1.1.12" + "ffi": "0.4.13", + "models": "1.1.16" }, "pactSpecification": { "version": "4.0" diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json index ab308e25..3f154b0f 100644 --- a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json +++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json @@ -78,9 +78,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "4.0" diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index 21cc297b..2216cf4b 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -16,7 +16,7 @@ }, "matchingRules": { "header": { - "$.Accept[0]": { + "Accept": { "combine": "AND", "matchers": [ { @@ -120,7 +120,7 @@ } }, "header": { - "$['Content-Type'][0]": { + "Content-Type": { "combine": "AND", "matchers": [ { @@ -138,9 +138,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.11", + "ffi": "0.4.13", "mockserver": "1.2.4", - "models": "1.1.12" + "models": "1.1.16" }, "pactSpecification": { "version": "3.0.0" From 464c5773eced8a189e4a78440377b46ccf25399f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 25 Jan 2024 14:08:00 +0700 Subject: [PATCH 199/298] feat: Allow turn off log --- composer.json | 4 ++-- .../pacts/binaryConsumer-binaryProvider.json | 6 +++--- example/csv/pacts/csvConsumer-csvProvider.json | 6 +++--- .../generatorsConsumer-generatorsProvider.json | 6 +++--- .../json/pacts/jsonConsumer-jsonProvider.json | 6 +++--- .../matchersConsumer-matchersProvider.json | 6 +++--- .../consumer/src/ExampleMessageConsumer.php | 18 ++++++++++-------- .../pacts/messageConsumer-messageProvider.json | 4 ++-- .../multipartConsumer-multipartProvider.json | 6 +++--- ...eConsumer-protobufAsyncMessageProvider.json | 4 ++-- ...geConsumer-protobufSyncMessageProvider.json | 6 +++--- example/xml/pacts/xmlConsumer-xmlProvider.json | 6 +++--- src/PhpPact/Config/LogLevelTrait.php | 2 +- tests/PhpPact/Helper/AbstractProcess.php | 9 ++++++--- 14 files changed, 47 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index 290a44eb..60b306aa 100644 --- a/composer.json +++ b/composer.json @@ -109,12 +109,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.13", + "version": "0.4.14", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.13", + "version": "0.4.14", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json index da895fcb..7480e0e4 100644 --- a/example/binary/pacts/binaryConsumer-binaryProvider.json +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -43,9 +43,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/csv/pacts/csvConsumer-csvProvider.json b/example/csv/pacts/csvConsumer-csvProvider.json index f0f4adea..36910ea1 100644 --- a/example/csv/pacts/csvConsumer-csvProvider.json +++ b/example/csv/pacts/csvConsumer-csvProvider.json @@ -87,9 +87,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "4.0" diff --git a/example/generators/pacts/generatorsConsumer-generatorsProvider.json b/example/generators/pacts/generatorsConsumer-generatorsProvider.json index 7a6e1a07..9c9b83d3 100644 --- a/example/generators/pacts/generatorsConsumer-generatorsProvider.json +++ b/example/generators/pacts/generatorsConsumer-generatorsProvider.json @@ -261,9 +261,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "4.0" diff --git a/example/json/pacts/jsonConsumer-jsonProvider.json b/example/json/pacts/jsonConsumer-jsonProvider.json index a37f611e..739d22d8 100644 --- a/example/json/pacts/jsonConsumer-jsonProvider.json +++ b/example/json/pacts/jsonConsumer-jsonProvider.json @@ -64,9 +64,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index 8cd325d0..9b215c84 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -591,9 +591,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "4.0" diff --git a/example/message/consumer/src/ExampleMessageConsumer.php b/example/message/consumer/src/ExampleMessageConsumer.php index d913ff63..edd1e6f2 100644 --- a/example/message/consumer/src/ExampleMessageConsumer.php +++ b/example/message/consumer/src/ExampleMessageConsumer.php @@ -7,13 +7,15 @@ class ExampleMessageConsumer public function processMessage(string $message): void { $obj = \json_decode($message); - print " [x] Processing \n"; - print " [x] Contents: \n"; - print ' [x] Text: ' . \print_r($obj->contents->text, true) . "\n"; - print ' [x] Number: ' . \print_r($obj->contents->number, true) . "\n"; - print " [x] Metadata: \n"; - print ' [x] Queue: ' . \print_r($obj->metadata->queue, true) . "\n"; - print ' [x] Routing Key: ' . \print_r($obj->metadata->routing_key, true) . "\n"; - print " [x] Processed \n"; + if (($logLevel = \getenv('PACT_LOGLEVEL')) && !in_array(\strtoupper($logLevel), ['OFF', 'NONE'])) { + print " [x] Processing \n"; + print " [x] Contents: \n"; + print ' [x] Text: ' . \print_r($obj->contents->text, true) . "\n"; + print ' [x] Number: ' . \print_r($obj->contents->number, true) . "\n"; + print " [x] Metadata: \n"; + print ' [x] Queue: ' . \print_r($obj->metadata->queue, true) . "\n"; + print ' [x] Routing Key: ' . \print_r($obj->metadata->routing_key, true) . "\n"; + print " [x] Processed \n"; + } } } diff --git a/example/message/pacts/messageConsumer-messageProvider.json b/example/message/pacts/messageConsumer-messageProvider.json index 9f51d8d7..8d8a6179 100644 --- a/example/message/pacts/messageConsumer-messageProvider.json +++ b/example/message/pacts/messageConsumer-messageProvider.json @@ -63,8 +63,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "models": "1.1.16" + "ffi": "0.4.14", + "models": "1.1.17" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/multipart/pacts/multipartConsumer-multipartProvider.json b/example/multipart/pacts/multipartConsumer-multipartProvider.json index 5bfc8331..c094318a 100644 --- a/example/multipart/pacts/multipartConsumer-multipartProvider.json +++ b/example/multipart/pacts/multipartConsumer-multipartProvider.json @@ -116,9 +116,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "3.0.0" diff --git a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json index ba9383a5..77a15e75 100644 --- a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json +++ b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json @@ -68,8 +68,8 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "models": "1.1.16" + "ffi": "0.4.14", + "models": "1.1.17" }, "pactSpecification": { "version": "4.0" diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json index 3f154b0f..fca72dad 100644 --- a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json +++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json @@ -78,9 +78,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "4.0" diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index 2216cf4b..68d11370 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -138,9 +138,9 @@ ], "metadata": { "pactRust": { - "ffi": "0.4.13", - "mockserver": "1.2.4", - "models": "1.1.16" + "ffi": "0.4.14", + "mockserver": "1.2.5", + "models": "1.1.17" }, "pactSpecification": { "version": "3.0.0" diff --git a/src/PhpPact/Config/LogLevelTrait.php b/src/PhpPact/Config/LogLevelTrait.php index de5265ce..4d109e87 100644 --- a/src/PhpPact/Config/LogLevelTrait.php +++ b/src/PhpPact/Config/LogLevelTrait.php @@ -14,7 +14,7 @@ public function getLogLevel(): ?string public function setLogLevel(string $logLevel): self { $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'])) { + if (!\in_array($logLevel, ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'OFF', 'NONE'])) { throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); } $this->logLevel = $logLevel; diff --git a/tests/PhpPact/Helper/AbstractProcess.php b/tests/PhpPact/Helper/AbstractProcess.php index 8c2630f9..3f6a36e6 100644 --- a/tests/PhpPact/Helper/AbstractProcess.php +++ b/tests/PhpPact/Helper/AbstractProcess.php @@ -19,9 +19,12 @@ public function start(): void if ($this->process->isRunning()) { return; } - $this->process->start(function (string $type, string $buffer): void { - echo "\n$type > $buffer"; - }); + if (($logLevel = \getenv('PACT_LOGLEVEL')) && !in_array(\strtoupper($logLevel), ['OFF', 'NONE'])) { + $callback = function (string $type, string $buffer): void { + echo "\n$type > $buffer"; + }; + } + $this->process->start($callback ?? null); $this->process->waitUntil(function (): bool { $fp = @fsockopen('127.0.0.1', $this->getPort()); $isOpen = \is_resource($fp); From 555368c57de8708efac67ca2b3204420a8f5bc1d Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Wed, 31 Jan 2024 16:52:59 +0000 Subject: [PATCH 200/298] chore(ci): remove cirrus-ci --- .cirrus.yml | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 .cirrus.yml diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 16bafdd6..00000000 --- a/.cirrus.yml +++ /dev/null @@ -1,53 +0,0 @@ -BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE - arch_check_script: - - uname -am - install_script: - - composer install - cirrus_ci_macos_local_script: | - if [ "$(uname -s)" == 'Darwin' ] && [ "$CIRRUS_CLI" == 'true' ]; then - chmod +x vendor/bin/php-cs-fixer - chmod +x vendor/bin/phpstan - chmod +x vendor/bin/phpunit - chmod +x bin/pact-stub-server/pact-stub-server - fi - lint_script: - - composer run lint - static_analysis_script: - - composer run static-code-analysis - generate_library_script: - - composer gen-lib - test_script: - - composer test - -linux_arm64_task: - env: - COMPOSER_ALLOW_SUPERUSER: 1 - PACT_DO_NOT_TRACK: true - matrix: - - VERSION: 8.2 - - VERSION: 8.1 - - VERSION: 8.0 - arm_container: - dockerfile: .cirrus/linux_arm64/Dockerfile - docker_arguments: - VERSION: $VERSION - version_check_script: - - php --version - << : *BUILD_TEST_TASK_TEMPLATE - -macos_arm64_task: -# https://www.markhesketh.com/switching-multiple-php-versions-on-macos/ - env: - PACT_DO_NOT_TRACK: true - matrix: - - VERSION: 8.2 - - VERSION: 8.1 - - VERSION: 8.0 - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-base:latest - pre_req_script: - - brew install php@$VERSION composer protobuf - - MAKEFLAGS=" -j4" pecl install grpc - version_check_script: - - php --version - << : *BUILD_TEST_TASK_TEMPLATE From 343c91b118d1298a2c4629ba3208c42007395592 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 5 Feb 2024 11:10:10 +0700 Subject: [PATCH 201/298] ci(test): Test mac arm --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a36eb477..f539c00d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - operating-system: [ ubuntu-latest, macos-latest, windows-latest ] + operating-system: [ ubuntu-latest, windows-latest, macos-12, macos-14 ] php: [ '8.0', '8.1', '8.2' ] dependencies: [ 'lowest', 'locked' ] timeout-minutes: 5 From f2eba2ad913e81f6ca8c40e78bc8fdba0a19bc8f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 5 Feb 2024 19:19:05 +0700 Subject: [PATCH 202/298] chore: Remove unnecessary return --- src/PhpPact/FFI/Model/ArrayData.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/FFI/Model/ArrayData.php b/src/PhpPact/FFI/Model/ArrayData.php index 8945b2d1..93df1d7a 100644 --- a/src/PhpPact/FFI/Model/ArrayData.php +++ b/src/PhpPact/FFI/Model/ArrayData.php @@ -36,14 +36,14 @@ public static function createFrom(array $values): ?self $items = FFI::new("char*[{$size}]"); if ($items === null) { - return throw new CDataNotCreatedException(); + throw new CDataNotCreatedException(); } foreach ($values as $index => $value) { $length = \strlen($value); $itemSize = $length + 1; $item = FFI::new("char[{$itemSize}]", false); if ($item === null) { - return throw new CDataNotCreatedException(); + throw new CDataNotCreatedException(); } FFI::memcpy($item, $value, $length); $items[$index] = $item; // @phpstan-ignore-line From 767e95506369fb801c6d2a1d3deecefd36cdd606 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 5 Feb 2024 22:10:54 +0700 Subject: [PATCH 203/298] chore: Throw CDataNotCreatedException --- src/PhpPact/FFI/Model/BinaryData.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PhpPact/FFI/Model/BinaryData.php b/src/PhpPact/FFI/Model/BinaryData.php index 94ac4521..ed77c4b0 100644 --- a/src/PhpPact/FFI/Model/BinaryData.php +++ b/src/PhpPact/FFI/Model/BinaryData.php @@ -4,6 +4,7 @@ use FFI\CData; use FFI; +use PhpPact\FFI\Exception\CDataNotCreatedException; use PhpPact\FFI\Exception\EmptyBinaryFileNotSupportedException; class BinaryData @@ -32,6 +33,9 @@ public static function createFrom(string $contents): self $length = \strlen($contents); $cData = FFI::new("uint8_t[{$length}]", false); + if ($cData === null) { + throw new CDataNotCreatedException(); + } FFI::memcpy($cData, $contents, $length); return new self($cData, $length); From ae9d81f2a6fa01937df98084a6ae47c798bccc72 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 5 Feb 2024 22:20:05 +0700 Subject: [PATCH 204/298] chore: Add FFIException --- src/PhpPact/FFI/Exception/CDataNotCreatedException.php | 4 +--- .../Exception/EmptyBinaryFileNotSupportedException.php | 4 +--- src/PhpPact/FFI/Exception/FFIException.php | 9 +++++++++ src/PhpPact/FFI/Exception/HeaderNotReadException.php | 4 +--- 4 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/PhpPact/FFI/Exception/FFIException.php diff --git a/src/PhpPact/FFI/Exception/CDataNotCreatedException.php b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php index 2e17f429..aa882a06 100644 --- a/src/PhpPact/FFI/Exception/CDataNotCreatedException.php +++ b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php @@ -2,8 +2,6 @@ namespace PhpPact\FFI\Exception; -use Exception; - -class CDataNotCreatedException extends Exception +class CDataNotCreatedException extends FFIException { } diff --git a/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php b/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php index 602f9a4b..f8b8191c 100644 --- a/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php +++ b/src/PhpPact/FFI/Exception/EmptyBinaryFileNotSupportedException.php @@ -2,8 +2,6 @@ namespace PhpPact\FFI\Exception; -use Exception; - -class EmptyBinaryFileNotSupportedException extends Exception +class EmptyBinaryFileNotSupportedException extends FFIException { } diff --git a/src/PhpPact/FFI/Exception/FFIException.php b/src/PhpPact/FFI/Exception/FFIException.php new file mode 100644 index 00000000..fc79008e --- /dev/null +++ b/src/PhpPact/FFI/Exception/FFIException.php @@ -0,0 +1,9 @@ + Date: Tue, 6 Feb 2024 12:17:05 +0700 Subject: [PATCH 205/298] chore: Deprecate Values matcher --- .../consumer/tests/Service/MatchersTest.php | 20 ------------- .../matchersConsumer-matchersProvider.json | 28 +------------------ example/matchers/provider/public/index.php | 12 -------- src/PhpPact/Consumer/Matcher/Matcher.php | 2 ++ .../Consumer/Matcher/Matchers/Values.php | 2 ++ 5 files changed, 5 insertions(+), 59 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index fb02ef63..4cf76940 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -84,16 +84,6 @@ public function testGetMatchers(): void ]), 'notEmpty' => $this->matcher->notEmpty(['1','2','3']), 'semver' => $this->matcher->semver('10.0.0-alpha4'), - 'values' => $this->matcher->values([ - 'a', - 'bb', - 'ccc', - ]), - 'valuesWithKeys' => $this->matcher->values([ - 'a' => 'a', - 'b' => 'bb', - 'c' => 'ccc', - ]), 'contentType' => $this->matcher->contentType('text/html'), 'eachKey' => $this->matcher->eachKey( ['page 3' => 'example text'], @@ -179,16 +169,6 @@ public function testGetMatchers(): void ], 'notEmpty' => ['1', '2', '3'], 'semver' => '10.0.0-alpha4', - 'values' => [ - 'a', - 'bb', - 'ccc', - ], - 'valuesWithKeys' => [ - 'a' => 'a', - 'b' => 'bb', - 'c' => 'ccc', - ], 'contentType' => 'text/html', 'eachKey' => [ 'page 3' => 'example text', diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index 9b215c84..6f7b91bc 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -141,17 +141,7 @@ "timeISO8601": "T22:44:30.652Z", "timestampRFC3339": "Mon, 31 Oct 2016 15:21:41 -0400", "url": "http://localhost:8080/users/1234/posts/latest", - "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb", - "values": [ - "a", - "bb", - "ccc" - ], - "valuesWithKeys": { - "a": "a", - "b": "bb", - "c": "ccc" - } + "uuid": "52c9585e-f345-4964-aa28-a45c64b2b2eb" }, "contentType": "application/json", "encoded": false @@ -552,22 +542,6 @@ "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" } ] - }, - "$.values": { - "combine": "AND", - "matchers": [ - { - "match": "values" - } - ] - }, - "$.valuesWithKeys": { - "combine": "AND", - "matchers": [ - { - "match": "values" - } - ] } }, "header": {}, diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 87fde298..03fac97f 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -53,18 +53,6 @@ ], 'notEmpty' => [111], 'semver' => '0.27.1-beta2', - 'values' => [ // Missing ending values are OK, additional values are NOT OK - 'a', - 'bb', - //'ccc', - //'dddd', - ], - 'valuesWithKeys' => [ // Missing keys are OK, additional keys are NOT OK - //'a' => 'a', - 'b' => 'bb', - //'c' => 'ccc', - //'d' => 'dddd', - ], 'contentType' => << diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 3bcf2d2c..fd3ae6ae 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -412,6 +412,8 @@ public function statusCode(string $status, ?int $value = null): MatcherInterface /** * Match the values in a map, ignoring the keys * + * @deprecated use eachKey or eachValue + * * @param array $values */ public function values(array $values): MatcherInterface diff --git a/src/PhpPact/Consumer/Matcher/Matchers/Values.php b/src/PhpPact/Consumer/Matcher/Matchers/Values.php index cfb6f5ec..7e68cce4 100644 --- a/src/PhpPact/Consumer/Matcher/Matchers/Values.php +++ b/src/PhpPact/Consumer/Matcher/Matchers/Values.php @@ -4,6 +4,8 @@ /** * Match the values in a map, ignoring the keys + * + * @deprecated use EachKey or EachValue */ class Values extends AbstractMatcher { From 60800ce10cf50b59d17f582b81ccdebd060d512d Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 6 Feb 2024 13:45:23 +0700 Subject: [PATCH 206/298] ci: Remove remaining Dockerfile for cirrus ci --- .cirrus/linux_arm64/Dockerfile | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .cirrus/linux_arm64/Dockerfile diff --git a/.cirrus/linux_arm64/Dockerfile b/.cirrus/linux_arm64/Dockerfile deleted file mode 100644 index f3c4e43a..00000000 --- a/.cirrus/linux_arm64/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -ARG VERSION -FROM php:${VERSION} -RUN apt update --yes && apt install --yes zip unzip git libffi-dev protobuf-compiler -COPY --from=composer/composer:2-bin /composer /usr/local/bin/composer -RUN docker-php-ext-install sockets -RUN docker-php-ext-install ffi -RUN pecl install grpc -RUN echo 'extension=grpc.so' >> /usr/local/etc/php/conf.d/grpc.ini From a33dc12a2e287f6e03695b465570948b488abde3 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 23 Oct 2023 09:36:55 +0700 Subject: [PATCH 207/298] chore: add example for header matchers --- .../src/Service/HttpClientService.php | 2 +- .../consumer/tests/Service/MatchersTest.php | 4 ++- .../matchersConsumer-matchersProvider.json | 29 +++++++++++++++++-- example/matchers/provider/public/index.php | 7 ++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/example/matchers/consumer/src/Service/HttpClientService.php b/example/matchers/consumer/src/Service/HttpClientService.php index 8b857165..b3fb1a73 100644 --- a/example/matchers/consumer/src/Service/HttpClientService.php +++ b/example/matchers/consumer/src/Service/HttpClientService.php @@ -20,7 +20,7 @@ public function __construct(string $baseUri) public function sendRequest(): ResponseInterface { return $this->httpClient->get("{$this->baseUri}/matchers", [ - 'headers' => ['Accept' => 'application/json'], + 'headers' => ['Accept' => 'application/json', 'Theme' => 'light'], 'query' => 'pages=2&pages=3&locales[]=fr-BE&locales[]=ru-RU', 'http_errors' => false, ]); diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 4cf76940..fa9f61e7 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -34,12 +34,14 @@ public function testGetMatchers(): void json_encode($this->matcher->regex(['en-US', 'en-AU'], '^[a-z]{2}-[A-Z]{2}$')), ], ]) - ->addHeader('Accept', 'application/json'); + ->addHeader('Accept', 'application/json') + ->addHeader('Theme', $this->matcher->regex('dark', 'light|dark')); // arrayContains, eachKey, eachValue matchers are not working with headers $response = new ProviderResponse(); $response ->setStatus($this->matcher->statusCode(HttpStatus::SERVER_ERROR, 512)) ->addHeader('Content-Type', 'application/json') + ->addHeader('X-Powered-By', $this->matcher->string('PHP')) ->setBody([ 'like' => $this->matcher->like(['key' => 'value']), 'likeNull' => $this->matcher->like(null), diff --git a/example/matchers/pacts/matchersConsumer-matchersProvider.json b/example/matchers/pacts/matchersConsumer-matchersProvider.json index 6f7b91bc..12478be0 100644 --- a/example/matchers/pacts/matchersConsumer-matchersProvider.json +++ b/example/matchers/pacts/matchersConsumer-matchersProvider.json @@ -15,10 +15,23 @@ "headers": { "Accept": [ "application/json" + ], + "Theme": [ + "dark" ] }, "matchingRules": { - "header": {}, + "header": { + "Theme": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "light|dark" + } + ] + } + }, "path": { "combine": "AND", "matchers": [ @@ -149,6 +162,9 @@ "headers": { "Content-Type": [ "application/json" + ], + "X-Powered-By": [ + "PHP" ] }, "matchingRules": { @@ -544,7 +560,16 @@ ] } }, - "header": {}, + "header": { + "X-Powered-By": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + } + }, "status": { "$": { "combine": "AND", diff --git a/example/matchers/provider/public/index.php b/example/matchers/provider/public/index.php index 03fac97f..b33364bf 100644 --- a/example/matchers/provider/public/index.php +++ b/example/matchers/provider/public/index.php @@ -79,7 +79,12 @@ return $response ->withHeader('Content-Type', 'application/json') - ->withStatus(503); + ->withStatus(503) + ->withHeader('X-Powered-By', [ + 'PHP', + 'Nginx', + 'Slim', + ]); }); $app->post('/pact-change-state', function (Request $request, Response $response) { From 30d4b6fa3126a674501e370b3324a2ae4f477b59 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 9 Feb 2024 17:14:45 +0700 Subject: [PATCH 208/298] chore: No need manually json encode matcher --- example/matchers/consumer/tests/Service/MatchersTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/example/matchers/consumer/tests/Service/MatchersTest.php b/example/matchers/consumer/tests/Service/MatchersTest.php index 4cf76940..c464277e 100644 --- a/example/matchers/consumer/tests/Service/MatchersTest.php +++ b/example/matchers/consumer/tests/Service/MatchersTest.php @@ -27,12 +27,8 @@ public function testGetMatchers(): void ->setMethod('GET') ->setPath($this->matcher->regex('/matchers', '^\/matchers$')) ->setQuery([ - 'pages' => [ // Consumer send multiple values, but provider receive single (last) value - json_encode($this->matcher->regex([1, 22], '\d+')), - ], - 'locales[]' => [ // Consumer send multiple values, provider receive all values - json_encode($this->matcher->regex(['en-US', 'en-AU'], '^[a-z]{2}-[A-Z]{2}$')), - ], + 'pages' => $this->matcher->regex([1, 22], '\d+'), // arrayContains, eachKey, eachValue matchers are not working with query + 'locales[]' => $this->matcher->regex(['en-US', 'en-AU'], '^[a-z]{2}-[A-Z]{2}$'), // Use `locales[]` instead of `locales` syntax if provider use PHP language ]) ->addHeader('Accept', 'application/json'); From 7faaf8b288eb08cce3da946acbf3ac435fd0b970 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 7 Feb 2024 10:57:51 +0700 Subject: [PATCH 209/298] deps: Upgrade PHP to 8.3 and drop 8.0 --- .github/workflows/build.yml | 12 +++++------- composer.json | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f539c00d..0153f37e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: fail-fast: false matrix: operating-system: [ ubuntu-latest, windows-latest, macos-12, macos-14 ] - php: [ '8.0', '8.1', '8.2' ] + php: [ '8.1', '8.2', '8.3' ] dependencies: [ 'lowest', 'locked' ] timeout-minutes: 5 @@ -60,7 +60,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} + extensions: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} php-version: ${{ matrix.php }} coverage: none ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} @@ -70,14 +70,12 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install gRPC Extension (for PHP 8.2 on Windows) + - name: Install gRPC Extension (for PHP 8.2 & 8.3 on Windows) run: | cd C:\tools\php - Invoke-WebRequest -Uri https://phpdev.toolsforresearch.com/php-8.2.7-nts-Win32-vs16-x64-grpc-protobuf.zip -OutFile php8.2.zip - unzip php8.2.zip ext/php_grpc.dll - rm php8.2.zip + Invoke-WebRequest -Uri https://github.com/tienvx/build-php-grpc-extension/releases/download/builds/grpc_windows-2022_php-${{ matrix.php }}.dll -OutFile ext\php_grpc.dll echo "extension=php_grpc.dll" >> php.ini - if: ${{ matrix.operating-system == 'windows-latest' && matrix.php == '8.2' }} + if: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) }} shell: pwsh - name: Composer install diff --git a/composer.json b/composer.json index 60b306aa..40356514 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", "symfony/process": "^4.4|^5.4|^6.0", From 2df6798fdbf6bf575d8303dd48bf07222226dd84 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 07:55:08 +0700 Subject: [PATCH 210/298] deps: Upgrade phpunit --- composer.json | 2 +- .../generators/consumer/tests/Service/GeneratorsTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 40356514..cd18e882 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": ">=8.5.23 <10", + "phpunit/phpunit": "^10|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", diff --git a/example/generators/consumer/tests/Service/GeneratorsTest.php b/example/generators/consumer/tests/Service/GeneratorsTest.php index eca73caa..e166208f 100644 --- a/example/generators/consumer/tests/Service/GeneratorsTest.php +++ b/example/generators/consumer/tests/Service/GeneratorsTest.php @@ -84,12 +84,12 @@ public function testGetGenerators(): void $this->lessThanOrEqual(499) ) ); - $this->assertRegExp('/^' . $regexWithoutAnchors . '$/', $body['regex']); + $this->assertMatchesRegularExpression('/^' . $regexWithoutAnchors . '$/', $body['regex']); $this->assertIsBool($body['boolean']); $this->assertIsInt($body['integer']); $this->assertIsFloat($body['decimal'] + 0); - $this->assertRegExp('/' . Matcher::HEX_FORMAT . '/', $body['hexadecimal']); - $this->assertRegExp('/' . Matcher::UUID_V4_FORMAT . '/', $body['uuid']); + $this->assertMatchesRegularExpression('/' . Matcher::HEX_FORMAT . '/', $body['hexadecimal']); + $this->assertMatchesRegularExpression('/' . Matcher::UUID_V4_FORMAT . '/', $body['uuid']); $this->assertTrue($this->validateDateTime($body['date'], 'Y-m-d')); $this->assertTrue($this->validateDateTime($body['time'], 'H:i:s')); $this->assertTrue($this->validateDateTime($body['datetime'], "Y-m-d\TH:i:s")); @@ -97,7 +97,7 @@ public function testGetGenerators(): void $this->assertNotSame(StringValue::DEFAULT_VALUE, $body['string']); $this->assertIsNumeric($body['number']); $this->assertNotSame('http://localhost/users/1234/posts/latest', $body['url']); - $this->assertRegExp('/.*(\\/users\\/\\d+\\/posts\\/latest)$/', $body['url']); + $this->assertMatchesRegularExpression('/.*(\\/users\\/\\d+\\/posts\\/latest)$/', $body['url']); $this->assertSame(222, $body['requestId']); } From 89f8927c9657a6b65db95fedee85b2ca3836a9da Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 15:20:51 +0700 Subject: [PATCH 211/298] test: Fix deprecations 'Data Provider method is not static' --- .../Consumer/Matcher/Formatters/PluginFormatterTest.php | 8 ++++---- tests/PhpPact/Consumer/Matcher/MatcherTest.php | 6 +++--- .../Matcher/Matchers/GeneratorAwareMatcherTestCase.php | 2 +- .../Consumer/Matcher/Matchers/MatchingFieldTest.php | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php b/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php index c9bf7723..b8afee3d 100644 --- a/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php +++ b/tests/PhpPact/Consumer/Matcher/Formatters/PluginFormatterTest.php @@ -64,7 +64,7 @@ public function testInvalidRules(EachKey|EachValue $matcher): void $this->formatter->format($matcher); } - public function invalidRulesProvider(): array + public static function invalidRulesProvider(): array { return [ [new EachKey(["doesn't matter"], [])], @@ -84,7 +84,7 @@ public function testInvalidValue(MatcherInterface $matcher, string $type): void $this->formatter->format($matcher); } - public function invalidValueProvider(): array + public static function invalidValueProvider(): array { return [ [new Type((object)['key' => 'value']), 'object'], @@ -105,7 +105,7 @@ public function testNotSupportedMatcher(MatcherInterface $matcher): void $this->formatter->format($matcher); } - public function notSupportedMatcherProvider(): array + public static function notSupportedMatcherProvider(): array { return [ [new Values([1, 2, 3])], @@ -122,7 +122,7 @@ public function testFormat(MatcherInterface $matcher, string $json): void $this->assertSame($json, json_encode($this->formatter->format($matcher))); } - public function matcherProvider(): array + public static function matcherProvider(): array { return [ [new MatchingField('product'), '"matching($\'product\')"'], diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 5d79515c..5f681635 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -120,7 +120,7 @@ public function testTimeISO8601(string $time): void /** * @return string[] */ - public function dataProviderForTimeTest(): array + public static function dataProviderForTimeTest(): array { return [ ['T22:44:30.652Z'], @@ -147,7 +147,7 @@ public function testDateTimeISO8601(string $dateTime): void /** * @return string[] */ - public function dataProviderForDateTimeTest(): array + public static function dataProviderForDateTimeTest(): array { return [ ['2015-08-06T16:53:10+01:00'], @@ -172,7 +172,7 @@ public function testDateTimeWithMillisISO8601(string $dateTime): void /** * @return string[] */ - public function dataProviderForDateTimeWithMillisTest(): array + public static function dataProviderForDateTimeWithMillisTest(): array { return [ ['2015-08-06T16:53:10.123+01:00'], diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php b/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php index f8faa886..d309162f 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/GeneratorAwareMatcherTestCase.php @@ -46,7 +46,7 @@ public function testGeneratorNotRequired(GeneratorInterface $generator): void /** * @return GeneratorInterface[] */ - public function generatorProvider(): array + public static function generatorProvider(): array { return [ [new Date()], diff --git a/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php b/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php index 88c1c422..d43fd132 100644 --- a/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php +++ b/tests/PhpPact/Consumer/Matcher/Matchers/MatchingFieldTest.php @@ -40,7 +40,7 @@ public function testNotSupportedFormatter(string $formatterClassName): void json_encode($matcher); } - public function formatterProvider(): array + public static function formatterProvider(): array { return [ [MinimalFormatter::class], From a3f77bf7a92a456382609b86f7124d8d003ffa6b Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 15:24:56 +0700 Subject: [PATCH 212/298] test: Fix PHPUnit XML deprecated schema --- phpunit.xml | 54 +++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index ffc6386c..117e1416 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,6 @@ - - - - ./src/PhpPact - + + @@ -11,67 +8,67 @@ - ./tests + ./tests - ./example/json/consumer/tests + ./example/json/consumer/tests - ./example/json/provider/tests + ./example/json/provider/tests - ./example/binary/consumer/tests + ./example/binary/consumer/tests - ./example/binary/provider/tests + ./example/binary/provider/tests - ./example/multipart/consumer/tests + ./example/multipart/consumer/tests - ./example/multipart/provider/tests + ./example/multipart/provider/tests - ./example/xml/consumer/tests + ./example/xml/consumer/tests - ./example/xml/provider/tests + ./example/xml/provider/tests - ./example/message/consumer/tests + ./example/message/consumer/tests - ./example/message/provider/tests + ./example/message/provider/tests - ./example/matchers/consumer/tests + ./example/matchers/consumer/tests - ./example/matchers/provider/tests + ./example/matchers/provider/tests - ./example/generators/consumer/tests + ./example/generators/consumer/tests - ./example/generators/provider/tests + ./example/generators/provider/tests - ./example/csv/consumer/tests + ./example/csv/consumer/tests - ./example/csv/provider/tests + ./example/csv/provider/tests - ./example/protobuf-sync-message/consumer/tests + ./example/protobuf-sync-message/consumer/tests - ./example/protobuf-sync-message/provider/tests + ./example/protobuf-sync-message/provider/tests - ./example/protobuf-async-message/consumer/tests + ./example/protobuf-async-message/consumer/tests - ./example/protobuf-async-message/provider/tests + ./example/protobuf-async-message/provider/tests @@ -82,4 +79,9 @@
+ + + ./src/PhpPact + + From d35a848b70d954b06fde92e7fa9894e97fa86219 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 15:38:00 +0700 Subject: [PATCH 213/298] chore: Ignore phpunit cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 607926b0..0a714913 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,4 @@ vendor/* test_results pact example/output/* +.phpunit.cache/ From 95d374bec6a4d7349edf193f3f56e892a56f10ba Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 15:55:13 +0700 Subject: [PATCH 214/298] ci: Enable code coverage now required by phpunit --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0153f37e..409b4f7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: with: extensions: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} php-version: ${{ matrix.php }} - coverage: none + coverage: pcov ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} - name: Install Protoc From 913e051be1361178c65b02129db230911a404d80 Mon Sep 17 00:00:00 2001 From: tienvx Date: Wed, 14 Feb 2024 16:39:12 +0700 Subject: [PATCH 215/298] ci: Fix phpunit error 'Unknown option --debug' --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cd18e882..ee870849 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^10|^11", + "phpunit/phpunit": "^10.5.6|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", From d12af02e8ccd702bd717fde9e18f7e247782755a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 10:23:15 +0700 Subject: [PATCH 216/298] ci: Remove phpunit composer's script --- .github/workflows/build.yml | 4 ++-- composer.json | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 409b4f7a..848f4e49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,7 +86,7 @@ jobs: - name: Generate Library run: composer gen-lib - - name: Composer test - run: composer test + - name: Run Tests + run: vendor/bin/phpunit --debug env: PACT_DO_NOT_TRACK: true diff --git a/composer.json b/composer.json index ee870849..0d2d2c39 100644 --- a/composer.json +++ b/composer.json @@ -96,10 +96,7 @@ "static-code-analysis": "phpstan", "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", - "test": [ - "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", - "phpunit --debug" - ], + "clean-up-pacts": "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", "gen-lib": [ "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto", "protoc --php_out=example/protobuf-async-message/library/src example/protobuf-async-message/library/proto/say_hello.proto" From 8790877ec37f9473cd8c7b09f799a2f8e3096ce8 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 11:56:05 +0700 Subject: [PATCH 217/298] Revert "ci: Remove phpunit composer's script" This reverts commit d12af02e8ccd702bd717fde9e18f7e247782755a. --- .github/workflows/build.yml | 4 ++-- composer.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 848f4e49..409b4f7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,7 +86,7 @@ jobs: - name: Generate Library run: composer gen-lib - - name: Run Tests - run: vendor/bin/phpunit --debug + - name: Composer test + run: composer test env: PACT_DO_NOT_TRACK: true diff --git a/composer.json b/composer.json index 0d2d2c39..ee870849 100644 --- a/composer.json +++ b/composer.json @@ -96,7 +96,10 @@ "static-code-analysis": "phpstan", "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", - "clean-up-pacts": "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", + "test": [ + "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", + "phpunit --debug" + ], "gen-lib": [ "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto", "protoc --php_out=example/protobuf-async-message/library/src example/protobuf-async-message/library/proto/say_hello.proto" From 01ad46817d5b8d311393b5d6457b023bbf374171 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 11:58:03 +0700 Subject: [PATCH 218/298] fix: Remove coverage --- .github/workflows/build.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 409b4f7a..0153f37e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: with: extensions: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} php-version: ${{ matrix.php }} - coverage: pcov + coverage: none ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} - name: Install Protoc diff --git a/composer.json b/composer.json index ee870849..a022ba10 100644 --- a/composer.json +++ b/composer.json @@ -98,7 +98,7 @@ "fix": "php-cs-fixer fix", "test": [ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", - "phpunit --debug" + "phpunit --debug --no-coverage" ], "gen-lib": [ "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto", From 18147b62e27401b7f72124ae75432fb668a3e02d Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 12:03:00 +0700 Subject: [PATCH 219/298] fix: Remove debug --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a022ba10..e5157bf1 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^10.5.6|^11", + "phpunit/phpunit": "^10|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", @@ -98,7 +98,7 @@ "fix": "php-cs-fixer fix", "test": [ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", - "phpunit --debug --no-coverage" + "phpunit --no-coverage" ], "gen-lib": [ "protoc --php_out=example/protobuf-sync-message/library/src example/protobuf-sync-message/library/proto/area_calculator.proto", From 49f9c5dc069de6c5f34291e52c56a5d5248f393e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 12:10:21 +0700 Subject: [PATCH 220/298] fix: Fix 'Element source not expected' This reverts commit a3f77bf7a92a456382609b86f7124d8d003ffa6b. --- phpunit.xml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 117e1416..058065eb 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,9 @@ - + + + ./src/PhpPact + @@ -79,9 +82,4 @@
- - - ./src/PhpPact - - From aa6f0a661039911f10dbd618c5510dde07cc73a7 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 12:33:52 +0700 Subject: [PATCH 221/298] deps: Upgrade PHPUnit to lowest 10.1 --- composer.json | 2 +- phpunit.xml | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index e5157bf1..c71b528c 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^10|^11", + "phpunit/phpunit": "^10.1|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", diff --git a/phpunit.xml b/phpunit.xml index 058065eb..a9253359 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,6 @@ - + - - ./src/PhpPact - @@ -82,4 +79,9 @@
+ + + ./src/PhpPact + + From 94ad0334103781fd294e59dc3a7ea49729a4660c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 16:56:22 +0700 Subject: [PATCH 222/298] deps: Revert PHPUnit 9 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c71b528c..9b2d7172 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^10.1|^11", + "phpunit/phpunit": "^9|^10.1|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", From 24f572060813eb258737c1b096054041129671ae Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 21:33:34 +0700 Subject: [PATCH 223/298] deps: Update spiral/roadrunner-grpc --- example/protobuf-sync-message/provider/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json index e864a0f1..8ce77214 100644 --- a/example/protobuf-sync-message/provider/composer.json +++ b/example/protobuf-sync-message/provider/composer.json @@ -2,7 +2,7 @@ "name": "pact-foundation/example-protobuf-sync-message-provider", "require": { "grpc/grpc": "^1.57", - "spiral/roadrunner-grpc": "^2.0", + "spiral/roadrunner-grpc": "^3.2", "tienvx/composer-downloads-plugin": "^1.2" }, "require-dev": { From 567b7998aa187d341f63b34ae16024d22a3ce380 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 21:49:16 +0700 Subject: [PATCH 224/298] deps: Update roadrunner binary --- example/protobuf-sync-message/provider/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json index e864a0f1..e2fd806e 100644 --- a/example/protobuf-sync-message/provider/composer.json +++ b/example/protobuf-sync-message/provider/composer.json @@ -14,7 +14,7 @@ "extra": { "downloads": { "rr": { - "version": "2023.3.9", + "version": "2023.3.10", "variables": { "{$os}": "strtolower(PHP_OS_FAMILY)", "{$architecture}": "in_array(php_uname('m'), ['x86_64', 'AMD64']) ? 'amd64' : 'arm64'", From b3894a1cf06008bfafa72d5584d0d19ee3afc0bf Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 22:11:40 +0700 Subject: [PATCH 225/298] chore: Update plugin versions in pacts --- example/csv/pacts/csvConsumer-csvProvider.json | 2 +- ...otobufAsyncMessageConsumer-protobufAsyncMessageProvider.json | 2 +- ...protobufSyncMessageConsumer-protobufSyncMessageProvider.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/csv/pacts/csvConsumer-csvProvider.json b/example/csv/pacts/csvConsumer-csvProvider.json index 36910ea1..a554343f 100644 --- a/example/csv/pacts/csvConsumer-csvProvider.json +++ b/example/csv/pacts/csvConsumer-csvProvider.json @@ -98,7 +98,7 @@ { "configuration": {}, "name": "csv", - "version": "0.0.3" + "version": "0.0.5" } ] }, diff --git a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json index 77a15e75..b6bf9bce 100644 --- a/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json +++ b/example/protobuf-async-message/pacts/protobufAsyncMessageConsumer-protobufAsyncMessageProvider.json @@ -83,7 +83,7 @@ } }, "name": "protobuf", - "version": "0.3.8" + "version": "0.3.13" } ] }, diff --git a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json index fca72dad..0cc74d33 100644 --- a/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json +++ b/example/protobuf-sync-message/pacts/protobufSyncMessageConsumer-protobufSyncMessageProvider.json @@ -94,7 +94,7 @@ } }, "name": "protobuf", - "version": "0.3.8" + "version": "0.3.13" } ] }, From ddba84daf90c983825b6af2d62a77bc2a535984d Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 15 Feb 2024 22:41:00 +0700 Subject: [PATCH 226/298] chore: Use PHP 8.1 feature: Unpack string-keyed arrays --- .../Consumer/Matcher/Formatters/MinimalFormatter.php | 3 ++- .../Matcher/Formatters/ValueOptionalFormatter.php | 12 ++++++++++-- .../Matcher/Formatters/ValueRequiredFormatter.php | 4 +++- .../Matcher/Formatters/XmlContentFormatter.php | 3 ++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php index 2c7e49ee..0b4f6a74 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/MinimalFormatter.php @@ -14,6 +14,7 @@ public function format(MatcherInterface $matcher): array { return [ 'pact:matcher:type' => $matcher->getType(), - ] + $matcher->getAttributes()->getData(); + ...$matcher->getAttributes()->getData(), + ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php index d6a9e199..61ce2a0f 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueOptionalFormatter.php @@ -20,9 +20,17 @@ public function format(MatcherInterface $matcher): array $generator = $matcher instanceof GeneratorAwareInterface ? $matcher->getGenerator() : null; if ($generator) { - return $data + ['pact:generator:type' => $generator->getType()] + $attributes->merge($generator->getAttributes())->getData(); + return [ + ...$data, + 'pact:generator:type' => $generator->getType(), + ...$attributes->merge($generator->getAttributes())->getData(), + ]; } - return $data + $attributes->getData() + ['value' => $matcher->getValue()]; + return [ + ...$data, + ...$attributes->getData(), + 'value' => $matcher->getValue(), + ]; } } diff --git a/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php index 89dad973..7e367b05 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/ValueRequiredFormatter.php @@ -11,6 +11,8 @@ class ValueRequiredFormatter extends ValueOptionalFormatter */ public function format(MatcherInterface $matcher): array { - return parent::format($matcher) + ['value' => $matcher->getValue()]; + return [ + ...parent::format($matcher), + 'value' => $matcher->getValue()]; } } diff --git a/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php b/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php index efbdf0d0..a554b964 100644 --- a/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php +++ b/src/PhpPact/Consumer/Matcher/Formatters/XmlContentFormatter.php @@ -18,7 +18,8 @@ public function format(MatcherInterface $matcher): array 'content' => $matcher->getValue(), 'matcher' => [ 'pact:matcher:type' => $matcher->getType(), - ] + $matcher->getAttributes()->merge($generator ? $generator->getAttributes() : new Attributes($matcher))->getData(), + ...$matcher->getAttributes()->merge($generator ? $generator->getAttributes() : new Attributes($matcher))->getData(), + ], ]; if ($generator) { From 3c8466ed5512daa71db6f7843ea71505223f8dbb Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 16 Feb 2024 11:51:30 +0700 Subject: [PATCH 227/298] feat: Add Selector class --- UPGRADE-10.0.md | 6 +-- .../InvalidSelectorValueException.php | 7 ++++ .../Exception/ProviderVerifierException.php | 9 +++++ .../Model/ConsumerVersionSelectors.php | 26 +++++++++++-- .../Model/Selector/Selector.php | 37 +++++++++++++++++++ .../Model/Selector/SelectorInterface.php | 9 +++++ .../Model/ConsumerVersionSelectorsTest.php | 30 +++++++++++++++ .../Model/Selector/SelectorTest.php | 37 +++++++++++++++++++ 8 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Exception/InvalidSelectorValueException.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Exception/ProviderVerifierException.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Selector/Selector.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorInterface.php create mode 100644 tests/PhpPact/Standalone/ProviderVerifier/Model/ConsumerVersionSelectorsTest.php create mode 100644 tests/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorTest.php diff --git a/UPGRADE-10.0.md b/UPGRADE-10.0.md index c7c77892..16880862 100644 --- a/UPGRADE-10.0.md +++ b/UPGRADE-10.0.md @@ -98,11 +98,9 @@ This migrates from a CLI driven process for the Pact Framework, to an FFI proces // we need to set the provider branch here for PactBrokerWithDynamicConfiguration // as $publishOptions->setProviderBranch value set above isn't used. $broker->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD')); - // NOTE - this needs to be a boolean, not a string value, otherwise it doesn't pass through the selector. - // Maybe a pact-php or pact-rust thing $selectors = (new ConsumerVersionSelectors()) - ->addSelector(' { "mainBranch" : true } ') - ->addSelector(' { "deployedOrReleased" : true } '); + ->addSelector(new Selector(mainBranch: true)) + ->addSelector(new Selector(deployedOrReleased: true)); $broker->setConsumerVersionSelectors($selectors); $broker->setEnablePending(true); $broker->setIncludeWipPactSince('2020-01-30'); diff --git a/src/PhpPact/Standalone/ProviderVerifier/Exception/InvalidSelectorValueException.php b/src/PhpPact/Standalone/ProviderVerifier/Exception/InvalidSelectorValueException.php new file mode 100644 index 00000000..f1c4357a --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Exception/InvalidSelectorValueException.php @@ -0,0 +1,7 @@ + @@ -16,16 +18,32 @@ class ConsumerVersionSelectors implements Iterator, Countable private array $selectors = []; /** - * @param array $selectors + * @param array $selectors */ public function __construct(array $selectors = []) { - $this->selectors = $selectors; + $this->setSelectors($selectors); } - public function addSelector(string $selector): self + /** + * @param array $selectors + */ + public function setSelectors(array $selectors): self + { + $this->selectors = []; + foreach ($selectors as $selector) { + $this->addSelector($selector); + } + + return $this; + } + + /** + * @throws JsonException + */ + public function addSelector(string|SelectorInterface $selector): self { - $this->selectors[] = $selector; + $this->selectors[] = $selector instanceof SelectorInterface ? json_encode($selector, JSON_THROW_ON_ERROR) : $selector; return $this; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/Selector.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/Selector.php new file mode 100644 index 00000000..31910832 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/Selector.php @@ -0,0 +1,37 @@ + $value) { + if (false === $value && 'latest' !== $key) { + throw new InvalidSelectorValueException(sprintf("Value 'false' is not allowed for selector %s", $key)); + } + } + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return array_filter(get_object_vars($this), fn (null|string|bool $value) => null !== $value); + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorInterface.php new file mode 100644 index 00000000..ec3dfdd8 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorInterface.php @@ -0,0 +1,9 @@ +> + */ + public static function selectorsProvider(): array + { + return [ + [['{ "mainBranch": true }', '{ "deployedOrReleased": true }'], ['{ "mainBranch": true }', '{ "deployedOrReleased": true }']], + [[new Selector(matchingBranch: true), '{ "mainBranch": true }', new Selector(deployedOrReleased: true)], ['{"matchingBranch":true}', '{ "mainBranch": true }', '{"deployedOrReleased":true}']], + [[new Selector(mainBranch: true)], ['{"mainBranch":true}']], + ]; + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorTest.php new file mode 100644 index 00000000..d93cfd79 --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/Selector/SelectorTest.php @@ -0,0 +1,37 @@ + false]; + $this->expectException(InvalidSelectorValueException::class); + $this->expectExceptionMessage(sprintf("Value 'false' is not allowed for selector %s", $key)); + new Selector(...$values); + } + + /** + * @testWith [{ "mainBranch": true }, "{\"mainBranch\":true}"] + * [{ "branch": "feat-xxx" }, "{\"branch\":\"feat-xxx\"}"] + * [{ "deployedOrReleased": true }, "{\"deployedOrReleased\":true}"] + * [{ "matchingBranch": true }, "{\"matchingBranch\":true}"] + * [{ "mainBranch": null, "branch": "fix-yyy", "fallbackBranch": null, "matchingBranch": null, "tag": null, "fallbackTag": null, "deployed": null, "released": null, "deployedOrReleased": null, "environment": null, "latest": null, "consumer": "my-consumer" }, "{\"branch\":\"fix-yyy\",\"consumer\":\"my-consumer\"}"] + */ + public function testJsonSerialize(array $values, string $json): void + { + static::assertSame($json, json_encode(new Selector(...$values))); + } +} From 6f536850756e94883fe65da6b2d1e47e54129c48 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 16 Feb 2024 12:54:59 +0700 Subject: [PATCH 228/298] chore: Remove unused exception ConnectionException --- src/PhpPact/Exception/ConnectionException.php | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/PhpPact/Exception/ConnectionException.php diff --git a/src/PhpPact/Exception/ConnectionException.php b/src/PhpPact/Exception/ConnectionException.php deleted file mode 100644 index 1eafedb4..00000000 --- a/src/PhpPact/Exception/ConnectionException.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Fri, 16 Feb 2024 15:13:15 +0700 Subject: [PATCH 229/298] deps: Require ffi extension --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 9b2d7172..3696dde5 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ ], "require": { "php": "^8.1", + "ext-ffi": "*", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", "symfony/process": "^4.4|^5.4|^6.0", From 69dfc0a9c1395cf1858aa1915fd2c5aeb72fd524 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 16 Feb 2024 15:25:07 +0700 Subject: [PATCH 230/298] deps: Update ffi library to 0.4.16 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 9b2d7172..92b45507 100644 --- a/composer.json +++ b/composer.json @@ -109,12 +109,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.14", + "version": "0.4.16", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.14", + "version": "0.4.16", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", From d4ad1a0ef955be8c3f8bfcba5c180f16016e1aea Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 17 Feb 2024 00:45:25 +0700 Subject: [PATCH 231/298] fix: FFI > Create managed binary data --- src/PhpPact/Consumer/Model/Body/Binary.php | 18 +++++++++++++++++- .../Interaction/Body/AbstractBodyRegistry.php | 2 +- .../Body/MessageContentsRegistry.php | 2 +- src/PhpPact/FFI/Model/BinaryData.php | 2 +- .../PhpPact/Consumer/Model/Body/BinaryTest.php | 8 ++++---- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/PhpPact/Consumer/Model/Body/Binary.php b/src/PhpPact/Consumer/Model/Body/Binary.php index 44f8a914..98a0164f 100644 --- a/src/PhpPact/Consumer/Model/Body/Binary.php +++ b/src/PhpPact/Consumer/Model/Body/Binary.php @@ -10,6 +10,8 @@ class Binary { use ContentTypeTrait; + private ?BinaryData $data = null; + public function __construct(private string $path, string $contentType) { $this->setContentType($contentType); @@ -27,7 +29,16 @@ public function setPath(string $path): self return $this; } - public function createBinaryData(): BinaryData + public function getData(): BinaryData + { + if (!$this->data) { + $this->data = $this->createBinaryData(); + } + + return $this->data; + } + + private function createBinaryData(): BinaryData { if (!file_exists($this->getPath())) { throw new BinaryFileNotExistException(sprintf('File %s does not exist', $this->getPath())); @@ -39,4 +50,9 @@ public function createBinaryData(): BinaryData return BinaryData::createFrom($contents); } + + public function __destruct() + { + $this->data = null; + } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php index 317bf173..27e00873 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -24,7 +24,7 @@ public function withBody(Text|Binary|Multipart $body): void { switch (true) { case $body instanceof Binary: - $data = $body->createBinaryData(); + $data = $body->getData(); $success = $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); break; diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php index 843ebe5e..177372b6 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php @@ -25,7 +25,7 @@ public function withBody(Text|Binary|Multipart $body): void { switch (true) { case $body instanceof Binary: - $data = $body->createBinaryData(); + $data = $body->getData(); $success = $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); break; diff --git a/src/PhpPact/FFI/Model/BinaryData.php b/src/PhpPact/FFI/Model/BinaryData.php index ed77c4b0..5172b550 100644 --- a/src/PhpPact/FFI/Model/BinaryData.php +++ b/src/PhpPact/FFI/Model/BinaryData.php @@ -32,7 +32,7 @@ public static function createFrom(string $contents): self } $length = \strlen($contents); - $cData = FFI::new("uint8_t[{$length}]", false); + $cData = FFI::new("uint8_t[{$length}]"); if ($cData === null) { throw new CDataNotCreatedException(); } diff --git a/tests/PhpPact/Consumer/Model/Body/BinaryTest.php b/tests/PhpPact/Consumer/Model/Body/BinaryTest.php index e21f172b..9812b01f 100644 --- a/tests/PhpPact/Consumer/Model/Body/BinaryTest.php +++ b/tests/PhpPact/Consumer/Model/Body/BinaryTest.php @@ -24,20 +24,20 @@ public function testGetContentType(): void $this->assertSame('text/csv', $body->getContentType()); } - public function testCreateBinaryDataFromInvalidFilePath(): void + public function testGetDataFromInvalidFilePath(): void { $path = __DIR__ . '/../../../../_resources/invalid.jpg'; $body = new Binary($path, 'image/jpeg'); $this->expectException(BinaryFileNotExistException::class); $this->expectExceptionMessage("File $path does not exist"); - $body->createBinaryData(); + $body->getData(); } - public function testCreateBinaryData(): void + public function testGetData(): void { $path = __DIR__ . '/../../../../_resources/image.jpg'; $body = new Binary($path, 'image/jpeg'); - $data = $body->createBinaryData(); + $data = $body->getData(); $this->assertEquals(file_get_contents($path), (string) $data); } From 4073642879b6fe5139e1ba41aac4bcccc1da84cd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 19 Feb 2024 09:54:50 +0700 Subject: [PATCH 232/298] refactor: Move description to trait --- src/PhpPact/Consumer/Model/Interaction.php | 17 +++------------- .../Model/Interaction/DescriptionTrait.php | 20 +++++++++++++++++++ src/PhpPact/Consumer/Model/Message.php | 16 ++------------- 3 files changed, 25 insertions(+), 28 deletions(-) create mode 100644 src/PhpPact/Consumer/Model/Interaction/DescriptionTrait.php diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 5cc23cab..ec402fb0 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -2,31 +2,20 @@ namespace PhpPact\Consumer\Model; +use PhpPact\Consumer\Model\Interaction\DescriptionTrait; + /** * Request/Response Pair to be posted to the Mock Server for PACT tests. */ class Interaction { use ProviderStates; - - private string $description; + use DescriptionTrait; private ConsumerRequest $request; private ProviderResponse $response; - public function getDescription(): string - { - return $this->description; - } - - public function setDescription(string $description): self - { - $this->description = $description; - - return $this; - } - public function getRequest(): ConsumerRequest { return $this->request; diff --git a/src/PhpPact/Consumer/Model/Interaction/DescriptionTrait.php b/src/PhpPact/Consumer/Model/Interaction/DescriptionTrait.php new file mode 100644 index 00000000..dabd9a80 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/DescriptionTrait.php @@ -0,0 +1,20 @@ +description; + } + + public function setDescription(string $description): self + { + $this->description = $description; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 4ee117d8..efce790e 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -8,6 +8,7 @@ use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; +use PhpPact\Consumer\Model\Interaction\DescriptionTrait; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -15,8 +16,7 @@ class Message { use ProviderStates; - - private string $description; + use DescriptionTrait; /** * @var array @@ -25,18 +25,6 @@ class Message private Text|Binary|null $contents = null; - public function getDescription(): string - { - return $this->description; - } - - public function setDescription(string $description): self - { - $this->description = $description; - - return $this; - } - /** * @return array */ From 9a8a939436e6c420db896e9294af590d10e903ab Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 19 Feb 2024 10:53:16 +0700 Subject: [PATCH 233/298] refactor: Add InteractionPart enum --- .../Consumer/Driver/Enum/InteractionPart.php | 19 +++++++++++++++++ .../Driver/Enum/InteractionPartTest.php | 21 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/PhpPact/Consumer/Driver/Enum/InteractionPart.php create mode 100644 tests/PhpPact/Consumer/Driver/Enum/InteractionPartTest.php diff --git a/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php b/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php new file mode 100644 index 00000000..fd84af83 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php @@ -0,0 +1,19 @@ +assertTrue(InteractionPart::REQUEST->isRequest()); + $this->assertFalse(InteractionPart::RESPONSE->isRequest()); + } + + public function testIsResponse(): void + { + $this->assertFalse(InteractionPart::REQUEST->isResponse()); + $this->assertTrue(InteractionPart::RESPONSE->isResponse()); + } +} From d1560fabae8d24261c7a23c5d8d6a1492296a881 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 19 Feb 2024 16:41:17 +0700 Subject: [PATCH 234/298] refactor: Add id trait --- src/PhpPact/Consumer/Model/Interaction.php | 2 + .../Consumer/Model/Interaction/IdTrait.php | 20 ++++++++++ src/PhpPact/Consumer/Model/Message.php | 2 + .../Consumer/Model/InteractionTest.php | 39 +++++++++++++++++++ tests/PhpPact/Consumer/Model/MessageTest.php | 3 ++ 5 files changed, 66 insertions(+) create mode 100644 src/PhpPact/Consumer/Model/Interaction/IdTrait.php create mode 100644 tests/PhpPact/Consumer/Model/InteractionTest.php diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index ec402fb0..0ea33527 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -3,6 +3,7 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; +use PhpPact\Consumer\Model\Interaction\IdTrait; /** * Request/Response Pair to be posted to the Mock Server for PACT tests. @@ -11,6 +12,7 @@ class Interaction { use ProviderStates; use DescriptionTrait; + use IdTrait; private ConsumerRequest $request; diff --git a/src/PhpPact/Consumer/Model/Interaction/IdTrait.php b/src/PhpPact/Consumer/Model/Interaction/IdTrait.php new file mode 100644 index 00000000..cb4c62ef --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/IdTrait.php @@ -0,0 +1,20 @@ +id; + } + + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index efce790e..64faef71 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -9,6 +9,7 @@ use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; +use PhpPact\Consumer\Model\Interaction\IdTrait; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -17,6 +18,7 @@ class Message { use ProviderStates; use DescriptionTrait; + use IdTrait; /** * @var array diff --git a/tests/PhpPact/Consumer/Model/InteractionTest.php b/tests/PhpPact/Consumer/Model/InteractionTest.php new file mode 100644 index 00000000..e10ab6c2 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/InteractionTest.php @@ -0,0 +1,39 @@ + 'bar']; + $request = new ConsumerRequest(); + $response = new ProviderResponse(); + + $subject = (new Interaction()) + ->setId($id) + ->setDescription($description) + ->addProviderState($providerStateName, $providerStateParams) + ->setRequest($request) + ->setResponse($response); + + static::assertSame($id, $subject->getId()); + static::assertSame($description, $subject->getDescription()); + $providerStates = $subject->getProviderStates(); + static::assertCount(1, $providerStates); + static::assertContainsOnlyInstancesOf(ProviderState::class, $providerStates); + static::assertEquals($providerStateName, $providerStates[0]->getName()); + static::assertEquals($providerStateParams, $providerStates[0]->getParams()); + static::assertSame($request, $subject->getRequest()); + static::assertSame($response, $subject->getResponse()); + } +} diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index 8182706d..5b9bdf53 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -11,6 +11,7 @@ class MessageTest extends TestCase { public function testSetters() { + $id = 123; $description = 'a message'; $providerStateName = 'a provider state'; $providerStateParams = ['foo' => 'bar']; @@ -18,11 +19,13 @@ public function testSetters() $contents = 'test'; $subject = (new Message()) + ->setId($id) ->setDescription($description) ->addProviderState($providerStateName, $providerStateParams) ->setMetadata($metadata) ->setContents($contents); + static::assertSame($id, $subject->getId()); static::assertSame($description, $subject->getDescription()); $providerStates = $subject->getProviderStates(); static::assertCount(1, $providerStates); From 252562cc73bf398acdcf7111bdbc1cd1e549a47e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 20 Feb 2024 16:12:19 +0700 Subject: [PATCH 235/298] refactor: Rename (interaction) id to handle --- src/PhpPact/Consumer/Model/Interaction.php | 4 ++-- .../Model/Interaction/HandleTrait.php | 20 +++++++++++++++++++ .../Consumer/Model/Interaction/IdTrait.php | 20 ------------------- src/PhpPact/Consumer/Model/Message.php | 4 ++-- .../Consumer/Model/InteractionTest.php | 6 +++--- tests/PhpPact/Consumer/Model/MessageTest.php | 6 +++--- 6 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 src/PhpPact/Consumer/Model/Interaction/HandleTrait.php delete mode 100644 src/PhpPact/Consumer/Model/Interaction/IdTrait.php diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 0ea33527..ef34b175 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -3,7 +3,7 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; -use PhpPact\Consumer\Model\Interaction\IdTrait; +use PhpPact\Consumer\Model\Interaction\HandleTrait; /** * Request/Response Pair to be posted to the Mock Server for PACT tests. @@ -12,7 +12,7 @@ class Interaction { use ProviderStates; use DescriptionTrait; - use IdTrait; + use HandleTrait; private ConsumerRequest $request; diff --git a/src/PhpPact/Consumer/Model/Interaction/HandleTrait.php b/src/PhpPact/Consumer/Model/Interaction/HandleTrait.php new file mode 100644 index 00000000..9807ae40 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/HandleTrait.php @@ -0,0 +1,20 @@ +handle; + } + + public function setHandle(int $handle): self + { + $this->handle = $handle; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/IdTrait.php b/src/PhpPact/Consumer/Model/Interaction/IdTrait.php deleted file mode 100644 index cb4c62ef..00000000 --- a/src/PhpPact/Consumer/Model/Interaction/IdTrait.php +++ /dev/null @@ -1,20 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->id = $id; - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 64faef71..0d175c80 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -9,7 +9,7 @@ use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; -use PhpPact\Consumer\Model\Interaction\IdTrait; +use PhpPact\Consumer\Model\Interaction\HandleTrait; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -18,7 +18,7 @@ class Message { use ProviderStates; use DescriptionTrait; - use IdTrait; + use HandleTrait; /** * @var array diff --git a/tests/PhpPact/Consumer/Model/InteractionTest.php b/tests/PhpPact/Consumer/Model/InteractionTest.php index e10ab6c2..a78c1144 100644 --- a/tests/PhpPact/Consumer/Model/InteractionTest.php +++ b/tests/PhpPact/Consumer/Model/InteractionTest.php @@ -12,7 +12,7 @@ class InteractionTest extends TestCase { public function testSetters() { - $id = 123; + $handle = 123; $description = 'a message'; $providerStateName = 'a provider state'; $providerStateParams = ['foo' => 'bar']; @@ -20,13 +20,13 @@ public function testSetters() $response = new ProviderResponse(); $subject = (new Interaction()) - ->setId($id) + ->setHandle($handle) ->setDescription($description) ->addProviderState($providerStateName, $providerStateParams) ->setRequest($request) ->setResponse($response); - static::assertSame($id, $subject->getId()); + static::assertSame($handle, $subject->getHandle()); static::assertSame($description, $subject->getDescription()); $providerStates = $subject->getProviderStates(); static::assertCount(1, $providerStates); diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index 5b9bdf53..d18fd22f 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -11,7 +11,7 @@ class MessageTest extends TestCase { public function testSetters() { - $id = 123; + $handle = 123; $description = 'a message'; $providerStateName = 'a provider state'; $providerStateParams = ['foo' => 'bar']; @@ -19,13 +19,13 @@ public function testSetters() $contents = 'test'; $subject = (new Message()) - ->setId($id) + ->setHandle($handle) ->setDescription($description) ->addProviderState($providerStateName, $providerStateParams) ->setMetadata($metadata) ->setContents($contents); - static::assertSame($id, $subject->getId()); + static::assertSame($handle, $subject->getHandle()); static::assertSame($description, $subject->getDescription()); $providerStates = $subject->getProviderStates(); static::assertCount(1, $providerStates); From ef8ddfc1b09fe041cdca989b195c6bd58ab6a1c6 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 21 Feb 2024 05:45:35 +0700 Subject: [PATCH 236/298] ci(compatibility-suite): Use wip tag --- .github/workflows/compatibility-suite.yml | 2 +- compatibility-suite/pact-compatibility-suite | 2 +- compatibility-suite/suites/v3/message/provider.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index efcb7cd8..d5faac90 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -53,7 +53,7 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!Kafka|binary body \(negative|Message provider).)*$/' + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!binary body \(negative|Message provider).)*$/' v4: runs-on: ubuntu-latest steps: diff --git a/compatibility-suite/pact-compatibility-suite b/compatibility-suite/pact-compatibility-suite index db548451..416f3a64 160000 --- a/compatibility-suite/pact-compatibility-suite +++ b/compatibility-suite/pact-compatibility-suite @@ -1 +1 @@ -Subproject commit db548451c9a7515ba2adb11cb4c140fb1363f00b +Subproject commit 416f3a64d49bee977b0f404a5ba3002eae570eb3 diff --git a/compatibility-suite/suites/v3/message/provider.yml b/compatibility-suite/suites/v3/message/provider.yml index 41dbc7e9..9a5d68d5 100644 --- a/compatibility-suite/suites/v3/message/provider.yml +++ b/compatibility-suite/suites/v3/message/provider.yml @@ -22,5 +22,5 @@ default: services: PhpPactTest\CompatibilitySuite\ServiceContainer\V3 - # filters: - # tags: ~@wip + filters: + tags: ~@wip From 7dab4a9e477ad43cdeb63c4126f78d0cbc45a558 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 21 Feb 2024 18:56:42 +0700 Subject: [PATCH 237/298] refactor: Add Interaction::getBody() and Interaction::getHeaders() methods --- .../Consumer/Driver/Enum/InteractionPart.php | 10 ---- src/PhpPact/Consumer/Model/Interaction.php | 23 +++++++ .../Driver/Enum/InteractionPartTest.php | 21 ------- .../Consumer/Model/InteractionTest.php | 60 +++++++++++++++---- 4 files changed, 71 insertions(+), 43 deletions(-) delete mode 100644 tests/PhpPact/Consumer/Driver/Enum/InteractionPartTest.php diff --git a/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php b/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php index fd84af83..21d45384 100644 --- a/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php +++ b/src/PhpPact/Consumer/Driver/Enum/InteractionPart.php @@ -6,14 +6,4 @@ enum InteractionPart { case REQUEST; case RESPONSE; - - public function isRequest(): bool - { - return $this === self::REQUEST; - } - - public function isResponse(): bool - { - return $this === self::RESPONSE; - } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index ef34b175..a3445517 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -2,6 +2,10 @@ namespace PhpPact\Consumer\Model; +use PhpPact\Consumer\Driver\Enum\InteractionPart; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; @@ -41,4 +45,23 @@ public function setResponse(ProviderResponse $response): self return $this; } + + public function getBody(InteractionPart $part): Text|Binary|Multipart|null + { + return match ($part) { + InteractionPart::REQUEST => $this->getRequest()->getBody(), + InteractionPart::RESPONSE => $this->getResponse()->getBody(), + }; + } + + /** + * @return array + */ + public function getHeaders(InteractionPart $part): array + { + return match ($part) { + InteractionPart::REQUEST => $this->getRequest()->getHeaders(), + InteractionPart::RESPONSE => $this->getResponse()->getHeaders(), + }; + } } diff --git a/tests/PhpPact/Consumer/Driver/Enum/InteractionPartTest.php b/tests/PhpPact/Consumer/Driver/Enum/InteractionPartTest.php deleted file mode 100644 index 0d290c32..00000000 --- a/tests/PhpPact/Consumer/Driver/Enum/InteractionPartTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertTrue(InteractionPart::REQUEST->isRequest()); - $this->assertFalse(InteractionPart::RESPONSE->isRequest()); - } - - public function testIsResponse(): void - { - $this->assertFalse(InteractionPart::REQUEST->isResponse()); - $this->assertTrue(InteractionPart::RESPONSE->isResponse()); - } -} diff --git a/tests/PhpPact/Consumer/Model/InteractionTest.php b/tests/PhpPact/Consumer/Model/InteractionTest.php index a78c1144..92c53e67 100644 --- a/tests/PhpPact/Consumer/Model/InteractionTest.php +++ b/tests/PhpPact/Consumer/Model/InteractionTest.php @@ -2,6 +2,9 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Driver\Enum\InteractionPart; +use PhpPact\Consumer\Model\Body\Multipart; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\Interaction; use PhpPact\Consumer\Model\ProviderResponse; @@ -10,30 +13,63 @@ class InteractionTest extends TestCase { - public function testSetters() + private ConsumerRequest $request; + private ProviderResponse $response; + private Interaction $interaction; + + public function setUp(): void + { + $this->request = new ConsumerRequest(); + $this->response = new ProviderResponse(); + $this->interaction = new Interaction(); + $this->interaction->setRequest($this->request); + $this->interaction->setResponse($this->response); + } + + public function testSetters(): void { $handle = 123; $description = 'a message'; $providerStateName = 'a provider state'; $providerStateParams = ['foo' => 'bar']; - $request = new ConsumerRequest(); - $response = new ProviderResponse(); - $subject = (new Interaction()) + $this->interaction ->setHandle($handle) ->setDescription($description) - ->addProviderState($providerStateName, $providerStateParams) - ->setRequest($request) - ->setResponse($response); + ->addProviderState($providerStateName, $providerStateParams); - static::assertSame($handle, $subject->getHandle()); - static::assertSame($description, $subject->getDescription()); - $providerStates = $subject->getProviderStates(); + static::assertSame($handle, $this->interaction->getHandle()); + static::assertSame($description, $this->interaction->getDescription()); + $providerStates = $this->interaction->getProviderStates(); static::assertCount(1, $providerStates); static::assertContainsOnlyInstancesOf(ProviderState::class, $providerStates); static::assertEquals($providerStateName, $providerStates[0]->getName()); static::assertEquals($providerStateParams, $providerStates[0]->getParams()); - static::assertSame($request, $subject->getRequest()); - static::assertSame($response, $subject->getResponse()); + static::assertSame($this->request, $this->interaction->getRequest()); + static::assertSame($this->response, $this->interaction->getResponse()); + } + + public function testGetBody(): void + { + $requestBody = new Multipart([], 'abc123'); + $this->request->setBody($requestBody); + + $responseBody = new Text('example', 'text/plain'); + $this->response->setBody($responseBody); + + $this->assertSame($requestBody, $this->interaction->getBody(InteractionPart::REQUEST)); + $this->assertSame($responseBody, $this->interaction->getBody(InteractionPart::RESPONSE)); + } + + public function testGetHeaders(): void + { + $requestHeaders = ['key1' => ['value1']]; + $this->request->setHeaders($requestHeaders); + + $responseHeaders = ['key2' => ['value1', 'value2']]; + $this->response->setHeaders($responseHeaders); + + $this->assertSame($requestHeaders, $this->interaction->getHeaders(InteractionPart::REQUEST)); + $this->assertSame($responseHeaders, $this->interaction->getHeaders(InteractionPart::RESPONSE)); } } From 77cd36da59a34049e3650ff6a24a21da49839b09 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 22 Feb 2024 21:48:10 +0700 Subject: [PATCH 238/298] ci: Tune network --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0153f37e..d9112239 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,9 @@ jobs: name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v3 name: Checkout repository From 47bdd51ad73360b28dc7c714dcf099a4259b9f43 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 23 Feb 2024 07:31:13 +0700 Subject: [PATCH 239/298] ci: Run command from the action directly --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9112239..5cd2bf63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,8 +54,10 @@ jobs: name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies steps: - - name: Tune GitHub-hosted runner network - uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Tune Windows Network + if: ${{ runner.os == 'Windows' }} + shell: pwsh + run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6 - uses: actions/checkout@v3 name: Checkout repository From d1e9c638ad4c3fbdc9c46690b1dc3902b75cfed1 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 19 Feb 2024 23:40:58 +0700 Subject: [PATCH 240/298] refactor: Add body drivers --- composer.json | 3 +- .../Driver/Body/InteractionBodyDriver.php | 56 +++++ .../Body/InteractionBodyDriverInterface.php | 11 + .../Driver/Body/MessageBodyDriver.php | 38 +++ .../Body/MessageBodyDriverInterface.php | 10 + .../Plugin/Driver/Body/PluginBodyDriver.php | 50 ++++ .../Driver/Body/PluginBodyDriverInterface.php | 12 + .../Plugins/Csv/Driver/Body/CsvBodyDriver.php | 20 ++ .../Driver/Body/ProtobufMessageBodyDriver.php | 20 ++ .../Driver/Body/InteractionBodyDriverTest.php | 196 +++++++++++++++ .../Driver/Body/MessageBodyDriverTest.php | 81 ++++++ .../Driver/Body/PluginBodyDriverTest.php | 232 ++++++++++++++++++ .../Csv/Driver/Body/CsvBodyDriverTest.php | 36 +++ .../Body/ProtobufMessageBodyDriverTest.php | 36 +++ 14 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Body/InteractionBodyDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Body/MessageBodyDriverInterface.php create mode 100644 src/PhpPact/Plugin/Driver/Body/PluginBodyDriver.php create mode 100644 src/PhpPact/Plugin/Driver/Body/PluginBodyDriverInterface.php create mode 100644 src/PhpPact/Plugins/Csv/Driver/Body/CsvBodyDriver.php create mode 100644 src/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriver.php create mode 100644 tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php create mode 100644 tests/PhpPact/Consumer/Driver/Body/MessageBodyDriverTest.php create mode 100644 tests/PhpPact/Plugin/Driver/Body/PluginBodyDriverTest.php create mode 100644 tests/PhpPact/Plugins/Csv/Driver/Body/CsvBodyDriverTest.php create mode 100644 tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php diff --git a/composer.json b/composer.json index 21f36849..4f250d09 100644 --- a/composer.json +++ b/composer.json @@ -34,11 +34,12 @@ "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9|^10.1|^11", + "phpunit/phpunit": "^10.1|^11", "guzzlehttp/guzzle": "^7.8", "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", "ramsey/uuid": "^4.7", + "mikey179/vfsstream": "^1.6.7", "pact-foundation/example-protobuf-sync-message-provider": "@dev" }, "autoload": { diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php new file mode 100644 index 00000000..1dcfa4b0 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php @@ -0,0 +1,56 @@ +getBody($interactionPart); + $partId = match ($interactionPart) { + InteractionPart::REQUEST => $this->client->get('InteractionPart_Request'), + InteractionPart::RESPONSE => $this->client->get('InteractionPart_Response'), + }; + switch (true) { + case $body instanceof Binary: + $data = $body->getData(); + $success = $this->client->call('pactffi_with_binary_file', $interaction->getHandle(), $partId, $body->getContentType(), $data->getValue(), $data->getSize()); + break; + + case $body instanceof Text: + $success = $this->client->call('pactffi_with_body', $interaction->getHandle(), $partId, $body->getContentType(), $body->getContents()); + break; + + case $body instanceof Multipart: + foreach ($body->getParts() as $part) { + $result = $this->client->call('pactffi_with_multipart_file_v2', $interaction->getHandle(), $partId, $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); + if (isset($result->failed) && $result->failed instanceof CData) { + throw new PartNotAddedException(FFI::string($result->failed)); + } + } + $success = true; + break; + + default: + break; + }; + if (isset($success) && false === $success) { + throw new InteractionBodyNotAddedException(); + } + } +} diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriverInterface.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriverInterface.php new file mode 100644 index 00000000..49f797b5 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriverInterface.php @@ -0,0 +1,11 @@ +getContents(); + $partId = $this->client->get('InteractionPart_Request'); + switch (true) { + case $body instanceof Binary: + $data = $body->getData(); + $success = $this->client->call('pactffi_with_binary_file', $message->getHandle(), $partId, $body->getContentType(), $data->getValue(), $data->getSize()); + break; + + case $body instanceof Text: + $success = $this->client->call('pactffi_with_body', $message->getHandle(), $partId, $body->getContentType(), $body->getContents()); + break; + + default: + break; + }; + if (isset($success) && false === $success) { + throw new MessageContentsNotAddedException(); + } + } +} diff --git a/src/PhpPact/Consumer/Driver/Body/MessageBodyDriverInterface.php b/src/PhpPact/Consumer/Driver/Body/MessageBodyDriverInterface.php new file mode 100644 index 00000000..5ebd774d --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Body/MessageBodyDriverInterface.php @@ -0,0 +1,10 @@ +getContents() : $interaction->getBody($interactionPart); + $partId = $interaction instanceof Message ? $this->client->get('InteractionPart_Request') : match ($interactionPart) { + InteractionPart::REQUEST => $this->client->get('InteractionPart_Request'), + InteractionPart::RESPONSE => $this->client->get('InteractionPart_Response'), + }; + switch (true) { + case $body instanceof Binary: + throw new BodyNotSupportedException('Plugin does not support binary body'); + + case $body instanceof Text: + json_decode($body->getContents()); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BodyNotSupportedException('Plugin only support json body contents'); + } + $error = $this->client->call('pactffi_interaction_contents', $interaction->getHandle(), $partId, $body->getContentType(), $body->getContents()); + if ($error) { + throw new PluginBodyNotAddedException($error); + } + break; + + case $body instanceof Multipart: + throw new BodyNotSupportedException('Plugin does not support multipart body'); + + default: + break; + }; + } +} diff --git a/src/PhpPact/Plugin/Driver/Body/PluginBodyDriverInterface.php b/src/PhpPact/Plugin/Driver/Body/PluginBodyDriverInterface.php new file mode 100644 index 00000000..5ca75854 --- /dev/null +++ b/src/PhpPact/Plugin/Driver/Body/PluginBodyDriverInterface.php @@ -0,0 +1,12 @@ +pluginBodyDriver->registerBody($interaction, $part); + } +} diff --git a/src/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriver.php b/src/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriver.php new file mode 100644 index 00000000..65b1c446 --- /dev/null +++ b/src/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriver.php @@ -0,0 +1,20 @@ +pluginBodyDriver->registerBody($message, InteractionPart::REQUEST); + } +} diff --git a/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php new file mode 100644 index 00000000..32a85a07 --- /dev/null +++ b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php @@ -0,0 +1,196 @@ +root = vfsStream::setup(); + file_put_contents($this->root->url() . '/id.txt', 'text'); + file_put_contents($this->root->url() . '/address.json', 'json'); + file_put_contents($this->root->url() . '/image.png', 'image'); + + $this->client = $this->createMock(ClientInterface::class); + $this->client + ->expects($this->once()) + ->method('get') + ->willReturnMap([ + ['InteractionPart_Request', $this->requestPartId], + ['InteractionPart_Response', $this->responsePartId], + ]); + $this->driver = new InteractionBodyDriver($this->client); + $this->interaction = new Interaction(); + $this->interaction->setHandle($this->interactionHandle); + $this->interaction->setRequest(new ConsumerRequest()); + $this->interaction->setResponse(new ProviderResponse()); + $this->binary = new Binary(__DIR__ . '/../../../../_resources/image.jpg', 'image/jpeg'); + $this->text = new Text('example', 'text/plain'); + $this->parts = [ + new Part($this->root->url() . '/id.txt', 'id', 'text/plain'), + new Part($this->root->url() . '/address.json', 'address', 'application/json'), + new Part($this->root->url() . '/image.png', 'profileImage', 'image/png'), + ]; + $this->multipart = new Multipart($this->parts, $this->boundary); + $this->failed = FFI::new('char[5]'); + FFI::memcpy($this->failed, $this->message, 5); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testRequestBinaryBody(bool $success): void + { + $data = $this->binary->getData(); + $this->interaction->getRequest()->setBody($this->binary); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_binary_file', $this->interactionHandle, $this->requestPartId, $this->binary->getContentType(), $data->getValue(), $data->getSize()) + ->willReturn($success); + if (!$success) { + $this->expectException(InteractionBodyNotAddedException::class); + } + $this->driver->registerBody($this->interaction, InteractionPart::REQUEST); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testResponseBinaryBody(bool $success): void + { + $data = $this->binary->getData(); + $this->interaction->getResponse()->setBody($this->binary); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_binary_file', $this->interactionHandle, $this->responsePartId, $this->binary->getContentType(), $data->getValue(), $data->getSize()) + ->willReturn($success); + if (!$success) { + $this->expectException(InteractionBodyNotAddedException::class); + } + $this->driver->registerBody($this->interaction, InteractionPart::RESPONSE); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testRequestTextBody(bool $success): void + { + $this->interaction->getRequest()->setBody($this->text); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_body', $this->interactionHandle, $this->requestPartId, $this->text->getContentType(), $this->text->getContents()) + ->willReturn($success); + if (!$success) { + $this->expectException(InteractionBodyNotAddedException::class); + } + $this->driver->registerBody($this->interaction, InteractionPart::REQUEST); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testResponseTextBody(bool $success): void + { + $this->interaction->getResponse()->setBody($this->text); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_body', $this->interactionHandle, $this->responsePartId, $this->text->getContentType(), $this->text->getContents()) + ->willReturn($success); + if (!$success) { + $this->expectException(InteractionBodyNotAddedException::class); + } + $this->driver->registerBody($this->interaction, InteractionPart::RESPONSE); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testRequestMultipartBody(bool $success): void + { + $this->interaction->getRequest()->setBody($this->multipart); + $this->client + ->expects($this->exactly(count($this->parts))) + ->method('call') + ->willReturnCallback( + fn (string $method, int $interactionId, int $partId, string $contentType, string $path, string $name, string $boundary) => + match([$method, $interactionId, $partId, $contentType, $path, $name, $boundary]) { + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) [], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) [], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) ($success ? [] : ['failed' => $this->failed]), + } + ); + if (!$success) { + $this->expectException(PartNotAddedException::class); + $this->expectExceptionMessage($this->message); + } + $this->driver->registerBody($this->interaction, InteractionPart::REQUEST); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testResponseMultipartBody(bool $success): void + { + $this->interaction->getResponse()->setBody($this->multipart); + $this->client + ->expects($this->exactly(count($this->parts))) + ->method('call') + ->willReturnCallback( + fn (string $method, int $interactionId, int $partId, string $contentType, string $path, string $name, string $boundary) => + match([$method, $interactionId, $partId, $contentType, $path, $name, $boundary]) { + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) [], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) [], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) ($success ? [] : ['failed' => $this->failed]), + } + ); + if (!$success) { + $this->expectException(PartNotAddedException::class); + $this->expectExceptionMessage($this->message); + } + $this->driver->registerBody($this->interaction, InteractionPart::RESPONSE); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testEmptyBody(InteractionPart $part): void + { + $this->client + ->expects($this->never()) + ->method('call'); + $this->driver->registerBody($this->interaction, $part); + } +} diff --git a/tests/PhpPact/Consumer/Driver/Body/MessageBodyDriverTest.php b/tests/PhpPact/Consumer/Driver/Body/MessageBodyDriverTest.php new file mode 100644 index 00000000..8535edfd --- /dev/null +++ b/tests/PhpPact/Consumer/Driver/Body/MessageBodyDriverTest.php @@ -0,0 +1,81 @@ +client = $this->createMock(ClientInterface::class); + $this->driver = new MessageBodyDriver($this->client); + $this->client + ->expects($this->once()) + ->method('get') + ->with('InteractionPart_Request') + ->willReturn($this->requestPartId); + $this->message = new Message(); + $this->message->setHandle($this->messageId); + $this->binary = new Binary(__DIR__ . '/../../../../_resources/image.jpg', 'image/jpeg'); + $this->text = new Text('example', 'text/plain'); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testMessageBinaryBody(bool $success): void + { + $data = $this->binary->getData(); + $this->message->setContents($this->binary); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_binary_file', $this->messageId, $this->requestPartId, $this->binary->getContentType(), $data->getValue(), $data->getSize()) + ->willReturn($success); + if (!$success) { + $this->expectException(MessageContentsNotAddedException::class); + } + $this->driver->registerBody($this->message); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testMessageTextBody(bool $success): void + { + $this->message->setContents($this->text); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_with_body', $this->messageId, $this->requestPartId, $this->text->getContentType(), $this->text->getContents()) + ->willReturn($success); + if (!$success) { + $this->expectException(MessageContentsNotAddedException::class); + } + $this->driver->registerBody($this->message); + } + + public function testEmptyBody(): void + { + $this->client + ->expects($this->never()) + ->method('call'); + $this->driver->registerBody($this->message); + } +} diff --git a/tests/PhpPact/Plugin/Driver/Body/PluginBodyDriverTest.php b/tests/PhpPact/Plugin/Driver/Body/PluginBodyDriverTest.php new file mode 100644 index 00000000..7bd4322b --- /dev/null +++ b/tests/PhpPact/Plugin/Driver/Body/PluginBodyDriverTest.php @@ -0,0 +1,232 @@ +client = $this->createMock(ClientInterface::class); + $this->client + ->expects($this->once()) + ->method('get') + ->willReturnMap([ + ['InteractionPart_Request', $this->requestPartId], + ['InteractionPart_Response', $this->responsePartId], + ]); + $this->driver = new PluginBodyDriver($this->client); + $this->interaction = new Interaction(); + $this->interaction->setHandle($this->interactionId); + $this->interaction->setRequest(new ConsumerRequest()); + $this->interaction->setResponse(new ProviderResponse()); + $this->message = new Message(); + $this->message->setHandle($this->messageId); + $this->binary = new Binary(__DIR__ . '/../../../../_resources/image.jpg', 'image/jpeg'); + $this->text = new Text('example', 'text/plain'); + $this->json = new Text('{}', 'application/json'); + $this->multipart = new Multipart([], 'abcde12345'); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testInteractionBinaryBody(InteractionPart $part): void + { + if ($part === InteractionPart::REQUEST) { + $this->interaction->getRequest()->setBody($this->binary); + } else { + $this->interaction->getResponse()->setBody($this->binary); + } + $this->client + ->expects($this->never()) + ->method('call'); + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Plugin does not support binary body'); + $this->driver->registerBody($this->interaction, $part); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testMessageBinaryBody(InteractionPart $part): void + { + $this->message->setContents($this->binary); + $this->client + ->expects($this->never()) + ->method('call'); + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Plugin does not support binary body'); + $this->driver->registerBody($this->message, $part); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testInteractionPlainTextBody(InteractionPart $part): void + { + if ($part === InteractionPart::REQUEST) { + $this->interaction->getRequest()->setBody($this->text); + } else { + $this->interaction->getResponse()->setBody($this->text); + } + $this->client + ->expects($this->never()) + ->method('call'); + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Plugin only support json body contents'); + $this->driver->registerBody($this->interaction, $part); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testMessagePlainTextBody(InteractionPart $part): void + { + $this->message->setContents($this->text); + $this->client + ->expects($this->never()) + ->method('call'); + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Plugin only support json body contents'); + $this->driver->registerBody($this->message, $part); + } + + private function getPluginBodyErrorMessage(int $error): string + { + return match ($error) { + 1 => 'A general panic was caught.', + 2 => 'The mock server has already been started.', + 3 => 'The interaction handle is invalid.', + 4 => 'The content type is not valid.', + 5 => 'The contents JSON is not valid JSON.', + 6 => 'The plugin returned an error.', + default => 'Unknown error', + }; + } + + #[DataProvider('errorProvider')] + public function testRequestJsonBody(int $error): void + { + $this->interaction->getRequest()->setBody($this->json); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_interaction_contents', $this->interactionId, $this->requestPartId, $this->json->getContentType(), $this->json->getContents()) + ->willReturn($error); + if ($error) { + $this->expectException(PluginBodyNotAddedException::class); + $this->expectExceptionMessage($this->getPluginBodyErrorMessage($error)); + } + $this->driver->registerBody($this->interaction, InteractionPart::REQUEST); + } + + #[DataProvider('errorProvider')] + public function testResponseJsonBody(int $error): void + { + $this->interaction->getResponse()->setBody($this->json); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_interaction_contents', $this->interactionId, $this->responsePartId, $this->json->getContentType(), $this->json->getContents()) + ->willReturn($error); + if ($error) { + $this->expectException(PluginBodyNotAddedException::class); + $this->expectExceptionMessage($this->getPluginBodyErrorMessage($error)); + } + $this->driver->registerBody($this->interaction, InteractionPart::RESPONSE); + } + + #[DataProvider('errorProvider')] + public function testMessageJsonBody(int $error): void + { + $this->message->setContents($this->json); + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_interaction_contents', $this->messageId, $this->requestPartId, $this->json->getContentType(), $this->json->getContents()) + ->willReturn($error); + if ($error) { + $this->expectException(PluginBodyNotAddedException::class); + $this->expectExceptionMessage($this->getPluginBodyErrorMessage($error)); + } + $this->driver->registerBody($this->message, InteractionPart::REQUEST); + } + + public static function errorProvider(): array + { + return [ + [0], + [1], + [2], + [3], + [4], + [5], + [6], + [7], + ]; + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testInteractionMultipartBody(InteractionPart $part): void + { + if ($part === InteractionPart::REQUEST) { + $this->interaction->getRequest()->setBody($this->multipart); + } else { + $this->interaction->getResponse()->setBody($this->multipart); + } + $this->client + ->expects($this->never()) + ->method('call'); + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Plugin does not support multipart body'); + $this->driver->registerBody($this->interaction, $part); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testEmptyInteractionBody(InteractionPart $part): void + { + $this->client + ->expects($this->never()) + ->method('call'); + $this->driver->registerBody($this->interaction, $part); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testEmptyMessageBody(InteractionPart $part): void + { + $this->client + ->expects($this->never()) + ->method('call'); + $this->driver->registerBody($this->message, $part); + } +} diff --git a/tests/PhpPact/Plugins/Csv/Driver/Body/CsvBodyDriverTest.php b/tests/PhpPact/Plugins/Csv/Driver/Body/CsvBodyDriverTest.php new file mode 100644 index 00000000..b51ad365 --- /dev/null +++ b/tests/PhpPact/Plugins/Csv/Driver/Body/CsvBodyDriverTest.php @@ -0,0 +1,36 @@ +decorated = $this->createMock(PluginBodyDriverInterface::class); + $this->driver = new CsvBodyDriver($this->decorated); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testRegisterBody(InteractionPart $part): void + { + $interaction = new Interaction(); + $this->decorated + ->expects($this->once()) + ->method('registerBody') + ->with($interaction, $part); + $this->driver->registerBody($interaction, $part); + } +} diff --git a/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php b/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php new file mode 100644 index 00000000..4c72a0b2 --- /dev/null +++ b/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php @@ -0,0 +1,36 @@ +decorated = $this->createMock(PluginBodyDriverInterface::class); + $this->driver = new ProtobufMessageBodyDriver($this->decorated); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + public function testRegisterBody(InteractionPart $part): void + { + $message = new Message(); + $this->decorated + ->expects($this->once()) + ->method('registerBody') + ->with($message, InteractionPart::REQUEST); + $this->driver->registerBody($message); + } +} From 5952c78abb7c155e8a0c556fafa416bbf4407038 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 20 Feb 2024 23:13:53 +0700 Subject: [PATCH 241/298] refactor: Add interaction part drivers --- .../AbstractInteractionPartDriver.php | 44 +++++++++++ .../Driver/InteractionPart/RequestDriver.php | 37 +++++++++ .../RequestDriverInterface.php | 10 +++ .../Driver/InteractionPart/ResponseDriver.php | 24 ++++++ .../ResponseDriverInterface.php | 10 +++ .../InteractionPart/RequestDriverTest.php | 76 +++++++++++++++++++ .../InteractionPart/ResponseDriverTest.php | 66 ++++++++++++++++ 7 files changed, 267 insertions(+) create mode 100644 src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionPart/RequestDriverInterface.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriver.php create mode 100644 src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverInterface.php create mode 100644 tests/PhpPact/Consumer/Driver/InteractionPart/RequestDriverTest.php create mode 100644 tests/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverTest.php diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php b/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php new file mode 100644 index 00000000..3381a53e --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php @@ -0,0 +1,44 @@ +bodyDriver = $bodyDriver ?? new InteractionBodyDriver($client); + } + + protected function withBody(Interaction $interaction, InteractionPart $part): self + { + $this->bodyDriver->registerBody($interaction, $part); + + return $this; + } + + protected function withHeaders(Interaction $interaction, InteractionPart $interactionPart): self + { + $headers = $interaction->getHeaders($interactionPart); + $partId = match ($interactionPart) { + InteractionPart::REQUEST => $this->client->get('InteractionPart_Request'), + InteractionPart::RESPONSE => $this->client->get('InteractionPart_Response'), + }; + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_header_v2', $interaction->getHandle(), $partId, (string) $header, (int) $index, (string) $value); + } + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php new file mode 100644 index 00000000..9f79c609 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php @@ -0,0 +1,37 @@ +withRequest($interaction) + ->withQueryParameters($interaction) + ->withHeaders($interaction, InteractionPart::REQUEST) + ->withBody($interaction, InteractionPart::REQUEST); + } + + private function withQueryParameters(Interaction $interaction): self + { + foreach ($interaction->getRequest()->getQuery() as $key => $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_query_parameter_v2', $interaction->getHandle(), (string) $key, (int) $index, (string) $value); + } + } + + return $this; + } + + private function withRequest(Interaction $interaction): self + { + $request = $interaction->getRequest(); + $this->client->call('pactffi_with_request', $interaction->getHandle(), $request->getMethod(), $request->getPath()); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriverInterface.php b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriverInterface.php new file mode 100644 index 00000000..2cc6d04c --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriverInterface.php @@ -0,0 +1,10 @@ +withResponse($interaction) + ->withHeaders($interaction, InteractionPart::RESPONSE) + ->withBody($interaction, InteractionPart::RESPONSE); + } + + private function withResponse(Interaction $interaction): self + { + $this->client->call('pactffi_response_status_v2', $interaction->getHandle(), $interaction->getResponse()->getStatus()); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverInterface.php b/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverInterface.php new file mode 100644 index 00000000..8fd9eebf --- /dev/null +++ b/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverInterface.php @@ -0,0 +1,10 @@ + ['query-value-1', 'query-value-2'], + 'query2' => ['query-value-3'], + ]; + private array $headers = [ + 'header1' => ['header-value-1'], + 'header2' => ['header-value-2', 'header-value-3'], + ]; + + public function setUp(): void + { + $this->client = $this->createMock(ClientInterface::class); + $this->bodyDriver = $this->createMock(InteractionBodyDriverInterface::class); + $this->driver = new RequestDriver($this->client, $this->bodyDriver); + $this->interaction = new Interaction(); + $this->interaction->setHandle($this->interactionHandle); + $request = new ConsumerRequest(); + $request->setMethod($this->method); + $request->setPath($this->path); + $request->setQuery($this->query); + $request->setHeaders($this->headers); + $this->interaction->setRequest($request); + } + + public function testRegisterRequest(): void + { + $this->client + ->expects($this->once()) + ->method('get') + ->with('InteractionPart_Request') + ->willReturn($this->requestPartId); + $this->client + ->expects($this->exactly(7)) + ->method('call') + ->willReturnCallback( + fn (...$args) => match($args) { + ['pactffi_with_request', $this->interactionHandle, $this->method, $this->path] => null, + ['pactffi_with_query_parameter_v2', $this->interactionHandle, 'query1', 0, 'query-value-1'] => null, + ['pactffi_with_query_parameter_v2', $this->interactionHandle, 'query1', 1, 'query-value-2'] => null, + ['pactffi_with_query_parameter_v2', $this->interactionHandle, 'query2', 0, 'query-value-3'] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->requestPartId, 'header1', 0, 'header-value-1'] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->requestPartId, 'header2', 0, 'header-value-2'] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->requestPartId, 'header2', 1, 'header-value-3'] => null, + } + ); + $this->bodyDriver + ->expects($this->once()) + ->method('registerBody') + ->with($this->interaction, InteractionPart::REQUEST); + $this->driver->registerRequest($this->interaction); + } +} diff --git a/tests/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverTest.php b/tests/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverTest.php new file mode 100644 index 00000000..fb2a1575 --- /dev/null +++ b/tests/PhpPact/Consumer/Driver/InteractionPart/ResponseDriverTest.php @@ -0,0 +1,66 @@ + ['header-value-1'], + 'header2' => ['header-value-2', 'header-value-3'], + ]; + + public function setUp(): void + { + $this->client = $this->createMock(ClientInterface::class); + $this->bodyDriver = $this->createMock(InteractionBodyDriverInterface::class); + $this->driver = new ResponseDriver($this->client, $this->bodyDriver); + $this->interaction = new Interaction(); + $this->interaction->setHandle($this->interactionHandle); + $response = new ProviderResponse(); + $response->setStatus($this->status); + $response->setHeaders($this->headers); + $this->interaction->setResponse($response); + } + + public function testRegisterResponse(): void + { + $this->client + ->expects($this->once()) + ->method('get') + ->with('InteractionPart_Response') + ->willReturn($this->responsePartId); + $this->client + ->expects($this->exactly(4)) + ->method('call') + ->willReturnCallback( + fn (...$args) => match($args) { + ['pactffi_response_status_v2', $this->interactionHandle, $this->status] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->responsePartId, 'header1', 0, 'header-value-1'] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->responsePartId, 'header2', 0, 'header-value-2'] => null, + ['pactffi_with_header_v2', $this->interactionHandle, $this->responsePartId, 'header2', 1, 'header-value-3'] => null, + } + ); + $this->bodyDriver + ->expects($this->once()) + ->method('registerBody') + ->with($this->interaction, InteractionPart::RESPONSE); + $this->driver->registerResponse($this->interaction); + } +} From 0b15197db3799a5aae28688c8ce707f98ec9bd92 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 25 Feb 2024 08:53:27 +0700 Subject: [PATCH 242/298] fix: Fix error 'Cannot use object of type FFI\CData as array' --- .../Consumer/Driver/Body/InteractionBodyDriver.php | 2 +- .../Driver/Body/InteractionBodyDriverTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php index 1dcfa4b0..c8f36525 100644 --- a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php @@ -39,7 +39,7 @@ public function registerBody(Interaction $interaction, InteractionPart $interact case $body instanceof Multipart: foreach ($body->getParts() as $part) { $result = $this->client->call('pactffi_with_multipart_file_v2', $interaction->getHandle(), $partId, $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); - if (isset($result->failed) && $result->failed instanceof CData) { + if ($result->failed instanceof CData) { throw new PartNotAddedException(FFI::string($result->failed)); } } diff --git a/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php index 32a85a07..c13503e6 100644 --- a/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php @@ -149,9 +149,9 @@ public function testRequestMultipartBody(bool $success): void ->willReturnCallback( fn (string $method, int $interactionId, int $partId, string $contentType, string $path, string $name, string $boundary) => match([$method, $interactionId, $partId, $contentType, $path, $name, $boundary]) { - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) [], - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) [], - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) ($success ? [] : ['failed' => $this->failed]), + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) ['failed' => null], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) ['failed' => null], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) (['failed' => $success ? null : $this->failed]), } ); if (!$success) { @@ -172,9 +172,9 @@ public function testResponseMultipartBody(bool $success): void ->willReturnCallback( fn (string $method, int $interactionId, int $partId, string $contentType, string $path, string $name, string $boundary) => match([$method, $interactionId, $partId, $contentType, $path, $name, $boundary]) { - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) [], - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) [], - ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) ($success ? [] : ['failed' => $this->failed]), + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary] => (object) ['failed' => null], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary] => (object) ['failed' => null], + ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary] => (object) (['failed' => $success ? null : $this->failed]), } ); if (!$success) { From 152eceb815601fc9fd1604ef7b22b7f0e18e7ae1 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 25 Feb 2024 16:46:00 +0700 Subject: [PATCH 243/298] refactor: Remove $this return value --- .../AbstractInteractionPartDriver.php | 8 ++------ .../Driver/InteractionPart/RequestDriver.php | 17 ++++++----------- .../Driver/InteractionPart/ResponseDriver.php | 13 ++++++------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php b/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php index 3381a53e..59cb2417 100644 --- a/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php +++ b/src/PhpPact/Consumer/Driver/InteractionPart/AbstractInteractionPartDriver.php @@ -19,14 +19,12 @@ public function __construct( $this->bodyDriver = $bodyDriver ?? new InteractionBodyDriver($client); } - protected function withBody(Interaction $interaction, InteractionPart $part): self + protected function withBody(Interaction $interaction, InteractionPart $part): void { $this->bodyDriver->registerBody($interaction, $part); - - return $this; } - protected function withHeaders(Interaction $interaction, InteractionPart $interactionPart): self + protected function withHeaders(Interaction $interaction, InteractionPart $interactionPart): void { $headers = $interaction->getHeaders($interactionPart); $partId = match ($interactionPart) { @@ -38,7 +36,5 @@ protected function withHeaders(Interaction $interaction, InteractionPart $intera $this->client->call('pactffi_with_header_v2', $interaction->getHandle(), $partId, (string) $header, (int) $index, (string) $value); } } - - return $this; } } diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php index 9f79c609..7f25fb2e 100644 --- a/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php +++ b/src/PhpPact/Consumer/Driver/InteractionPart/RequestDriver.php @@ -9,29 +9,24 @@ class RequestDriver extends AbstractInteractionPartDriver implements RequestDriv { public function registerRequest(Interaction $interaction): void { - $this - ->withRequest($interaction) - ->withQueryParameters($interaction) - ->withHeaders($interaction, InteractionPart::REQUEST) - ->withBody($interaction, InteractionPart::REQUEST); + $this->withBody($interaction, InteractionPart::REQUEST); + $this->withHeaders($interaction, InteractionPart::REQUEST); + $this->withQueryParameters($interaction); + $this->withRequest($interaction); } - private function withQueryParameters(Interaction $interaction): self + private function withQueryParameters(Interaction $interaction): void { foreach ($interaction->getRequest()->getQuery() as $key => $values) { foreach (array_values($values) as $index => $value) { $this->client->call('pactffi_with_query_parameter_v2', $interaction->getHandle(), (string) $key, (int) $index, (string) $value); } } - - return $this; } - private function withRequest(Interaction $interaction): self + private function withRequest(Interaction $interaction): void { $request = $interaction->getRequest(); $this->client->call('pactffi_with_request', $interaction->getHandle(), $request->getMethod(), $request->getPath()); - - return $this; } } diff --git a/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriver.php b/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriver.php index 74e7de54..29677de7 100644 --- a/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriver.php +++ b/src/PhpPact/Consumer/Driver/InteractionPart/ResponseDriver.php @@ -9,16 +9,15 @@ class ResponseDriver extends AbstractInteractionPartDriver implements ResponseDr { public function registerResponse(Interaction $interaction): void { - $this - ->withResponse($interaction) - ->withHeaders($interaction, InteractionPart::RESPONSE) - ->withBody($interaction, InteractionPart::RESPONSE); + // @todo Fix 'Exception: String could not be parsed as XML' in xml's consumer test + // when calling `withBody` before `withHeaders` + $this->withHeaders($interaction, InteractionPart::RESPONSE); + $this->withBody($interaction, InteractionPart::RESPONSE); + $this->withResponse($interaction); } - private function withResponse(Interaction $interaction): self + private function withResponse(Interaction $interaction): void { $this->client->call('pactffi_response_status_v2', $interaction->getHandle(), $interaction->getResponse()->getStatus()); - - return $this; } } From e876d5e81329206cde75837d925298dabfffb009 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 27 Feb 2024 22:24:37 +0700 Subject: [PATCH 244/298] refactor: Merge PactDriver and PactRegistry --- .../tests/Service/PactWriter.php | 7 +- compatibility-suite/tests/Service/Server.php | 9 +- .../tests/Service/SyncMessagePactWriter.php | 7 +- .../Driver/Exception/DriverException.php | 9 + .../Driver/Exception/MissingPactException.php | 7 + .../Driver/Interaction/InteractionDriver.php | 3 - .../Driver/Interaction/MessageDriver.php | 1 - .../Consumer/Driver/Pact/PactDriver.php | 55 ++++-- .../Driver/Pact/PactDriverInterface.php | 6 +- .../Factory/InteractionDriverFactory.php | 10 +- .../Consumer/Factory/MessageDriverFactory.php | 6 +- src/PhpPact/Consumer/Model/Pact/Pact.php | 10 ++ .../Registry/Interaction/AbstractRegistry.php | 4 +- .../Interaction/InteractionRegistry.php | 8 +- .../Registry/Interaction/MessageRegistry.php | 8 +- .../Consumer/Registry/Pact/PactRegistry.php | 50 ------ .../Registry/Pact/PactRegistryInterface.php | 12 -- src/PhpPact/Consumer/Service/MockServer.php | 8 +- .../Driver/Pact/AbstractPluginPactDriver.php | 7 +- .../Factory/CsvInteractionDriverFactory.php | 10 +- .../Interaction/CsvInteractionRegistry.php | 6 +- .../Factory/ProtobufMessageDriverFactory.php | 6 +- .../ProtobufSyncMessageDriverFactory.php | 10 +- .../Interaction/ProtobufMessageRegistry.php | 6 +- .../ProtobufSyncMessageRegistry.php | 6 +- .../Driver/Interaction/SyncMessageDriver.php | 3 - .../Factory/SyncMessageDriverFactory.php | 10 +- .../Interaction/SyncMessageRegistry.php | 2 +- .../Consumer/Driver/Pact/PactDriverTest.php | 169 ++++++++++++++++++ .../Pact/AbstractPluginPactDriverTestCase.php | 57 ++++++ .../Csv/Driver/Pact/CsvPactDriverTest.php | 19 ++ .../Driver/Pact/ProtobufPactDriverTest.php | 19 ++ 32 files changed, 385 insertions(+), 165 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Exception/DriverException.php create mode 100644 src/PhpPact/Consumer/Driver/Exception/MissingPactException.php create mode 100644 src/PhpPact/Consumer/Model/Pact/Pact.php delete mode 100644 src/PhpPact/Consumer/Registry/Pact/PactRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php create mode 100644 tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php create mode 100644 tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php create mode 100644 tests/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriverTest.php create mode 100644 tests/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriverTest.php diff --git a/compatibility-suite/tests/Service/PactWriter.php b/compatibility-suite/tests/Service/PactWriter.php index e56a2a04..e0cc230a 100644 --- a/compatibility-suite/tests/Service/PactWriter.php +++ b/compatibility-suite/tests/Service/PactWriter.php @@ -5,7 +5,6 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPactTest\CompatibilitySuite\Constant\Path; @@ -29,12 +28,10 @@ public function write(int $id, PactPath $pactPath, string $mode = PactConfigInte ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new PactDriver($client, $config, $pactRegistry); - $interactionRegistry = new InteractionRegistry($client, $pactRegistry); + $pactDriver = new PactDriver($client, $config); + $interactionRegistry = new InteractionRegistry($client, $pactDriver); $interaction = $this->storage->get(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id); - $pactDriver->setUp(); $interactionRegistry->registerInteraction($interaction); $pactDriver->writePact(); $pactDriver->cleanUp(); diff --git a/compatibility-suite/tests/Service/Server.php b/compatibility-suite/tests/Service/Server.php index 6f7d90cf..885cd2cd 100644 --- a/compatibility-suite/tests/Service/Server.php +++ b/compatibility-suite/tests/Service/Server.php @@ -7,7 +7,6 @@ use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\Consumer\Service\MockServer; use PhpPact\Consumer\Service\MockServerInterface; use PhpPact\FFI\Client; @@ -45,16 +44,14 @@ public function __construct( $this->logger = new Logger(); $client = new Client(); - $pactRegistry = new PactRegistry($client); - $this->pactDriver = new PactDriver($client, $this->config, $pactRegistry); - $this->mockServer = new MockServer($client, $pactRegistry, $this->config, $this->logger); - $this->interactionRegistry = new InteractionRegistry($client, $pactRegistry); + $this->pactDriver = new PactDriver($client, $this->config); + $this->mockServer = new MockServer($client, $this->pactDriver, $this->config, $this->logger); + $this->interactionRegistry = new InteractionRegistry($client, $this->pactDriver); } public function register(int ...$ids): void { $interactions = array_map(fn (int $id) => $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id), $ids); - $this->pactDriver->setUp(); foreach ($interactions as $interaction) { $this->interactionRegistry->registerInteraction($interaction); } diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriter.php b/compatibility-suite/tests/Service/SyncMessagePactWriter.php index f7b2b923..1ef9a9d9 100644 --- a/compatibility-suite/tests/Service/SyncMessagePactWriter.php +++ b/compatibility-suite/tests/Service/SyncMessagePactWriter.php @@ -5,7 +5,6 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Model\Message; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPact\SyncMessage\Registry\Interaction\SyncMessageRegistry; @@ -29,11 +28,9 @@ public function write(Message $message, PactPath $pactPath, string $mode = PactC ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new PactDriver($client, $config, $pactRegistry); - $messageRegistry = new SyncMessageRegistry($client, $pactRegistry); + $pactDriver = new PactDriver($client, $config); + $messageRegistry = new SyncMessageRegistry($client, $pactDriver); - $pactDriver->setUp(); $messageRegistry->registerMessage($message); $pactDriver->writePact(); $pactDriver->cleanUp(); diff --git a/src/PhpPact/Consumer/Driver/Exception/DriverException.php b/src/PhpPact/Consumer/Driver/Exception/DriverException.php new file mode 100644 index 00000000..3b8b4932 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Exception/DriverException.php @@ -0,0 +1,9 @@ +pactDriver->setUp(); $this->interactionRegistry->registerInteraction($interaction); $this->mockServer->start(); diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php index 6ba771ff..80a30661 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php @@ -31,7 +31,6 @@ public function writePactAndCleanUp(): bool public function registerMessage(Message $message): void { - $this->pactDriver->setUp(); $this->messageRegistry->registerMessage($message); } } diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index 427d81ba..a7baf923 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -4,29 +4,35 @@ use Composer\Semver\Comparator; use PhpPact\Config\PactConfigInterface; +use PhpPact\Consumer\Driver\Exception\MissingPactException; use PhpPact\Consumer\Exception\PactFileNotWroteException; -use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; +use PhpPact\Consumer\Model\Pact\Pact; use PhpPact\FFI\ClientInterface; class PactDriver implements PactDriverInterface { + protected ?Pact $pact = null; + public function __construct( protected ClientInterface $client, - protected PactConfigInterface $config, - protected PactRegistryInterface $pactRegistry + protected PactConfigInterface $config ) { + $this->setUp(); } public function cleanUp(): void { - $this->pactRegistry->deletePact(); + $this->validatePact(); + $this->client->call('pactffi_free_pact_handle', $this->pact->handle); + $this->pact = null; } public function writePact(): void { + $this->validatePact(); $error = $this->client->call( 'pactffi_pact_handle_write_file', - $this->pactRegistry->getId(), + $this->pact->handle, $this->config->getPactDir(), $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); @@ -35,11 +41,18 @@ public function writePact(): void } } - public function setUp(): void + public function getPact(): Pact { - $this - ->initWithLogLevel() - ->registerPact(); + $this->validatePact(); + + return $this->pact; + } + + protected function setUp(): void + { + $this->initWithLogLevel(); + $this->newPact(); + $this->withSpecification(); } protected function getSpecification(): int @@ -58,29 +71,33 @@ protected function getSpecification(): int }; } + protected function validatePact(): void + { + if (!$this->pact) { + throw new MissingPactException(); + } + } + private function versionEqualTo(string $version): bool { return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); } - private function initWithLogLevel(): self + private function initWithLogLevel(): void { $logLevel = $this->config->getLogLevel(); if ($logLevel) { $this->client->call('pactffi_init_with_log_level', $logLevel); } - - return $this; } - private function registerPact(): self + private function newPact(): void { - $this->pactRegistry->registerPact( - $this->config->getConsumer(), - $this->config->getProvider(), - $this->getSpecification() - ); + $this->pact = new Pact($this->client->call('pactffi_new_pact', $this->config->getConsumer(), $this->config->getProvider())); + } - return $this; + private function withSpecification(): void + { + $this->client->call('pactffi_with_specification', $this->pact->handle, $this->getSpecification()); } } diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php index 69ccdc42..729d1140 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php @@ -2,11 +2,13 @@ namespace PhpPact\Consumer\Driver\Pact; +use PhpPact\Consumer\Model\Pact\Pact; + interface PactDriverInterface { - public function setUp(): void; - public function cleanUp(): void; public function writePact(): void; + + public function getPact(): Pact; } diff --git a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php index f911c5fc..18a7d83e 100644 --- a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php @@ -6,7 +6,6 @@ use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; @@ -16,11 +15,10 @@ class InteractionDriverFactory implements InteractionDriverFactoryInterface public function create(MockServerConfigInterface $config): InteractionDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new PactDriver($client, $config, $pactRegistry); - $mockServer = new MockServer($client, $pactRegistry, $config); - $interactionRegistry = new InteractionRegistry($client, $pactRegistry); + $pactDriver = new PactDriver($client, $config); + $mockServer = new MockServer($client, $pactDriver, $config); + $interactionRegistry = new InteractionRegistry($client, $pactDriver); - return new InteractionDriver($pactDriver, $interactionRegistry, $mockServer); + return new InteractionDriver($interactionRegistry, $mockServer); } } diff --git a/src/PhpPact/Consumer/Factory/MessageDriverFactory.php b/src/PhpPact/Consumer/Factory/MessageDriverFactory.php index af8a5dac..e8a90ac0 100644 --- a/src/PhpPact/Consumer/Factory/MessageDriverFactory.php +++ b/src/PhpPact/Consumer/Factory/MessageDriverFactory.php @@ -7,7 +7,6 @@ use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Registry\Interaction\MessageRegistry; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; class MessageDriverFactory implements MessageDriverFactoryInterface @@ -15,9 +14,8 @@ class MessageDriverFactory implements MessageDriverFactoryInterface public function create(PactConfigInterface $config): MessageDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new PactDriver($client, $config, $pactRegistry); - $messageRegistry = new MessageRegistry($client, $pactRegistry); + $pactDriver = new PactDriver($client, $config); + $messageRegistry = new MessageRegistry($client, $pactDriver); return new MessageDriver($client, $pactDriver, $messageRegistry); } diff --git a/src/PhpPact/Consumer/Model/Pact/Pact.php b/src/PhpPact/Consumer/Model/Pact/Pact.php new file mode 100644 index 00000000..dfe3ec75 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Pact/Pact.php @@ -0,0 +1,10 @@ +requestRegistry = $requestRegistry ?? new RequestRegistry($client, $this); $this->responseRegistry = $responseRegistry ?? new ResponseRegistry($client, $this); } @@ -43,7 +43,7 @@ public function registerInteraction(Interaction $interaction): bool protected function newInteraction(string $description): self { - $this->id = $this->client->call('pactffi_new_interaction', $this->pactRegistry->getId(), $description); + $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getPact()->handle, $description); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index fa1f693b..021fab3f 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -2,13 +2,13 @@ namespace PhpPact\Consumer\Registry\Interaction; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; use PhpPact\Consumer\Registry\Interaction\Body\MessageContentsRegistry; -use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; class MessageRegistry extends AbstractRegistry implements MessageRegistryInterface @@ -17,10 +17,10 @@ class MessageRegistry extends AbstractRegistry implements MessageRegistryInterfa public function __construct( ClientInterface $client, - PactRegistryInterface $pactRegistry, + PactDriverInterface $pactDriver, ?BodyRegistryInterface $messageContentsRegistry = null ) { - parent::__construct($client, $pactRegistry); + parent::__construct($client, $pactDriver); $this->messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); } @@ -37,7 +37,7 @@ public function registerMessage(Message $message): void protected function newInteraction(string $description): self { - $this->id = $this->client->call('pactffi_new_message_interaction', $this->pactRegistry->getId(), $description); + $this->id = $this->client->call('pactffi_new_message_interaction', $this->pactDriver->getPact()->handle, $description); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Pact/PactRegistry.php b/src/PhpPact/Consumer/Registry/Pact/PactRegistry.php deleted file mode 100644 index 138b4d1c..00000000 --- a/src/PhpPact/Consumer/Registry/Pact/PactRegistry.php +++ /dev/null @@ -1,50 +0,0 @@ -id)) { - throw new PactNotRegisteredException('New pact must be registered.'); - } - return $this->id; - } - - public function deletePact(): void - { - $this->client->call('pactffi_free_pact_handle', $this->id); - unset($this->id); - } - - public function registerPact(string $consumer, string $provider, int $specification): void - { - $this - ->newPact($consumer, $provider) - ->withSpecification($specification); - } - - private function newPact(string $consumer, string $provider): self - { - $this->id = $this->client->call('pactffi_new_pact', $consumer, $provider); - - return $this; - } - - private function withSpecification(int $specification): self - { - $this->client->call('pactffi_with_specification', $this->id, $specification); - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php b/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php deleted file mode 100644 index c9ffb883..00000000 --- a/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -client->call( 'pactffi_create_mock_server_for_transport', - $this->pactRegistry->getId(), + $this->pactDriver->getPact()->handle, $this->config->getHost(), $this->config->getPort(), $this->getTransport(), @@ -81,7 +81,7 @@ private function writePact(): void private function cleanUp(): void { $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); - $this->pactRegistry->deletePact(); + $this->pactDriver->cleanUp(); } private function isMatched(): bool diff --git a/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php index 75d490d7..7844f5a3 100644 --- a/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php +++ b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php @@ -9,11 +9,12 @@ abstract class AbstractPluginPactDriver extends PactDriver { public function cleanUp(): void { - $this->client->call('pactffi_cleanup_plugins', $this->pactRegistry->getId()); + $this->validatePact(); + $this->client->call('pactffi_cleanup_plugins', $this->pact->handle); parent::cleanUp(); } - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->usingPlugin(); @@ -32,7 +33,7 @@ private function usingPlugin(): self throw new PluginNotSupportedBySpecificationException($this->config->getPactSpecificationVersion()); } - $this->client->call('pactffi_using_plugin', $this->pactRegistry->getId(), $this->getPluginName(), $this->getPluginVersion()); + $this->client->call('pactffi_using_plugin', $this->pact->handle, $this->getPluginName(), $this->getPluginVersion()); return $this; } diff --git a/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php b/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php index 1546638b..d9d9cb0e 100644 --- a/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php +++ b/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php @@ -5,7 +5,6 @@ use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Factory\InteractionDriverFactoryInterface; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; @@ -17,11 +16,10 @@ class CsvInteractionDriverFactory implements InteractionDriverFactoryInterface public function create(MockServerConfigInterface $config): InteractionDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new CsvPactDriver($client, $config, $pactRegistry); - $mockServer = new MockServer($client, $pactRegistry, $config); - $interactionRegistry = new CsvInteractionRegistry($client, $pactRegistry); + $pactDriver = new CsvPactDriver($client, $config); + $mockServer = new MockServer($client, $pactDriver, $config); + $interactionRegistry = new CsvInteractionRegistry($client, $pactDriver); - return new InteractionDriver($pactDriver, $interactionRegistry, $mockServer); + return new InteractionDriver($interactionRegistry, $mockServer); } } diff --git a/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php b/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php index 615d50c7..2801470f 100644 --- a/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php +++ b/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php @@ -2,11 +2,11 @@ namespace PhpPact\Plugins\Csv\Registry\Interaction; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; use PhpPact\Consumer\Registry\Interaction\Part\RequestRegistryInterface; use PhpPact\Consumer\Registry\Interaction\Part\ResponseRegistry; use PhpPact\Consumer\Registry\Interaction\Part\ResponseRegistryInterface; -use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; use PhpPact\Plugins\Csv\Registry\Interaction\Body\CsvResponseBodyRegistry; @@ -14,11 +14,11 @@ class CsvInteractionRegistry extends InteractionRegistry { public function __construct( ClientInterface $client, - PactRegistryInterface $pactRegistry, + PactDriverInterface $pactDriver, ?RequestRegistryInterface $requestRegistry = null, ?ResponseRegistryInterface $responseRegistry = null, ) { $responseRegistry = $responseRegistry ?? new ResponseRegistry($client, $this, new CsvResponseBodyRegistry($client, $this)); - parent::__construct($client, $pactRegistry, $requestRegistry, $responseRegistry); + parent::__construct($client, $pactDriver, $requestRegistry, $responseRegistry); } } diff --git a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php index b07f1d58..8bb3dc87 100644 --- a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php +++ b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php @@ -6,7 +6,6 @@ use PhpPact\Consumer\Driver\Interaction\MessageDriver; use PhpPact\Consumer\Driver\Interaction\MessageDriverInterface; use PhpPact\Consumer\Factory\MessageDriverFactoryInterface; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Plugins\Protobuf\Driver\Pact\ProtobufPactDriver; use PhpPact\Plugins\Protobuf\Registry\Interaction\ProtobufMessageRegistry; @@ -16,9 +15,8 @@ class ProtobufMessageDriverFactory implements MessageDriverFactoryInterface public function create(PactConfigInterface $config): MessageDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new ProtobufPactDriver($client, $config, $pactRegistry); - $messageRegistry = new ProtobufMessageRegistry($client, $pactRegistry); + $pactDriver = new ProtobufPactDriver($client, $config); + $messageRegistry = new ProtobufMessageRegistry($client, $pactDriver); return new MessageDriver($client, $pactDriver, $messageRegistry); } diff --git a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php index 79ee8eea..00ebba0b 100644 --- a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php +++ b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php @@ -2,7 +2,6 @@ namespace PhpPact\Plugins\Protobuf\Factory; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\FFI\Client; use PhpPact\Plugins\Protobuf\Driver\Pact\ProtobufPactDriver; use PhpPact\Plugins\Protobuf\Registry\Interaction\ProtobufSyncMessageRegistry; @@ -17,11 +16,10 @@ class ProtobufSyncMessageDriverFactory implements SyncMessageDriverFactoryInterf public function create(MockServerConfigInterface $config): SyncMessageDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new ProtobufPactDriver($client, $config, $pactRegistry); - $grpcMockServer = new GrpcMockServer($client, $pactRegistry, $config); - $syncMessageRegistry = new ProtobufSyncMessageRegistry($client, $pactRegistry); + $pactDriver = new ProtobufPactDriver($client, $config); + $grpcMockServer = new GrpcMockServer($client, $pactDriver, $config); + $syncMessageRegistry = new ProtobufSyncMessageRegistry($client, $pactDriver); - return new SyncMessageDriver($pactDriver, $syncMessageRegistry, $grpcMockServer); + return new SyncMessageDriver($syncMessageRegistry, $grpcMockServer); } } diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php index e8134b81..2a3ac0f3 100644 --- a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php +++ b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php @@ -2,9 +2,9 @@ namespace PhpPact\Plugins\Protobuf\Registry\Interaction; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; use PhpPact\Consumer\Registry\Interaction\MessageRegistry; -use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; use PhpPact\Plugins\Protobuf\Registry\Interaction\Body\ProtobufMessageContentsRegistry; @@ -12,9 +12,9 @@ class ProtobufMessageRegistry extends MessageRegistry { public function __construct( ClientInterface $client, - PactRegistryInterface $pactRegistry, + PactDriverInterface $pactDriver, ?BodyRegistryInterface $messageContentsRegistry = null ) { - parent::__construct($client, $pactRegistry, $messageContentsRegistry ?? new ProtobufMessageContentsRegistry($client, $this)); + parent::__construct($client, $pactDriver, $messageContentsRegistry ?? new ProtobufMessageContentsRegistry($client, $this)); } } diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php index fca59408..aa32d8a5 100644 --- a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php +++ b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php @@ -2,8 +2,8 @@ namespace PhpPact\Plugins\Protobuf\Registry\Interaction; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; -use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; use PhpPact\Plugins\Protobuf\Registry\Interaction\Body\ProtobufMessageContentsRegistry; use PhpPact\SyncMessage\Registry\Interaction\SyncMessageRegistry; @@ -12,9 +12,9 @@ class ProtobufSyncMessageRegistry extends SyncMessageRegistry { public function __construct( ClientInterface $client, - PactRegistryInterface $pactRegistry, + PactDriverInterface $pactDriver, ?BodyRegistryInterface $messageContentsRegistry = null ) { - parent::__construct($client, $pactRegistry, $messageContentsRegistry ?? new ProtobufMessageContentsRegistry($client, $this)); + parent::__construct($client, $pactDriver, $messageContentsRegistry ?? new ProtobufMessageContentsRegistry($client, $this)); } } diff --git a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php index a0260adc..f46735bb 100644 --- a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php +++ b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriver.php @@ -2,7 +2,6 @@ namespace PhpPact\SyncMessage\Driver\Interaction; -use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Registry\Interaction\MessageRegistryInterface; use PhpPact\Consumer\Service\MockServerInterface; @@ -10,7 +9,6 @@ class SyncMessageDriver implements SyncMessageDriverInterface { public function __construct( - private PactDriverInterface $pactDriver, private MessageRegistryInterface $messageRegistry, private MockServerInterface $mockServer ) { @@ -23,7 +21,6 @@ public function verifyMessage(): bool public function registerMessage(Message $message): void { - $this->pactDriver->setUp(); $this->messageRegistry->registerMessage($message); $this->mockServer->start(); diff --git a/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php b/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php index 8b9c116b..49ed97ca 100644 --- a/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php +++ b/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php @@ -3,7 +3,6 @@ namespace PhpPact\SyncMessage\Factory; use PhpPact\Consumer\Driver\Pact\PactDriver; -use PhpPact\Consumer\Registry\Pact\PactRegistry; use PhpPact\Consumer\Service\MockServer; use PhpPact\FFI\Client; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriver; @@ -16,11 +15,10 @@ class SyncMessageDriverFactory implements SyncMessageDriverFactoryInterface public function create(MockServerConfigInterface $config): SyncMessageDriverInterface { $client = new Client(); - $pactRegistry = new PactRegistry($client); - $pactDriver = new PactDriver($client, $config, $pactRegistry); - $messageRegistry = new SyncMessageRegistry($client, $pactRegistry); - $mockServer = new MockServer($client, $pactRegistry, $config); + $pactDriver = new PactDriver($client, $config); + $messageRegistry = new SyncMessageRegistry($client, $pactDriver); + $mockServer = new MockServer($client, $pactDriver, $config); - return new SyncMessageDriver($pactDriver, $messageRegistry, $mockServer); + return new SyncMessageDriver($messageRegistry, $mockServer); } } diff --git a/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php b/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php index 1f22d653..364747c2 100644 --- a/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php +++ b/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php @@ -9,7 +9,7 @@ class SyncMessageRegistry extends MessageRegistry { protected function newInteraction(string $description): self { - $this->id = $this->client->call('pactffi_new_sync_message_interaction', $this->pactRegistry->getId(), $description); + $this->id = $this->client->call('pactffi_new_sync_message_interaction', $this->pactDriver->getPact()->handle, $description); return $this; } diff --git a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php new file mode 100644 index 00000000..9dfd54a9 --- /dev/null +++ b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php @@ -0,0 +1,169 @@ +client = $this->createMock(ClientInterface::class); + $this->config = $this->createMock(PactConfigInterface::class); + $this->client + ->expects($this->any()) + ->method('get') + ->willReturnMap([ + ['PactSpecification_Unknown', self::SPEC_UNKNOWN], + ['PactSpecification_V1', self::SPEC_V1], + ['PactSpecification_V1_1', self::SPEC_V1_1], + ['PactSpecification_V2', self::SPEC_V2], + ['PactSpecification_V3', self::SPEC_V3], + ['PactSpecification_V4', self::SPEC_V4], + ]); + } + + #[TestWith([null , '1.0.0', self::SPEC_V1])] + #[TestWith(['trace', '1.1.0', self::SPEC_V1_1])] + #[TestWith(['debug', '2.0.0', self::SPEC_V2])] + #[TestWith(['info' , '3.0.0', self::SPEC_V3])] + #[TestWith(['warn' , '4.0.0', self::SPEC_V4])] + #[TestWith(['error', '1.0.0', self::SPEC_V1])] + #[TestWith(['off' , '1.1.0', self::SPEC_V1_1])] + #[TestWith(['none' , '2.0.0', self::SPEC_V2])] + public function testSetUp(?string $logLevel, string $version, int $specificationHandle): void + { + $this->assertConfig($logLevel, $version); + $calls = $logLevel ? [ + ['pactffi_init_with_log_level', $logLevel, null], + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, $specificationHandle, null], + ] : [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, $specificationHandle, null], + ]; + $this->assertClientCalls($calls); + $this->driver = new PactDriver($this->client, $this->config); + $this->assertSame($this->pactHandle, $this->driver->getPact()->handle); + } + + #[TestWith([false, false])] + #[TestWith([true, false])] + #[TestWith([false, true])] + public function testCleanUp(bool $getPactAfterCleanUp, bool $cleanUpAfterCleanUp): void + { + $this->assertConfig(null, '1.0.0'); + $calls = [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, self::SPEC_V1, null], + ['pactffi_free_pact_handle', $this->pactHandle, null], + ]; + $this->assertClientCalls($calls); + $this->driver = new PactDriver($this->client, $this->config); + $this->driver->cleanUp(); + if ($getPactAfterCleanUp || $cleanUpAfterCleanUp) { + $this->expectException(MissingPactException::class); + if ($getPactAfterCleanUp) { + $this->driver->getPact(); + } else { + $this->driver->cleanUp(); + } + } + } + + #[TestWith([0, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([1, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([2, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([3, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([4, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([0, PactConfigInterface::MODE_MERGE])] + #[TestWith([1, PactConfigInterface::MODE_MERGE])] + #[TestWith([2, PactConfigInterface::MODE_MERGE])] + #[TestWith([3, PactConfigInterface::MODE_MERGE])] + #[TestWith([4, PactConfigInterface::MODE_MERGE])] + public function testWritePact(int $error, string $writeMode): void + { + $this->assertConfig(null, '1.0.0'); + $this->config + ->expects($this->once()) + ->method('getPactDir') + ->willReturn($this->pactDir); + $this->config + ->expects($this->once()) + ->method('getPactFileWriteMode') + ->willReturn($writeMode); + $calls = [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, self::SPEC_V1, null], + ['pactffi_pact_handle_write_file', $this->pactHandle, $this->pactDir, $writeMode === PactConfigInterface::MODE_OVERWRITE, $error], + ]; + $this->assertClientCalls($calls); + $this->driver = new PactDriver($this->client, $this->config); + if ($error) { + $this->expectException(PactFileNotWroteException::class); + $this->expectExceptionMessage(match ($error) { + 1 => 'The function panicked.', + 2 => 'The pact file was not able to be written.', + 3 => 'The pact for the given handle was not found.', + default => 'Unknown error', + }); + } + $this->driver->writePact(); + } + + protected function assertConfig(?string $logLevel, string $version): void + { + $this->config + ->expects($this->once()) + ->method('getLogLevel') + ->willReturn($logLevel); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn($version); + $this->config + ->expects($this->once()) + ->method('getConsumer') + ->willReturn($this->consumer); + $this->config + ->expects($this->once()) + ->method('getProvider') + ->willReturn($this->provider); + } + + protected function assertClientCalls(array $calls): void + { + $this->client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + } +} diff --git a/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php b/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php new file mode 100644 index 00000000..ebdaf972 --- /dev/null +++ b/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php @@ -0,0 +1,57 @@ +assertConfig(null, $version); + $calls = $supported ? [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, $specificationHandle, null], + ['pactffi_using_plugin', $this->pactHandle, $this->getPluginName(), null, null], + ] : [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, $specificationHandle, null], + ]; + $this->assertClientCalls($calls); + if (!$supported) { + $this->expectException(PluginNotSupportedBySpecificationException::class); + $this->expectExceptionMessage(sprintf( + 'Plugin is not supported by specification %s, use 4.0.0 or above', + $version, + )); + } + $this->driver = $this->createPactDriver(); + } + + public function testCleanUpPlugin(): void + { + $this->assertConfig(null, '4.0.0'); + $calls = [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, self::SPEC_V4, null], + ['pactffi_using_plugin', $this->pactHandle, $this->getPluginName(), null, null], + ['pactffi_cleanup_plugins', $this->pactHandle, null], + ['pactffi_free_pact_handle', $this->pactHandle, null], + ]; + $this->assertClientCalls($calls); + $this->driver = $this->createPactDriver(); + $this->driver->cleanUp(); + } + + abstract protected function createPactDriver(): AbstractPluginPactDriver; + + abstract protected function getPluginName(): string; +} diff --git a/tests/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriverTest.php b/tests/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriverTest.php new file mode 100644 index 00000000..dfa26d9f --- /dev/null +++ b/tests/PhpPact/Plugins/Csv/Driver/Pact/CsvPactDriverTest.php @@ -0,0 +1,19 @@ +client, $this->config); + } + + protected function getPluginName(): string + { + return 'csv'; + } +} diff --git a/tests/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriverTest.php b/tests/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriverTest.php new file mode 100644 index 00000000..ccb321eb --- /dev/null +++ b/tests/PhpPact/Plugins/Protobuf/Driver/Pact/ProtobufPactDriverTest.php @@ -0,0 +1,19 @@ +client, $this->config); + } + + protected function getPluginName(): string + { + return 'protobuf'; + } +} From 2ad9974c330fa61c9cb0b8403016a94f1d88e4ee Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 28 Feb 2024 16:19:09 +0700 Subject: [PATCH 245/298] refactor: Merge InteractionRegistry and InteractionDriver --- .../tests/Model/VerifyResult.php | 2 +- .../tests/Service/PactWriter.php | 13 +- compatibility-suite/tests/Service/Server.php | 32 ++--- .../Driver/Interaction/DriverInterface.php | 8 ++ .../Driver/Interaction/InteractionDriver.php | 70 ++++++++++- .../InteractionDriverInterface.php | 8 +- .../Factory/InteractionDriverFactory.php | 4 +- src/PhpPact/Consumer/InteractionBuilder.php | 2 +- .../Interaction/Body/AbstractBodyRegistry.php | 51 -------- .../Interaction/Body/RequestBodyRegistry.php | 10 -- .../Interaction/Body/ResponseBodyRegistry.php | 10 -- .../Interaction/InteractionRegistry.php | 93 -------------- .../InteractionRegistryInterface.php | 10 -- .../Interaction/Part/AbstractPartRegistry.php | 47 -------- .../Part/PartRegistryInterface.php | 17 --- .../Interaction/Part/RequestRegistry.php | 39 ------ .../Part/RequestRegistryInterface.php | 13 -- .../Interaction/Part/ResponseRegistry.php | 28 ----- .../Part/ResponseRegistryInterface.php | 8 -- src/PhpPact/Consumer/Service/MockServer.php | 17 ++- .../Consumer/Service/MockServerInterface.php | 8 +- .../Factory/CsvInteractionDriverFactory.php | 9 +- .../Body/CsvResponseBodyRegistry.php | 25 ---- .../Interaction/CsvInteractionRegistry.php | 24 ---- .../MockService/Model/VerifyResult.php | 10 ++ .../Driver/Interaction/SyncMessageDriver.php | 3 +- .../SyncMessageDriverInterface.php | 3 +- .../SyncMessage/SyncMessageBuilder.php | 2 +- .../Interaction/InteractionDriverTest.php | 113 ++++++++++++++++++ 29 files changed, 242 insertions(+), 437 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/ResponseBodyRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php delete mode 100644 src/PhpPact/Plugins/Csv/Registry/Interaction/Body/CsvResponseBodyRegistry.php delete mode 100644 src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php create mode 100644 src/PhpPact/Standalone/MockService/Model/VerifyResult.php create mode 100644 tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php diff --git a/compatibility-suite/tests/Model/VerifyResult.php b/compatibility-suite/tests/Model/VerifyResult.php index 899c93f8..a0e59583 100644 --- a/compatibility-suite/tests/Model/VerifyResult.php +++ b/compatibility-suite/tests/Model/VerifyResult.php @@ -4,7 +4,7 @@ class VerifyResult { - public function __construct(private bool $success, private string $output) + public function __construct(private readonly bool $success, private readonly string $output) { } diff --git a/compatibility-suite/tests/Service/PactWriter.php b/compatibility-suite/tests/Service/PactWriter.php index e0cc230a..f4580229 100644 --- a/compatibility-suite/tests/Service/PactWriter.php +++ b/compatibility-suite/tests/Service/PactWriter.php @@ -3,9 +3,7 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Driver\Pact\PactDriver; -use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; -use PhpPact\FFI\Client; +use PhpPact\Consumer\Factory\InteractionDriverFactory; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPactTest\CompatibilitySuite\Constant\Path; use PhpPactTest\CompatibilitySuite\Model\PactPath; @@ -27,13 +25,10 @@ public function write(int $id, PactPath $pactPath, string $mode = PactConfigInte ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); - $client = new Client(); - $pactDriver = new PactDriver($client, $config); - $interactionRegistry = new InteractionRegistry($client, $pactDriver); + $driver = (new InteractionDriverFactory())->create($config); $interaction = $this->storage->get(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id); - $interactionRegistry->registerInteraction($interaction); - $pactDriver->writePact(); - $pactDriver->cleanUp(); + $driver->registerInteraction($interaction); + $driver->writePactAndCleanUp(); } } diff --git a/compatibility-suite/tests/Service/Server.php b/compatibility-suite/tests/Service/Server.php index 885cd2cd..dab539a3 100644 --- a/compatibility-suite/tests/Service/Server.php +++ b/compatibility-suite/tests/Service/Server.php @@ -3,17 +3,11 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Driver\Pact\PactDriver; -use PhpPact\Consumer\Driver\Pact\PactDriverInterface; -use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; -use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; -use PhpPact\Consumer\Service\MockServer; -use PhpPact\Consumer\Service\MockServerInterface; -use PhpPact\FFI\Client; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\Consumer\Factory\InteractionDriverFactory; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPactTest\CompatibilitySuite\Constant\Path; -use PhpPactTest\CompatibilitySuite\Model\Logger; use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Model\VerifyResult; use Psr\Http\Message\UriInterface; @@ -21,11 +15,8 @@ final class Server implements ServerInterface { private MockServerConfigInterface $config; - private PactDriverInterface $pactDriver; - private InteractionRegistryInterface $interactionRegistry; - private MockServerInterface $mockServer; + private InteractionDriverInterface $driver; private VerifyResult $verifyResult; - private Logger $logger; private PactPath $pactPath; public function __construct( @@ -41,21 +32,16 @@ public function __construct( ->setPactSpecificationVersion($specificationVersion) ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); - $this->logger = new Logger(); - - $client = new Client(); - $this->pactDriver = new PactDriver($client, $this->config); - $this->mockServer = new MockServer($client, $this->pactDriver, $this->config, $this->logger); - $this->interactionRegistry = new InteractionRegistry($client, $this->pactDriver); + $this->driver = (new InteractionDriverFactory())->create($this->config); } public function register(int ...$ids): void { $interactions = array_map(fn (int $id) => $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id), $ids); - foreach ($interactions as $interaction) { - $this->interactionRegistry->registerInteraction($interaction); + foreach ($interactions as $index => $interaction) { + $startMockServer = $index === count($interactions) - 1; + $this->driver->registerInteraction($interaction, $startMockServer); } - $this->mockServer->start(); } public function getBaseUri(): UriInterface @@ -65,8 +51,8 @@ public function getBaseUri(): UriInterface public function verify(): void { - $success = $this->mockServer->verify(); - $this->verifyResult = new VerifyResult($success, !$success ? $this->logger->getOutput() : ''); + $result = $this->driver->verifyInteractions(); + $this->verifyResult = new VerifyResult($result->matched, $result->mismatches); } public function getVerifyResult(): VerifyResult diff --git a/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php new file mode 100644 index 00000000..d1e2c80a --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/DriverInterface.php @@ -0,0 +1,8 @@ +requestDriver = $requestDriver ?? new RequestDriver($client); + $this->responseDriver = $responseDriver ?? new ResponseDriver($client); } - public function verifyInteractions(): bool + public function verifyInteractions(): VerifyResult { return $this->mockServer->verify(); } - public function registerInteraction(Interaction $interaction): bool + public function registerInteraction(Interaction $interaction, bool $startMockServer = true): bool { - $this->interactionRegistry->registerInteraction($interaction); - $this->mockServer->start(); + $this->newInteraction($interaction); + $this->given($interaction); + $this->uponReceiving($interaction); + $this->withRequest($interaction); + $this->willRespondWith($interaction); + + if ($startMockServer) { + $this->mockServer->start(); + } return true; } + + public function writePactAndCleanUp(): void + { + $this->mockServer->writePact(); + $this->mockServer->cleanUp(); + } + + protected function newInteraction(Interaction $interaction): void + { + $handle = $this->client->call('pactffi_new_interaction', $this->pactDriver->getPact()->handle, $interaction->getDescription()); + $interaction->setHandle($handle); + } + + private function uponReceiving(Interaction $interaction): void + { + $this->client->call('pactffi_upon_receiving', $interaction->getHandle(), $interaction->getDescription()); + } + + private function given(Interaction $interaction): void + { + foreach ($interaction->getProviderStates() as $providerState) { + $this->client->call('pactffi_given', $interaction->getHandle(), $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_given_with_param', $interaction->getHandle(), $providerState->getName(), (string) $key, (string) $value); + } + } + } + + private function withRequest(Interaction $interaction): void + { + $this->requestDriver->registerRequest($interaction); + } + + private function willRespondWith(Interaction $interaction): void + { + $this->responseDriver->registerResponse($interaction); + } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php index fe5a3e07..5b592473 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -4,9 +4,11 @@ use PhpPact\Consumer\Model\Interaction; -interface InteractionDriverInterface +use PhpPact\Standalone\MockService\Model\VerifyResult; + +interface InteractionDriverInterface extends DriverInterface { - public function registerInteraction(Interaction $interaction): bool; + public function registerInteraction(Interaction $interaction, bool $startMockServer = true): bool; - public function verifyInteractions(): bool; + public function verifyInteractions(): VerifyResult; } diff --git a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php index 18a7d83e..61859c6d 100644 --- a/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php +++ b/src/PhpPact/Consumer/Factory/InteractionDriverFactory.php @@ -5,7 +5,6 @@ use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Driver\Pact\PactDriver; -use PhpPact\Consumer\Registry\Interaction\InteractionRegistry; use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; @@ -17,8 +16,7 @@ public function create(MockServerConfigInterface $config): InteractionDriverInte $client = new Client(); $pactDriver = new PactDriver($client, $config); $mockServer = new MockServer($client, $pactDriver, $config); - $interactionRegistry = new InteractionRegistry($client, $pactDriver); - return new InteractionDriver($interactionRegistry, $mockServer); + return new InteractionDriver($client, $mockServer, $pactDriver); } } diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 000a08ae..a09c1202 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -73,6 +73,6 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->driver->verifyInteractions(); + return $this->driver->verifyInteractions()->matched; } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php deleted file mode 100644 index 27e00873..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php +++ /dev/null @@ -1,51 +0,0 @@ -getData(); - $success = $this->client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); - break; - - case $body instanceof Text: - $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); - break; - - case $body instanceof Multipart: - foreach ($body->getParts() as $part) { - $result = $this->client->call('pactffi_with_multipart_file_v2', $this->interactionRegistry->getId(), $this->getPart(), $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); - if ($result->failed instanceof CData) { - throw new PartNotAddedException(FFI::string($result->failed)); - } - } - $success = true; - break; - }; - if (!isset($success) || !$success) { - throw new InteractionBodyNotAddedException(); - } - } - - abstract protected function getPart(): int; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php deleted file mode 100644 index f1266404..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php +++ /dev/null @@ -1,10 +0,0 @@ -requestRegistry = $requestRegistry ?? new RequestRegistry($client, $this); - $this->responseRegistry = $responseRegistry ?? new ResponseRegistry($client, $this); - } - - public function registerInteraction(Interaction $interaction): bool - { - $this - ->newInteraction($interaction->getDescription()) - ->given($interaction->getProviderStates()) - ->uponReceiving($interaction->getDescription()) - ->with($interaction->getRequest()) - ->willRespondWith($interaction->getResponse()); - - return true; - } - - protected function newInteraction(string $description): self - { - $this->id = $this->client->call('pactffi_new_interaction', $this->pactDriver->getPact()->handle, $description); - - return $this; - } - - private function uponReceiving(string $description): self - { - $this->client->call('pactffi_upon_receiving', $this->id, $description); - - return $this; - } - - /** - * @param ProviderState[] $providerStates - */ - private function given(array $providerStates): self - { - foreach ($providerStates as $providerState) { - $this->client->call('pactffi_given', $this->id, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); - } - } - - return $this; - } - - private function with(ConsumerRequest $request): self - { - $this->requestRegistry - ->withRequest($request->getMethod(), $request->getPath()) - ->withQueryParameters($request->getQuery()) - ->withHeaders($request->getHeaders()) - ->withBody($request->getBody()); - - return $this; - } - - private function willRespondWith(ProviderResponse $response): self - { - $this->responseRegistry - ->withResponse($response->getStatus()) - ->withHeaders($response->getHeaders()) - ->withBody($response->getBody()); - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php deleted file mode 100644 index c3f1c7fc..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -bodyRegistry->withBody($body); - } - - return $this; - } - - public function withHeaders(array $headers): self - { - foreach ($headers as $header => $values) { - foreach (array_values($values) as $index => $value) { - $this->client->call('pactffi_with_header_v2', $this->getInteractionId(), $this->getPart(), (string) $header, (int) $index, (string) $value); - } - } - - return $this; - } - - protected function getInteractionId(): int - { - return $this->interactionRegistry->getId(); - } - - abstract protected function getPart(): int; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php deleted file mode 100644 index 383e3a41..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - $headers - */ - public function withHeaders(array $headers): self; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php deleted file mode 100644 index 890ad9b7..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php +++ /dev/null @@ -1,39 +0,0 @@ - $values) { - foreach (array_values($values) as $index => $value) { - $this->client->call('pactffi_with_query_parameter_v2', $this->getInteractionId(), (string) $key, (int) $index, (string) $value); - } - } - - return $this; - } - - public function withRequest(string $method, string $path): self - { - $this->client->call('pactffi_with_request', $this->getInteractionId(), $method, $path); - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php deleted file mode 100644 index 548c964f..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - $queryParams - */ - public function withQueryParameters(array $queryParams): self; - - public function withRequest(string $method, string $path): self; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php deleted file mode 100644 index e5c0d4d3..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php +++ /dev/null @@ -1,28 +0,0 @@ -client->call('pactffi_response_status_v2', $this->getInteractionId(), $status); - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php deleted file mode 100644 index fdcf0258..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -config->setPort($port); } - public function verify(): bool + public function verify(): VerifyResult { try { $matched = $this->isMatched(); if ($matched) { $this->writePact(); - } elseif ($this->logger) { - $this->logger->log($this->getMismatches()); + } else { + $mismatches = $this->getMismatches(); } - return $matched; + return new VerifyResult($matched, $mismatches ?? ''); } finally { $this->cleanUp(); } @@ -65,7 +64,7 @@ protected function getTransportConfig(): ?string return null; } - private function writePact(): void + public function writePact(): void { $error = $this->client->call( 'pactffi_write_pact_file', @@ -78,7 +77,7 @@ private function writePact(): void } } - private function cleanUp(): void + public function cleanUp(): void { $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); $this->pactDriver->cleanUp(); diff --git a/src/PhpPact/Consumer/Service/MockServerInterface.php b/src/PhpPact/Consumer/Service/MockServerInterface.php index 5737269c..0096610c 100644 --- a/src/PhpPact/Consumer/Service/MockServerInterface.php +++ b/src/PhpPact/Consumer/Service/MockServerInterface.php @@ -2,9 +2,15 @@ namespace PhpPact\Consumer\Service; +use PhpPact\Standalone\MockService\Model\VerifyResult; + interface MockServerInterface { public function start(): void; - public function verify(): bool; + public function verify(): VerifyResult; + + public function writePact(): void; + + public function cleanUp(): void; } diff --git a/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php b/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php index d9d9cb0e..4dc30e3f 100644 --- a/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php +++ b/src/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactory.php @@ -4,12 +4,14 @@ use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\Consumer\Driver\InteractionPart\ResponseDriver; use PhpPact\Consumer\Factory\InteractionDriverFactoryInterface; use PhpPact\FFI\Client; +use PhpPact\Plugin\Driver\Body\PluginBodyDriver; +use PhpPact\Plugins\Csv\Driver\Body\CsvBodyDriver; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Consumer\Service\MockServer; use PhpPact\Plugins\Csv\Driver\Pact\CsvPactDriver; -use PhpPact\Plugins\Csv\Registry\Interaction\CsvInteractionRegistry; class CsvInteractionDriverFactory implements InteractionDriverFactoryInterface { @@ -18,8 +20,9 @@ public function create(MockServerConfigInterface $config): InteractionDriverInte $client = new Client(); $pactDriver = new CsvPactDriver($client, $config); $mockServer = new MockServer($client, $pactDriver, $config); - $interactionRegistry = new CsvInteractionRegistry($client, $pactDriver); + $csvBodyDriver = new CsvBodyDriver(new PluginBodyDriver($client)); + $responseDriver = new ResponseDriver($client, $csvBodyDriver); - return new InteractionDriver($interactionRegistry, $mockServer); + return new InteractionDriver($client, $mockServer, $pactDriver, null, $responseDriver); } } diff --git a/src/PhpPact/Plugins/Csv/Registry/Interaction/Body/CsvResponseBodyRegistry.php b/src/PhpPact/Plugins/Csv/Registry/Interaction/Body/CsvResponseBodyRegistry.php deleted file mode 100644 index 4d7eb920..00000000 --- a/src/PhpPact/Plugins/Csv/Registry/Interaction/Body/CsvResponseBodyRegistry.php +++ /dev/null @@ -1,25 +0,0 @@ -interactionRegistry->getId(); - } -} diff --git a/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php b/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php deleted file mode 100644 index 2801470f..00000000 --- a/src/PhpPact/Plugins/Csv/Registry/Interaction/CsvInteractionRegistry.php +++ /dev/null @@ -1,24 +0,0 @@ -mockServer->verify(); } diff --git a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php index 778dc591..2d239684 100644 --- a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php +++ b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php @@ -3,10 +3,11 @@ namespace PhpPact\SyncMessage\Driver\Interaction; use PhpPact\Consumer\Model\Message; +use PhpPact\Standalone\MockService\Model\VerifyResult; interface SyncMessageDriverInterface { - public function verifyMessage(): bool; + public function verifyMessage(): VerifyResult; public function registerMessage(Message $message): void; } diff --git a/src/PhpPact/SyncMessage/SyncMessageBuilder.php b/src/PhpPact/SyncMessage/SyncMessageBuilder.php index aa5e5511..9eb77805 100644 --- a/src/PhpPact/SyncMessage/SyncMessageBuilder.php +++ b/src/PhpPact/SyncMessage/SyncMessageBuilder.php @@ -25,6 +25,6 @@ public function registerMessage(): void public function verify(): bool { - return $this->driver->verifyMessage(); + return $this->driver->verifyMessage()->matched; } } diff --git a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php new file mode 100644 index 00000000..c758f167 --- /dev/null +++ b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php @@ -0,0 +1,113 @@ + [ + 'id' => 12, + 'name' => 'abc', + ] + ]; + + public function setUp(): void + { + $this->client = $this->createMock(ClientInterface::class); + $this->mockServer = $this->createMock(MockServerInterface::class); + $this->pactDriver = $this->createMock(PactDriverInterface::class); + $this->requestDriver = $this->createMock(RequestDriverInterface::class); + $this->responseDriver = $this->createMock(ResponseDriverInterface::class); + $this->driver = new InteractionDriver($this->client, $this->mockServer, $this->pactDriver, $this->requestDriver, $this->responseDriver); + $this->interaction = new Interaction(); + $this->interaction->setDescription($this->description); + foreach ($this->providerStates as $name => $params) { + $this->interaction->addProviderState($name, $params); + } + } + + public function testVerifyInteractions(): void + { + $result = new VerifyResult(true, ''); + $this->mockServer + ->expects($this->once()) + ->method('verify') + ->willReturn($result); + $this->assertSame($result, $this->driver->verifyInteractions()); + } + + public function testWritePactAndCleanUp(): void + { + $this->mockServer + ->expects($this->once()) + ->method('writePact'); + $this->mockServer + ->expects($this->once()) + ->method('cleanUp'); + $this->driver->writePactAndCleanUp(); + } + + #[TestWith([false])] + #[TestWith([true])] + public function testRegisterInteraction(bool $startMockServer): void + { + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $this->requestDriver + ->expects($this->once()) + ->method('registerRequest') + ->with($this->interaction); + $this->responseDriver + ->expects($this->once()) + ->method('registerResponse') + ->with($this->interaction); + $calls = [ + ['pactffi_new_interaction', $this->pactHandle, $this->description, $this->interactionHandle], + ['pactffi_given', $this->interactionHandle, 'item exist', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'name', 'abc', null], + ['pactffi_upon_receiving', $this->interactionHandle, $this->description, null], + ]; + $this->client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + $this->mockServer + ->expects($this->exactly($startMockServer)) + ->method('start'); + $this->assertTrue($this->driver->registerInteraction($this->interaction, $startMockServer)); + $this->assertSame($this->interactionHandle, $this->interaction->getHandle()); + } +} From fae219a78416aaa38a77bbdf7e563307645b525d Mon Sep 17 00:00:00 2001 From: Tien Vo Xuan Date: Thu, 29 Feb 2024 08:17:44 +0700 Subject: [PATCH 246/298] Revert "ci: Tune network" --- .github/workflows/build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5cd2bf63..0153f37e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,11 +54,6 @@ jobs: name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies steps: - - name: Tune Windows Network - if: ${{ runner.os == 'Windows' }} - shell: pwsh - run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6 - - uses: actions/checkout@v3 name: Checkout repository From 1c45cadf83a5c923165c69cc01368ddd0b9e8d81 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 28 Feb 2024 22:54:33 +0700 Subject: [PATCH 247/298] refactor: Merge MessageRegistry and MessageDriver --- .../Interaction/AbstractMessageDriver.php | 70 ++++++++++++ .../Driver/Interaction/MessageDriver.php | 29 +---- .../Interaction/MessageDriverInterface.php | 8 +- .../SharedMessageDriverInterface.php | 10 ++ .../Consumer/Factory/MessageDriverFactory.php | 4 +- src/PhpPact/Consumer/MessageBuilder.php | 6 +- .../Factory/ProtobufMessageDriverFactory.php | 7 +- .../Interaction/ProtobufMessageRegistry.php | 20 ---- .../Driver/Interaction/MessageDriverTest.php | 105 ++++++++++++++++++ 9 files changed, 199 insertions(+), 60 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/SharedMessageDriverInterface.php delete mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php create mode 100644 tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php new file mode 100644 index 00000000..f0e6a204 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php @@ -0,0 +1,70 @@ +messageBodyDriver = $messageBodyDriver ?? new MessageBodyDriver($client); + } + + public function registerMessage(Message $message): void + { + $this->newInteraction($message); + $this->given($message); + $this->expectsToReceive($message); + $this->withMetadata($message); + $this->withContents($message); + } + + public function writePactAndCleanUp(): void + { + $this->pactDriver->writePact(); + $this->pactDriver->cleanUp(); + } + + protected function newInteraction(Message $message): void + { + $handle = $this->client->call('pactffi_new_message_interaction', $this->pactDriver->getPact()->handle, $message->getDescription()); + $message->setHandle($handle); + } + + private function withContents(Message $message): void + { + $this->messageBodyDriver->registerBody($message); + } + + private function expectsToReceive(Message $message): void + { + $this->client->call('pactffi_message_expects_to_receive', $message->getHandle(), $message->getDescription()); + } + + protected function given(Message $message): void + { + foreach ($message->getProviderStates() as $providerState) { + $this->client->call('pactffi_message_given', $message->getHandle(), $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_message_given_with_param', $message->getHandle(), $providerState->getName(), (string) $key, (string) $value); + } + } + } + + private function withMetadata(Message $message): void + { + foreach ($message->getMetadata() as $key => $value) { + $this->client->call('pactffi_message_with_metadata_v2', $message->getHandle(), (string) $key, (string) $value); + } + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php index 80a30661..62233c01 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriver.php @@ -2,35 +2,12 @@ namespace PhpPact\Consumer\Driver\Interaction; -use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Message; -use PhpPact\Consumer\Registry\Interaction\MessageRegistryInterface; -use PhpPact\FFI\ClientInterface; -class MessageDriver implements MessageDriverInterface +class MessageDriver extends AbstractMessageDriver implements MessageDriverInterface { - public function __construct( - private ClientInterface $client, - private PactDriverInterface $pactDriver, - private MessageRegistryInterface $messageRegistry - ) { - } - - public function reify(): string - { - return $this->client->call('pactffi_message_reify', $this->messageRegistry->getId()); - } - - public function writePactAndCleanUp(): bool - { - $this->pactDriver->writePact(); - $this->pactDriver->cleanUp(); - - return true; - } - - public function registerMessage(Message $message): void + public function reify(Message $message): string { - $this->messageRegistry->registerMessage($message); + return $this->client->call('pactffi_message_reify', $message->getHandle()); } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php index be9a3089..008a5ac1 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php @@ -4,11 +4,7 @@ use PhpPact\Consumer\Model\Message; -interface MessageDriverInterface +interface MessageDriverInterface extends SharedMessageDriverInterface { - public function registerMessage(Message $message): void; - - public function reify(): string; - - public function writePactAndCleanUp(): bool; + public function reify(Message $message): string; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/SharedMessageDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/SharedMessageDriverInterface.php new file mode 100644 index 00000000..807d9a20 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/SharedMessageDriverInterface.php @@ -0,0 +1,10 @@ +driver->registerMessage($this->message); - return $this->driver->reify(); + return $this->driver->reify($this->message); } /** @@ -85,9 +85,11 @@ public function verify(): bool \call_user_func($callback, $pactJson); } - return $this->driver->writePactAndCleanUp(); + $this->driver->writePactAndCleanUp(); } catch (\Exception $e) { return false; } + + return true; } } diff --git a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php index 8bb3dc87..7e331981 100644 --- a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php +++ b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactory.php @@ -7,8 +7,9 @@ use PhpPact\Consumer\Driver\Interaction\MessageDriverInterface; use PhpPact\Consumer\Factory\MessageDriverFactoryInterface; use PhpPact\FFI\Client; +use PhpPact\Plugin\Driver\Body\PluginBodyDriver; +use PhpPact\Plugins\Protobuf\Driver\Body\ProtobufMessageBodyDriver; use PhpPact\Plugins\Protobuf\Driver\Pact\ProtobufPactDriver; -use PhpPact\Plugins\Protobuf\Registry\Interaction\ProtobufMessageRegistry; class ProtobufMessageDriverFactory implements MessageDriverFactoryInterface { @@ -16,8 +17,8 @@ public function create(PactConfigInterface $config): MessageDriverInterface { $client = new Client(); $pactDriver = new ProtobufPactDriver($client, $config); - $messageRegistry = new ProtobufMessageRegistry($client, $pactDriver); + $messageBodyDriver = new ProtobufMessageBodyDriver(new PluginBodyDriver($client)); - return new MessageDriver($client, $pactDriver, $messageRegistry); + return new MessageDriver($client, $pactDriver, $messageBodyDriver); } } diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php deleted file mode 100644 index 2a3ac0f3..00000000 --- a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufMessageRegistry.php +++ /dev/null @@ -1,20 +0,0 @@ - [ + 'id' => 12, + 'name' => 'abc', + ] + ]; + private array $metadata = [ + 'key1' => 'value1', + 'key2' => 'value2', + ]; + + public function setUp(): void + { + $this->client = $this->createMock(ClientInterface::class); + $this->pactDriver = $this->createMock(PactDriverInterface::class); + $this->messageBodyDriver = $this->createMock(MessageBodyDriverInterface::class); + $this->driver = new MessageDriver($this->client, $this->pactDriver, $this->messageBodyDriver); + $this->message = new Message(); + $this->message->setDescription($this->description); + foreach ($this->providerStates as $name => $params) { + $this->message->addProviderState($name, $params); + } + $this->message->setMetadata($this->metadata); + } + + public function testReify(): void + { + $this->message->setHandle($this->messageHandle); + $result = 'message'; + $this->client + ->expects($this->once()) + ->method('call') + ->with('pactffi_message_reify', $this->messageHandle) + ->willReturn($result); + $this->assertSame($result, $this->driver->reify($this->message)); + } + + public function testWritePactAndCleanUp(): void + { + $this->pactDriver + ->expects($this->once()) + ->method('writePact'); + $this->pactDriver + ->expects($this->once()) + ->method('cleanUp'); + $this->driver->writePactAndCleanUp(); + } + + public function testRegisterInteraction(): void + { + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $this->messageBodyDriver + ->expects($this->once()) + ->method('registerBody') + ->with($this->message); + $calls = [ + ['pactffi_new_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_message_given', $this->messageHandle, 'item exist', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + $this->client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + $this->driver->registerMessage($this->message); + $this->assertSame($this->messageHandle, $this->message->getHandle()); + } +} From 7a455933f0a35c256268dcd651a02aac6d40dc26 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 29 Feb 2024 09:32:43 +0700 Subject: [PATCH 248/298] refactor: Merge SyncMessageRegistry and SyncMessageDriver --- .../tests/Service/SyncMessagePactWriter.php | 13 +-- .../Registry/Interaction/AbstractRegistry.php | 24 ---- .../Body/BodyRegistryInterface.php | 12 -- .../Body/MessageContentsRegistry.php | 43 ------- .../Registry/Interaction/MessageRegistry.php | 87 -------------- .../Interaction/MessageRegistryInterface.php | 10 -- .../Interaction/Part/RequestPartTrait.php | 11 -- .../Interaction/Part/ResponsePartTrait.php | 11 -- .../Interaction/RegistryInterface.php | 8 -- .../Body/AbstractPluginBodyRegistry.php | 44 ------- .../ProtobufSyncMessageDriverFactory.php | 7 +- .../Body/ProtobufMessageContentsRegistry.php | 25 ---- .../ProtobufSyncMessageRegistry.php | 20 ---- .../Driver/Interaction/SyncMessageDriver.php | 38 ++++++- .../SyncMessageDriverInterface.php | 3 +- .../Factory/SyncMessageDriverFactory.php | 4 +- .../Interaction/SyncMessageRegistry.php | 31 ----- .../Interaction/SyncMessageDriverTest.php | 107 ++++++++++++++++++ 18 files changed, 151 insertions(+), 347 deletions(-) delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Body/MessageContentsRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/RequestPartTrait.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php delete mode 100644 src/PhpPact/Consumer/Registry/Interaction/RegistryInterface.php delete mode 100644 src/PhpPact/Plugin/Registry/Interaction/Body/AbstractPluginBodyRegistry.php delete mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/Body/ProtobufMessageContentsRegistry.php delete mode 100644 src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php delete mode 100644 src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php create mode 100644 tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php diff --git a/compatibility-suite/tests/Service/SyncMessagePactWriter.php b/compatibility-suite/tests/Service/SyncMessagePactWriter.php index 1ef9a9d9..6def54d0 100644 --- a/compatibility-suite/tests/Service/SyncMessagePactWriter.php +++ b/compatibility-suite/tests/Service/SyncMessagePactWriter.php @@ -3,11 +3,9 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; -use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Model\Message; -use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfig; -use PhpPact\SyncMessage\Registry\Interaction\SyncMessageRegistry; +use PhpPact\SyncMessage\Factory\SyncMessageDriverFactory; use PhpPactTest\CompatibilitySuite\Constant\Path; use PhpPactTest\CompatibilitySuite\Model\PactPath; @@ -27,12 +25,9 @@ public function write(Message $message, PactPath $pactPath, string $mode = PactC ->setPactDir(Path::PACTS_PATH) ->setPactSpecificationVersion($this->specificationVersion) ->setPactFileWriteMode($mode); - $client = new Client(); - $pactDriver = new PactDriver($client, $config); - $messageRegistry = new SyncMessageRegistry($client, $pactDriver); + $driver = (new SyncMessageDriverFactory())->create($config); - $messageRegistry->registerMessage($message); - $pactDriver->writePact(); - $pactDriver->cleanUp(); + $driver->registerMessage($message); + $driver->writePactAndCleanUp(); } } diff --git a/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php deleted file mode 100644 index f4ac20c5..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php +++ /dev/null @@ -1,24 +0,0 @@ -id; - } - - abstract protected function newInteraction(string $description): self; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php deleted file mode 100644 index d1f668b9..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getData(); - $success = $this->client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $data->getValue(), $data->getSize()); - break; - - case $body instanceof Text: - $success = $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); - break; - - case $body instanceof Multipart: - throw new BodyNotSupportedException('Message does not support multipart'); - }; - if (!isset($success) || !$success) { - throw new MessageContentsNotAddedException(); - } - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php deleted file mode 100644 index 021fab3f..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ /dev/null @@ -1,87 +0,0 @@ -messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); - } - - - public function registerMessage(Message $message): void - { - $this - ->newInteraction($message->getDescription()) - ->given($message->getProviderStates()) - ->expectsToReceive($message->getDescription()) - ->withMetadata($message->getMetadata()) - ->withContents($message->getContents()); - } - - protected function newInteraction(string $description): self - { - $this->id = $this->client->call('pactffi_new_message_interaction', $this->pactDriver->getPact()->handle, $description); - - return $this; - } - - private function withContents(Text|Binary|null $contents): self - { - if ($contents) { - $this->messageContentsRegistry->withBody($contents); - } - - return $this; - } - - private function expectsToReceive(string $description): self - { - $this->client->call('pactffi_message_expects_to_receive', $this->id, $description); - - return $this; - } - - /** - * @param ProviderState[] $providerStates - */ - protected function given(array $providerStates): self - { - foreach ($providerStates as $providerState) { - $this->client->call('pactffi_message_given', $this->id, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->client->call('pactffi_message_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); - } - } - - return $this; - } - - /** - * @param array $metadata - */ - private function withMetadata(array $metadata): self - { - foreach ($metadata as $key => $value) { - $this->client->call('pactffi_message_with_metadata_v2', $this->id, (string) $key, (string) $value); - } - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php deleted file mode 100644 index ca3af5c2..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -client->get('InteractionPart_Request'); - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php deleted file mode 100644 index 389fe949..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php +++ /dev/null @@ -1,11 +0,0 @@ -client->get('InteractionPart_Response'); - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/RegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/RegistryInterface.php deleted file mode 100644 index 1346d95b..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/RegistryInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -getContents()); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new BodyNotSupportedException('Plugin does not support non-json body contents'); - } - $error = $this->client->call('pactffi_interaction_contents', $this->getInteractionId(), $this->getPart(), $body->getContentType(), $body->getContents()); - if ($error) { - throw new PluginBodyNotAddedException($error); - } - break; - - case $body instanceof Multipart: - throw new BodyNotSupportedException('Plugin does not support multipart body'); - }; - } - - abstract protected function getInteractionId(): int; - - abstract protected function getPart(): int; -} diff --git a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php index 00ebba0b..cd121b51 100644 --- a/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php +++ b/src/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactory.php @@ -3,8 +3,9 @@ namespace PhpPact\Plugins\Protobuf\Factory; use PhpPact\FFI\Client; +use PhpPact\Plugin\Driver\Body\PluginBodyDriver; +use PhpPact\Plugins\Protobuf\Driver\Body\ProtobufMessageBodyDriver; use PhpPact\Plugins\Protobuf\Driver\Pact\ProtobufPactDriver; -use PhpPact\Plugins\Protobuf\Registry\Interaction\ProtobufSyncMessageRegistry; use PhpPact\Plugins\Protobuf\Service\GrpcMockServer; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriver; @@ -18,8 +19,8 @@ public function create(MockServerConfigInterface $config): SyncMessageDriverInte $client = new Client(); $pactDriver = new ProtobufPactDriver($client, $config); $grpcMockServer = new GrpcMockServer($client, $pactDriver, $config); - $syncMessageRegistry = new ProtobufSyncMessageRegistry($client, $pactDriver); + $messageBodyDriver = new ProtobufMessageBodyDriver(new PluginBodyDriver($client)); - return new SyncMessageDriver($syncMessageRegistry, $grpcMockServer); + return new SyncMessageDriver($grpcMockServer, $client, $pactDriver, $messageBodyDriver); } } diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/Body/ProtobufMessageContentsRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/Body/ProtobufMessageContentsRegistry.php deleted file mode 100644 index cd7fc8a0..00000000 --- a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/Body/ProtobufMessageContentsRegistry.php +++ /dev/null @@ -1,25 +0,0 @@ -messageRegistry->getId(); - } -} diff --git a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php b/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php deleted file mode 100644 index aa32d8a5..00000000 --- a/src/PhpPact/Plugins/Protobuf/Registry/Interaction/ProtobufSyncMessageRegistry.php +++ /dev/null @@ -1,20 +0,0 @@ -messageRegistry->registerMessage($message); + parent::registerMessage($message); $this->mockServer->start(); } + + public function writePactAndCleanUp(): void + { + $this->mockServer->writePact(); + $this->mockServer->cleanUp(); + } + + protected function newInteraction(Message $message): void + { + $handle = $this->client->call('pactffi_new_sync_message_interaction', $this->pactDriver->getPact()->handle, $message->getDescription()); + $message->setHandle($handle); + } + + protected function given(Message $message): void + { + foreach ($message->getProviderStates() as $providerState) { + $this->client->call('pactffi_given', $message->getHandle(), $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_given_with_param', $message->getHandle(), $providerState->getName(), (string) $key, (string) $value); + } + } + } } diff --git a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php index 2d239684..84893fb8 100644 --- a/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php +++ b/src/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverInterface.php @@ -2,10 +2,11 @@ namespace PhpPact\SyncMessage\Driver\Interaction; +use PhpPact\Consumer\Driver\Interaction\SharedMessageDriverInterface; use PhpPact\Consumer\Model\Message; use PhpPact\Standalone\MockService\Model\VerifyResult; -interface SyncMessageDriverInterface +interface SyncMessageDriverInterface extends SharedMessageDriverInterface { public function verifyMessage(): VerifyResult; diff --git a/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php b/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php index 49ed97ca..2e9d571a 100644 --- a/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php +++ b/src/PhpPact/SyncMessage/Factory/SyncMessageDriverFactory.php @@ -7,7 +7,6 @@ use PhpPact\FFI\Client; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriver; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriverInterface; -use PhpPact\SyncMessage\Registry\Interaction\SyncMessageRegistry; use PhpPact\Standalone\MockService\MockServerConfigInterface; class SyncMessageDriverFactory implements SyncMessageDriverFactoryInterface @@ -16,9 +15,8 @@ public function create(MockServerConfigInterface $config): SyncMessageDriverInte { $client = new Client(); $pactDriver = new PactDriver($client, $config); - $messageRegistry = new SyncMessageRegistry($client, $pactDriver); $mockServer = new MockServer($client, $pactDriver, $config); - return new SyncMessageDriver($messageRegistry, $mockServer); + return new SyncMessageDriver($mockServer, $client, $pactDriver); } } diff --git a/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php b/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php deleted file mode 100644 index 364747c2..00000000 --- a/src/PhpPact/SyncMessage/Registry/Interaction/SyncMessageRegistry.php +++ /dev/null @@ -1,31 +0,0 @@ -id = $this->client->call('pactffi_new_sync_message_interaction', $this->pactDriver->getPact()->handle, $description); - - return $this; - } - - /** - * @param ProviderState[] $providerStates - */ - protected function given(array $providerStates): self - { - foreach ($providerStates as $providerState) { - $this->client->call('pactffi_given', $this->id, $providerState->getName()); - foreach ($providerState->getParams() as $key => $value) { - $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); - } - } - - return $this; - } -} diff --git a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php new file mode 100644 index 00000000..0d306f94 --- /dev/null +++ b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php @@ -0,0 +1,107 @@ + [ + 'id' => 12, + 'name' => 'abc', + ] + ]; + private array $metadata = [ + 'key1' => 'value1', + 'key2' => 'value2', + ]; + + public function setUp(): void + { + $this->mockServer = $this->createMock(MockServerInterface::class); + $this->client = $this->createMock(ClientInterface::class); + $this->pactDriver = $this->createMock(PactDriverInterface::class); + $this->messageBodyDriver = $this->createMock(MessageBodyDriverInterface::class); + $this->driver = new SyncMessageDriver($this->mockServer, $this->client, $this->pactDriver, $this->messageBodyDriver); + $this->message = new Message(); + $this->message->setDescription($this->description); + foreach ($this->providerStates as $name => $params) { + $this->message->addProviderState($name, $params); + } + $this->message->setMetadata($this->metadata); + } + + public function testVerifyMessage(): void + { + $result = new VerifyResult(false, 'some mismatches'); + $this->mockServer + ->expects($this->once()) + ->method('verify') + ->willReturn($result); + $this->assertSame($result, $this->driver->verifyMessage()); + } + + public function testWritePactAndCleanUp(): void + { + $this->mockServer + ->expects($this->once()) + ->method('writePact'); + $this->mockServer + ->expects($this->once()) + ->method('cleanUp'); + $this->driver->writePactAndCleanUp(); + } + + public function testRegisterInteraction(): void + { + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $this->messageBodyDriver + ->expects($this->once()) + ->method('registerBody') + ->with($this->message); + $calls = [ + ['pactffi_new_sync_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_given', $this->messageHandle, 'item exist', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + $this->client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + $this->driver->registerMessage($this->message); + $this->assertSame($this->messageHandle, $this->message->getHandle()); + } +} From 2da6624e4e30cea66446bb48fc825c7d2d3f7252 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 29 Feb 2024 12:18:02 +0700 Subject: [PATCH 249/298] test: Test mock server --- .../Consumer/Service/MockServerTest.php | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/PhpPact/Consumer/Service/MockServerTest.php diff --git a/tests/PhpPact/Consumer/Service/MockServerTest.php b/tests/PhpPact/Consumer/Service/MockServerTest.php new file mode 100644 index 00000000..0d623ab6 --- /dev/null +++ b/tests/PhpPact/Consumer/Service/MockServerTest.php @@ -0,0 +1,157 @@ +client = $this->createMock(ClientInterface::class); + $this->pactDriver = $this->createMock(PactDriverInterface::class); + $this->config = new MockServerConfig(); + $this->mockServer = new MockServer($this->client, $this->pactDriver, $this->config); + } + + #[TestWith([234, true, 'https'])] + #[TestWith([234, false, 'http'])] + #[TestWith([0, true, 'https'])] + #[TestWith([-1, true, 'https'])] + #[TestWith([-2, true, 'https'])] + #[TestWith([-3, true, 'https'])] + #[TestWith([-4, true, 'https'])] + #[TestWith([-5, true, 'https'])] + #[TestWith([-6, true, 'https'])] + public function testStart(int $returnedPort, bool $secure, string $transport): void + { + $this->config->setHost($this->host); + $this->config->setPort($this->port); + $this->config->setSecure($secure); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_create_mock_server_for_transport', $this->pactHandle, $this->host, $this->port, $transport, null, $returnedPort], + ]; + $this->assertClientCalls($calls); + if ($returnedPort < 0) { + $this->expectException(MockServerNotStartedException::class); + $this->expectExceptionMessage(match ($returnedPort) { + -1 => 'An invalid handle was received. Handles should be created with `pactffi_new_pact`', + -2 => 'Transport_config is not valid JSON', + -3 => 'The mock server could not be started', + -4 => 'The method panicked', + -5 => 'The address is not valid', + default => 'Unknown error', + }); + } + $this->mockServer->start(); + $this->assertSame($returnedPort, $this->config->getPort()); + } + + #[TestWith([false])] + #[TestWith([true])] + public function testVerify(bool $matched): void + { + $this->config->setPort($this->port); + $this->config->setPactDir($this->pactDir); + $calls = $matched ? [ + ['pactffi_mock_server_matched', $this->port, $matched], + ['pactffi_write_pact_file', $this->port, $this->pactDir, false, 0], + ['pactffi_cleanup_mock_server', $this->port, null], + ] : [ + ['pactffi_mock_server_matched', $this->port, $matched], + ['pactffi_mock_server_mismatches', $this->port, FFI::new('char[1]')], + ['pactffi_cleanup_mock_server', $this->port, null], + ]; + $this->assertClientCalls($calls); + $this->pactDriver + ->expects($this->once()) + ->method('cleanUp'); + $result = $this->mockServer->verify(); + $this->assertSame($matched, $result->matched); + $this->assertSame('', $result->mismatches); + } + + #[TestWith([0, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([1, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([2, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([3, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([4, PactConfigInterface::MODE_OVERWRITE])] + #[TestWith([0, PactConfigInterface::MODE_MERGE])] + #[TestWith([1, PactConfigInterface::MODE_MERGE])] + #[TestWith([2, PactConfigInterface::MODE_MERGE])] + #[TestWith([3, PactConfigInterface::MODE_MERGE])] + #[TestWith([4, PactConfigInterface::MODE_MERGE])] + public function testWritePact(int $error, string $writeMode): void + { + $this->config->setPort($this->port); + $this->config->setPactDir($this->pactDir); + $this->config->setPactFileWriteMode($writeMode); + $calls = [ + ['pactffi_write_pact_file', $this->port, $this->pactDir, $writeMode === PactConfigInterface::MODE_OVERWRITE, $error], + ]; + $this->assertClientCalls($calls); + if ($error) { + $this->expectException(MockServerNotWrotePactFileException::class); + $this->expectExceptionMessage(match ($error) { + 1 => 'A general panic was caught', + 2 => 'The pact file was not able to be written', + 3 => 'A mock server with the provided port was not found', + default => 'Unknown error', + }); + } + $this->mockServer->writePact(); + } + + public function testCleanUp(): void + { + $this->config->setPort($this->port); + $calls = [ + ['pactffi_cleanup_mock_server', $this->port, null], + ]; + $this->assertClientCalls($calls); + $this->pactDriver + ->expects($this->once()) + ->method('cleanUp'); + $this->mockServer->cleanUp(); + } + + protected function assertClientCalls(array $calls): void + { + $this->client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + } +} From 08109bf9ed889ae5f93fbd314c0d08117b3a6ff1 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 29 Feb 2024 13:18:21 +0700 Subject: [PATCH 250/298] ci: Split jobs --- .github/workflows/build.yml | 114 ++++++++++++++++------ .github/workflows/compatibility-suite.yml | 2 +- phpunit.xml | 72 ++++---------- 3 files changed, 108 insertions(+), 80 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0153f37e..0c1034e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ -name: Pact-PHP Code Analysis & Test +name: Code Analysis & Test on: - push: + push: pull_request: # Once on the first of the month at 06:00 UTC schedule: @@ -16,17 +16,15 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - php: [ '8.2' ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout repository - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + php-version: 8.1 coverage: none - uses: ramsey/composer-install@v2 @@ -39,54 +37,114 @@ jobs: - name: Static Code Analysis run: composer run static-code-analysis - test: - runs-on: ${{ matrix.operating-system }} + examples: + runs-on: ${{ matrix.os }} needs: - php-cs strategy: fail-fast: false matrix: - operating-system: [ ubuntu-latest, windows-latest, macos-12, macos-14 ] - php: [ '8.1', '8.2', '8.3' ] - dependencies: [ 'lowest', 'locked' ] + include: + - os: ubuntu-latest + php: 8.1 + - os: macos-12 + php: 8.2 + - os: macos-14 + php: 8.2 + - os: windows-latest + example: 'json' + php: 8.2 + - os: windows-latest + example: 'binary' + php: 8.2 + - os: windows-latest + example: 'multipart' + php: 8.2 + - os: windows-latest + example: 'xml' + php: 8.2 + - os: windows-latest + example: 'message' + php: 8.2 + - os: windows-latest + example: 'matchers' + php: 8.2 + - os: windows-latest + example: 'generators' + php: 8.2 + - os: windows-latest + example: 'csv' + php: 8.2 + - os: windows-latest + example: 'protobuf-sync-message' + php: 8.1 + - os: windows-latest + example: 'protobuf-async-message' + php: 8.2 timeout-minutes: 5 - name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout repository - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) && 'sockets, curl, zip, ffi' || 'sockets, curl, zip, ffi, grpc' }} + extensions: sockets, curl, zip, ffi ${{ (!matrix.example || matrix.example == 'protobuf-sync-message') && ', grpc' || '' }} php-version: ${{ matrix.php }} coverage: none - ini-values: ${{ matrix.operating-system == 'windows-latest' && 'opcache.enable=0 opcache.enable_cli=0' || '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Composer install + uses: ramsey/composer-install@v2 + with: + dependency-versions: 'locked' - name: Install Protoc - uses: arduino/setup-protoc@v2 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + if: ${{ !matrix.example || contains(matrix.example, 'protobuf') }} + + - name: Generate Library + run: composer gen-lib + if: ${{ !matrix.example || contains(matrix.example, 'protobuf') }} - - name: Install gRPC Extension (for PHP 8.2 & 8.3 on Windows) + - name: Run example(s) run: | - cd C:\tools\php - Invoke-WebRequest -Uri https://github.com/tienvx/build-php-grpc-extension/releases/download/builds/grpc_windows-2022_php-${{ matrix.php }}.dll -OutFile ext\php_grpc.dll - echo "extension=php_grpc.dll" >> php.ini - if: ${{ matrix.operating-system == 'windows-latest' && contains('["8.2","8.3"]', matrix.php) }} - shell: pwsh + php -r "array_map('unlink', glob('./example/*/pacts/*.json'));" + vendor/bin/phpunit --no-coverage --exclude-testsuite unit ${{ matrix.example && format('--testsuite {0}-example', matrix.example) || '' }} + env: + PACT_DO_NOT_TRACK: true + + unit: + runs-on: ubuntu-latest + needs: + - php-cs + strategy: + fail-fast: false + matrix: + php: [ '8.1', '8.2', '8.3' ] + dependencies: [ 'lowest', 'locked' ] + + steps: + - uses: actions/checkout@v4 + name: Checkout repository + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + extensions: 'sockets, curl, zip, ffi' + php-version: ${{ matrix.php }} + coverage: pcov - name: Composer install uses: ramsey/composer-install@v2 with: dependency-versions: ${{ matrix.dependencies }} - - name: Generate Library - run: composer gen-lib - - - name: Composer test - run: composer test + - name: Test Unit + run: vendor/bin/phpunit --testsuite unit env: PACT_DO_NOT_TRACK: true diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index d5faac90..ab63da06 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -1,4 +1,4 @@ -name: Pact-PHP Compatibility Suite +name: Compatibility Suite on: [push, pull_request] diff --git a/phpunit.xml b/phpunit.xml index a9253359..97bc65b3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,68 +7,38 @@ - + ./tests - - ./example/json/consumer/tests + + ./example/json - - ./example/json/provider/tests + + ./example/binary - - ./example/binary/consumer/tests + + ./example/multipart - - ./example/binary/provider/tests + + ./example/xml - - ./example/multipart/consumer/tests + + ./example/message - - ./example/multipart/provider/tests + + ./example/matchers - - ./example/xml/consumer/tests + + ./example/generators - - ./example/xml/provider/tests + + ./example/csv - - ./example/message/consumer/tests + + ./example/protobuf-sync-message - - ./example/message/provider/tests - - - ./example/matchers/consumer/tests - - - ./example/matchers/provider/tests - - - ./example/generators/consumer/tests - - - ./example/generators/provider/tests - - - ./example/csv/consumer/tests - - - ./example/csv/provider/tests - - - ./example/protobuf-sync-message/consumer/tests - - - ./example/protobuf-sync-message/provider/tests - - - ./example/protobuf-async-message/consumer/tests - - - ./example/protobuf-async-message/provider/tests + + ./example/protobuf-async-message From d15623850ac086816edc996db2f57efda21b98fc Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 29 Feb 2024 19:50:53 +0700 Subject: [PATCH 251/298] ci: Allow 'composer test' to take extra options --- .github/workflows/build.yml | 4 +--- composer.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c1034e0..feceb4fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,9 +112,7 @@ jobs: if: ${{ !matrix.example || contains(matrix.example, 'protobuf') }} - name: Run example(s) - run: | - php -r "array_map('unlink', glob('./example/*/pacts/*.json'));" - vendor/bin/phpunit --no-coverage --exclude-testsuite unit ${{ matrix.example && format('--testsuite {0}-example', matrix.example) || '' }} + run: composer test -- --exclude-testsuite unit ${{ matrix.example && format('--testsuite {0}-example', matrix.example) || '' }} env: PACT_DO_NOT_TRACK: true diff --git a/composer.json b/composer.json index 4f250d09..1c37a0b2 100644 --- a/composer.json +++ b/composer.json @@ -99,7 +99,7 @@ "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", "test": [ - "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", + "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\" --", "phpunit --no-coverage" ], "gen-lib": [ From 26884c4bbd30b565c75d7b590cb2664106970de8 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 1 Mar 2024 10:42:16 +0700 Subject: [PATCH 252/298] ci(compatibility-suite): Force output colors for behat --- .github/workflows/compatibility-suite.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index ab63da06..49c337b3 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -21,7 +21,7 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V1 + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V1 --colors v2: runs-on: ubuntu-latest steps: @@ -37,7 +37,7 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V2 + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V2 --colors v3: runs-on: ubuntu-latest steps: @@ -53,7 +53,7 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!binary body \(negative|Message provider).)*$/' + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!binary body \(negative|Message provider).)*$/' --colors v4: runs-on: ubuntu-latest steps: @@ -69,4 +69,4 @@ jobs: - uses: ramsey/composer-install@v2 - name: Run Behat - run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V4 + run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V4 --colors From 182cd038b162550a384fe73bbfb3e1c0753dc31c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 1 Mar 2024 11:05:00 +0700 Subject: [PATCH 253/298] ci(compatibility-suite): Update Github Actions --- .github/workflows/compatibility-suite.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index ab63da06..4836d834 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -9,7 +9,7 @@ jobs: v1: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -18,14 +18,14 @@ jobs: php-version: 8.2 coverage: none - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V1 v2: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -34,14 +34,14 @@ jobs: php-version: 8.2 coverage: none - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V2 v3: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -50,14 +50,14 @@ jobs: php-version: 8.2 coverage: none - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V3 --name '/^((?!binary body \(negative|Message provider).)*$/' v4: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -66,7 +66,7 @@ jobs: php-version: 8.2 coverage: none - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 - name: Run Behat run: vendor/bin/behat compatibility-suite/pact-compatibility-suite/features/V4 From 7e95c93bf2fa4087672bf3eb963a9fc42c154349 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 29 Feb 2024 23:26:06 +0700 Subject: [PATCH 254/298] feat: Allow set interaction key for v4 --- .../suites/v4/message/provider.yml | 1 + .../Context/V3/Message/ProviderContext.php | 6 ++- .../tests/Context/V4/CombinedContext.php | 5 +- .../tests/Context/V4/Http/ConsumerContext.php | 4 +- .../Context/V4/Message/ConsumerContext.php | 10 ++-- .../Context/V4/Message/ProviderContext.php | 13 ++++- .../V4/SyncMessage/ConsumerContext.php | 4 +- .../tests/Service/MessagePactWriter.php | 5 +- .../Service/MessagePactWriterInterface.php | 3 +- composer.json | 4 +- .../Consumer/AbstractMessageBuilder.php | 10 ++++ .../InteractionKeyNotSetException.php | 7 +++ .../Driver/Interaction/AbstractDriver.php | 28 +++++++++++ .../Interaction/AbstractMessageDriver.php | 6 ++- .../Driver/Interaction/InteractionDriver.php | 6 ++- src/PhpPact/Consumer/InteractionBuilder.php | 10 ++++ src/PhpPact/Consumer/Model/Interaction.php | 2 + .../Consumer/Model/Interaction/KeyTrait.php | 20 ++++++++ src/PhpPact/Consumer/Model/Message.php | 2 + .../Interaction/InteractionDriverTest.php | 45 ++++++++++++----- .../Driver/Interaction/MessageDriverTest.php | 48 ++++++++++++++----- .../Consumer/Driver/Pact/PactDriverTest.php | 18 ++----- tests/PhpPact/Helper/FFI/ClientTrait.php | 25 ++++++++++ .../Interaction/SyncMessageDriverTest.php | 48 ++++++++++++++----- 24 files changed, 260 insertions(+), 70 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Exception/InteractionKeyNotSetException.php create mode 100644 src/PhpPact/Consumer/Driver/Interaction/AbstractDriver.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/KeyTrait.php create mode 100644 tests/PhpPact/Helper/FFI/ClientTrait.php diff --git a/compatibility-suite/suites/v4/message/provider.yml b/compatibility-suite/suites/v4/message/provider.yml index c66c344f..31a14f5f 100644 --- a/compatibility-suite/suites/v4/message/provider.yml +++ b/compatibility-suite/suites/v4/message/provider.yml @@ -11,6 +11,7 @@ default: - '@interactions_storage' - '@message_pact_writer' - '@provider_verifier' + - '@parser' filters: tags: "@provider&&@message" diff --git a/compatibility-suite/tests/Context/V3/Message/ProviderContext.php b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php index 687a354d..843f9875 100644 --- a/compatibility-suite/tests/Context/V3/Message/ProviderContext.php +++ b/compatibility-suite/tests/Context/V3/Message/ProviderContext.php @@ -5,6 +5,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Hook\Scope\BeforeStepScope; use Behat\Gherkin\Node\TableNode; +use PhpPact\Consumer\Model\Message; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\FixtureLoaderInterface; @@ -80,7 +81,10 @@ public function registerInteractions(BeforeStepScope $scope): void */ public function aPactFileForIsToBeVerified(string $name, string $fixture): void { - $this->pactWriter->write($name, $fixture, $this->pactPath); + $message = new Message(); + $message->setDescription($name); + $message->setContents($this->parser->parseBody($fixture)); + $this->pactWriter->write($message, $this->pactPath); $this->providerVerifier->addSource($this->pactPath); } diff --git a/compatibility-suite/tests/Context/V4/CombinedContext.php b/compatibility-suite/tests/Context/V4/CombinedContext.php index 91415bf1..d0fbb9f1 100644 --- a/compatibility-suite/tests/Context/V4/CombinedContext.php +++ b/compatibility-suite/tests/Context/V4/CombinedContext.php @@ -4,6 +4,7 @@ use Behat\Behat\Context\Context; use PhpPact\Config\PactConfigInterface; +use PhpPact\Consumer\Model\Message; use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; @@ -51,7 +52,9 @@ public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void public function thePactFileForTheTestIsGenerated(): void { $this->pactWriter->write($this->id, $this->pactPath, PactConfigInterface::MODE_MERGE); - $this->messagePactWriter->write('message interaction', '', $this->pactPath, PactConfigInterface::MODE_MERGE); + $message = new Message(); + $message->setDescription('message interaction'); + $this->messagePactWriter->write($message, $this->pactPath, PactConfigInterface::MODE_MERGE); } /** diff --git a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php index 297ef737..69075a63 100644 --- a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php @@ -52,7 +52,7 @@ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): v */ public function aKeyOfIsSpecifiedForTheHttpInteraction(string $key): void { - throw new PendingException("Can't set interaction's key using FFI call"); + $this->interaction->setKey($key); } /** @@ -61,7 +61,7 @@ public function aKeyOfIsSpecifiedForTheHttpInteraction(string $key): void public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame($value, $pact['interactions'][0][$name]); + Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); } /** diff --git a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php index 418783cd..cf9df913 100644 --- a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php @@ -4,6 +4,7 @@ use Behat\Behat\Context\Context; use Behat\Behat\Tester\Exception\PendingException; +use PhpPact\Consumer\Model\Message; use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\MessagePactWriterInterface; use PHPUnit\Framework\Assert; @@ -11,6 +12,7 @@ final class ConsumerContext implements Context { private PactPath $pactPath; + private Message $message; public function __construct( private MessagePactWriterInterface $pactWriter @@ -23,6 +25,8 @@ public function __construct( */ public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void { + $this->message = new Message(); + $this->message->setDescription('a message'); } /** @@ -30,7 +34,7 @@ public function aMessageInteractionIsBeingDefinedForAConsumerTest(): void */ public function thePactFileForTheTestIsGenerated(): void { - $this->pactWriter->write('a message', '', $this->pactPath); + $this->pactWriter->write($this->message, $this->pactPath); } /** @@ -47,7 +51,7 @@ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): v */ public function aKeyOfIsSpecifiedForTheMessageInteraction(string $key): void { - throw new PendingException("Can't set message's key using FFI call"); + $this->message->setKey($key); } /** @@ -72,6 +76,6 @@ public function aCommentIsAddedToTheMessageInteraction(string $value): void public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame($value, $pact['interactions'][0][$name]); + Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); } } diff --git a/compatibility-suite/tests/Context/V4/Message/ProviderContext.php b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php index b53a4774..460b6310 100644 --- a/compatibility-suite/tests/Context/V4/Message/ProviderContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ProviderContext.php @@ -4,12 +4,14 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; +use PhpPact\Consumer\Model\Message; use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPactTest\CompatibilitySuite\Constant\Mismatch; use PhpPactTest\CompatibilitySuite\Model\PactPath; use PhpPactTest\CompatibilitySuite\Service\InteractionBuilderInterface; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\MessagePactWriterInterface; +use PhpPactTest\CompatibilitySuite\Service\ParserInterface; use PhpPactTest\CompatibilitySuite\Service\ProviderVerifierInterface; use PhpPactTest\CompatibilitySuite\Service\ServerInterface; use PHPUnit\Framework\Assert; @@ -25,6 +27,7 @@ public function __construct( private InteractionsStorageInterface $storage, private MessagePactWriterInterface $pactWriter, private ProviderVerifierInterface $providerVerifier, + private ParserInterface $parser, ) { $this->pactPath = new PactPath(); } @@ -61,7 +64,10 @@ public function aProviderIsStartedThatCanGenerateTheMessageWith(string $name, st */ public function aPactFileForIsToBeVerifiedButIsMarkedPending(string $name, string $fixture): void { - $this->pactWriter->write($name, $fixture, $this->pactPath); + $message = new Message(); + $message->setDescription($name); + $message->setContents($this->parser->parseBody($fixture)); + $this->pactWriter->write($message, $this->pactPath); $this->providerVerifier->addSource($this->pactPath); $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['pending'] = true; @@ -89,7 +95,10 @@ public function aPactFileForIsToBeVerifiedWithTheFollowingComments(string $name, break; } } - $this->pactWriter->write($name, $fixture, $this->pactPath); + $message = new Message(); + $message->setDescription($name); + $message->setContents($this->parser->parseBody($fixture)); + $this->pactWriter->write($message, $this->pactPath); $this->providerVerifier->addSource($this->pactPath); $pact = json_decode(file_get_contents($this->pactPath), true); $pact['interactions'][0]['comments'] = $comments; diff --git a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php index 0067a758..727ce937 100644 --- a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php @@ -53,7 +53,7 @@ public function theFirstInteractionInThePactFileWillHaveATypeOf(string $type): v */ public function aKeyOfIsSpecifiedForTheSynchronousMessageInteraction(string $key): void { - throw new PendingException("Can't set sync message's key using FFI call"); + $this->message->setKey($key); } /** @@ -78,7 +78,7 @@ public function aCommentIsAddedToTheSynchronousMessageInteraction(string $value) public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame($value, $pact['interactions'][0][$name]); + Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); } /** diff --git a/compatibility-suite/tests/Service/MessagePactWriter.php b/compatibility-suite/tests/Service/MessagePactWriter.php index cdbe2500..36328bf9 100644 --- a/compatibility-suite/tests/Service/MessagePactWriter.php +++ b/compatibility-suite/tests/Service/MessagePactWriter.php @@ -17,7 +17,7 @@ public function __construct( ) { } - public function write(string $name, string $body, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void + public function write(Message $message, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void { $config = new MockServerConfig(); $config @@ -28,9 +28,6 @@ public function write(string $name, string $body, PactPath $pactPath, string $mo ->setPactFileWriteMode($mode); $driver = (new MessageDriverFactory())->create($config); - $message = new Message(); - $message->setDescription($name); - $message->setContents($this->parser->parseBody($body)); $driver->registerMessage($message); $driver->writePactAndCleanUp(); } diff --git a/compatibility-suite/tests/Service/MessagePactWriterInterface.php b/compatibility-suite/tests/Service/MessagePactWriterInterface.php index 7612d0b0..59d3ffd5 100644 --- a/compatibility-suite/tests/Service/MessagePactWriterInterface.php +++ b/compatibility-suite/tests/Service/MessagePactWriterInterface.php @@ -3,9 +3,10 @@ namespace PhpPactTest\CompatibilitySuite\Service; use PhpPact\Config\PactConfigInterface; +use PhpPact\Consumer\Model\Message; use PhpPactTest\CompatibilitySuite\Model\PactPath; interface MessagePactWriterInterface { - public function write(string $name, string $body, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void; + public function write(Message $message, PactPath $pactPath, string $mode = PactConfigInterface::MODE_OVERWRITE): void; } diff --git a/composer.json b/composer.json index 4f250d09..06dba979 100644 --- a/composer.json +++ b/composer.json @@ -111,12 +111,12 @@ "extra": { "downloads": { "pact-ffi-headers": { - "version": "0.4.16", + "version": "0.4.18", "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", "path": "bin/pact-ffi-headers/pact.h" }, "pact-ffi-lib": { - "version": "0.4.16", + "version": "0.4.18", "variables": { "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index 71a23275..e84f9e17 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -56,4 +56,14 @@ public function withContent(mixed $contents): self return $this; } + + /** + * Set key for message interaction. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + */ + public function key(?string $key): self + { + $this->message->setKey($key); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Driver/Exception/InteractionKeyNotSetException.php b/src/PhpPact/Consumer/Driver/Exception/InteractionKeyNotSetException.php new file mode 100644 index 00000000..e8fb02bf --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Exception/InteractionKeyNotSetException.php @@ -0,0 +1,7 @@ +getKey(); + if (null === $key) { + return; + } + $success = $this->client->call('pactffi_set_key', $interaction->getHandle(), $key); + if (!$success) { + throw new InteractionKeyNotSetException(sprintf("Can not set the key '%s' for the interaction '%s'", $key, $interaction->getDescription())); + } + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php index f0e6a204..55a5fdc1 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php @@ -8,15 +8,16 @@ use PhpPact\Consumer\Model\Message; use PhpPact\FFI\ClientInterface; -abstract class AbstractMessageDriver implements SharedMessageDriverInterface +abstract class AbstractMessageDriver extends AbstractDriver implements SharedMessageDriverInterface { private MessageBodyDriverInterface $messageBodyDriver; public function __construct( - protected ClientInterface $client, + ClientInterface $client, protected PactDriverInterface $pactDriver, ?MessageBodyDriverInterface $messageBodyDriver = null ) { + parent::__construct($client); $this->messageBodyDriver = $messageBodyDriver ?? new MessageBodyDriver($client); } @@ -27,6 +28,7 @@ public function registerMessage(Message $message): void $this->expectsToReceive($message); $this->withMetadata($message); $this->withContents($message); + $this->setKey($message); } public function writePactAndCleanUp(): void diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index a834780c..f10829e0 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -12,18 +12,19 @@ use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\Model\VerifyResult; -class InteractionDriver implements InteractionDriverInterface +class InteractionDriver extends AbstractDriver implements InteractionDriverInterface { private RequestDriverInterface $requestDriver; private ResponseDriverInterface $responseDriver; public function __construct( - private ClientInterface $client, + ClientInterface $client, private MockServerInterface $mockServer, private PactDriverInterface $pactDriver, ?RequestDriverInterface $requestDriver = null, ?ResponseDriverInterface $responseDriver = null, ) { + parent::__construct($client); $this->requestDriver = $requestDriver ?? new RequestDriver($client); $this->responseDriver = $responseDriver ?? new ResponseDriver($client); } @@ -40,6 +41,7 @@ public function registerInteraction(Interaction $interaction, bool $startMockSer $this->uponReceiving($interaction); $this->withRequest($interaction); $this->willRespondWith($interaction); + $this->setKey($interaction); if ($startMockServer) { $this->mockServer->start(); diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index a09c1202..5249ba03 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -75,4 +75,14 @@ public function verify(): bool { return $this->driver->verifyInteractions()->matched; } + + /** + * Set key for interaction. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + */ + public function key(?string $key): self + { + $this->interaction->setKey($key); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index a3445517..97aa0c7f 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -8,6 +8,7 @@ use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; +use PhpPact\Consumer\Model\Interaction\KeyTrait; /** * Request/Response Pair to be posted to the Mock Server for PACT tests. @@ -17,6 +18,7 @@ class Interaction use ProviderStates; use DescriptionTrait; use HandleTrait; + use KeyTrait; private ConsumerRequest $request; diff --git a/src/PhpPact/Consumer/Model/Interaction/KeyTrait.php b/src/PhpPact/Consumer/Model/Interaction/KeyTrait.php new file mode 100644 index 00000000..2757bc0d --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/KeyTrait.php @@ -0,0 +1,20 @@ +key; + } + + public function setKey(?string $key): self + { + $this->key = $key; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 0d175c80..43e24bb6 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -10,6 +10,7 @@ use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; +use PhpPact\Consumer\Model\Interaction\KeyTrait; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -19,6 +20,7 @@ class Message use ProviderStates; use DescriptionTrait; use HandleTrait; + use KeyTrait; /** * @var array diff --git a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php index c758f167..59cc2eb3 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; +use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Driver\InteractionPart\RequestDriverInterface; @@ -12,14 +13,16 @@ use PhpPact\Consumer\Service\MockServerInterface; use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\Model\VerifyResult; +use PhpPactTest\Helper\FFI\ClientTrait; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class InteractionDriverTest extends TestCase { + use ClientTrait; + private InteractionDriverInterface $driver; - private ClientInterface|MockObject $client; private MockServerInterface|MockObject $mockServer; private PactDriverInterface|MockObject $pactDriver; private RequestDriverInterface|MockObject $requestDriver; @@ -94,20 +97,40 @@ public function testRegisterInteraction(bool $startMockServer): void ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'name', 'abc', null], ['pactffi_upon_receiving', $this->interactionHandle, $this->description, null], ]; - $this->client - ->expects($this->exactly(count($calls))) - ->method('call') - ->willReturnCallback(function (...$args) use (&$calls) { - $call = array_shift($calls); - $return = array_pop($call); - $this->assertSame($call, $args); - - return $return; - }); + $this->assertClientCalls($calls); $this->mockServer ->expects($this->exactly($startMockServer)) ->method('start'); $this->assertTrue($this->driver->registerInteraction($this->interaction, $startMockServer)); $this->assertSame($this->interactionHandle, $this->interaction->getHandle()); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith(['123ABC', false])] + #[TestWith(['123ABC', true])] + public function testSetKey(?string $key, $success): void + { + $this->interaction->setKey($key); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_interaction', $this->pactHandle, $this->description, $this->interactionHandle], + ['pactffi_given', $this->interactionHandle, 'item exist', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'name', 'abc', null], + ['pactffi_upon_receiving', $this->interactionHandle, $this->description, null], + ]; + if (is_string($key)) { + $calls[] = ['pactffi_set_key', $this->interactionHandle, $key, $success]; + } + if (!$success) { + $this->expectException(InteractionKeyNotSetException::class); + $this->expectExceptionMessage("Can not set the key '$key' for the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerInteraction($this->interaction, false); + } } diff --git a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php index 2e3e4221..91b851fb 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php @@ -3,19 +3,23 @@ namespace PhpPactTest\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; +use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Interaction\MessageDriver; use PhpPact\Consumer\Driver\Interaction\MessageDriverInterface; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\Pact\Pact; use PhpPact\FFI\ClientInterface; +use PhpPactTest\Helper\FFI\ClientTrait; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class MessageDriverTest extends TestCase { + use ClientTrait; + private MessageDriverInterface $driver; - private ClientInterface|MockObject $client; private PactDriverInterface|MockObject $pactDriver; private MessageBodyDriverInterface|MockObject $messageBodyDriver; private Message $message; @@ -89,17 +93,39 @@ public function testRegisterInteraction(): void ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], ]; - $this->client - ->expects($this->exactly(count($calls))) - ->method('call') - ->willReturnCallback(function (...$args) use (&$calls) { - $call = array_shift($calls); - $return = array_pop($call); - $this->assertSame($call, $args); - - return $return; - }); + $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); $this->assertSame($this->messageHandle, $this->message->getHandle()); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith(['123ABC', false])] + #[TestWith(['123ABC', true])] + public function testSetKey(?string $key, $success): void + { + $this->message->setKey($key); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_message_given', $this->messageHandle, 'item exist', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + if (is_string($key)) { + $calls[] = ['pactffi_set_key', $this->messageHandle, $key, $success]; + } + if (!$success) { + $this->expectException(InteractionKeyNotSetException::class); + $this->expectExceptionMessage("Can not set the key '$key' for the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } diff --git a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php index 9dfd54a9..4bd0d342 100644 --- a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php @@ -8,12 +8,15 @@ use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\PactFileNotWroteException; use PhpPact\FFI\ClientInterface; +use PhpPactTest\Helper\FFI\ClientTrait; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PactDriverTest extends TestCase { + use ClientTrait; + protected const SPEC_UNKNOWN = 0; protected const SPEC_V1 = 1; protected const SPEC_V1_1 = 2; @@ -21,7 +24,6 @@ class PactDriverTest extends TestCase protected const SPEC_V3 = 4; protected const SPEC_V4 = 5; protected PactDriverInterface $driver; - protected ClientInterface|MockObject $client; protected PactConfigInterface|MockObject $config; protected int $pactHandle = 123; protected string $consumer = 'consumer'; @@ -152,18 +154,4 @@ protected function assertConfig(?string $logLevel, string $version): void ->method('getProvider') ->willReturn($this->provider); } - - protected function assertClientCalls(array $calls): void - { - $this->client - ->expects($this->exactly(count($calls))) - ->method('call') - ->willReturnCallback(function (...$args) use (&$calls) { - $call = array_shift($calls); - $return = array_pop($call); - $this->assertSame($call, $args); - - return $return; - }); - } } diff --git a/tests/PhpPact/Helper/FFI/ClientTrait.php b/tests/PhpPact/Helper/FFI/ClientTrait.php new file mode 100644 index 00000000..1faa1683 --- /dev/null +++ b/tests/PhpPact/Helper/FFI/ClientTrait.php @@ -0,0 +1,25 @@ +client + ->expects($this->exactly(count($calls))) + ->method('call') + ->willReturnCallback(function (...$args) use (&$calls) { + $call = array_shift($calls); + $return = array_pop($call); + $this->assertSame($call, $args); + + return $return; + }); + } +} diff --git a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php index 0d306f94..e5240489 100644 --- a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php +++ b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; +use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\Pact\Pact; @@ -11,14 +12,17 @@ use PhpPact\Standalone\MockService\Model\VerifyResult; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriver; use PhpPact\SyncMessage\Driver\Interaction\SyncMessageDriverInterface; +use PhpPactTest\Helper\FFI\ClientTrait; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SyncMessageDriverTest extends TestCase { + use ClientTrait; + private SyncMessageDriverInterface $driver; private MockServerInterface|MockObject $mockServer; - private ClientInterface|MockObject $client; private PactDriverInterface|MockObject $pactDriver; private MessageBodyDriverInterface|MockObject $messageBodyDriver; private Message $message; @@ -91,17 +95,39 @@ public function testRegisterInteraction(): void ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], ]; - $this->client - ->expects($this->exactly(count($calls))) - ->method('call') - ->willReturnCallback(function (...$args) use (&$calls) { - $call = array_shift($calls); - $return = array_pop($call); - $this->assertSame($call, $args); - - return $return; - }); + $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); $this->assertSame($this->messageHandle, $this->message->getHandle()); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith(['123ABC', false])] + #[TestWith(['123ABC', true])] + public function testSetKey(?string $key, $success): void + { + $this->message->setKey($key); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_sync_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_given', $this->messageHandle, 'item exist', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + if (is_string($key)) { + $calls[] = ['pactffi_set_key', $this->messageHandle, $key, $success]; + } + if (!$success) { + $this->expectException(InteractionKeyNotSetException::class); + $this->expectExceptionMessage("Can not set the key '$key' for the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } From 9c9b13ebd95e3554c5f0d09d64d0e525426f8711 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 1 Mar 2024 22:05:21 +0700 Subject: [PATCH 255/298] feat: Allow mark interaction as pending for v4 --- .../tests/Context/V4/Http/ConsumerContext.php | 2 +- .../Context/V4/Message/ConsumerContext.php | 2 +- .../V4/SyncMessage/ConsumerContext.php | 2 +- .../Consumer/AbstractMessageBuilder.php | 10 ++++++ .../InteractionPendingNotSetException.php | 7 ++++ .../Driver/Interaction/AbstractDriver.php | 13 +++++++ .../Interaction/AbstractMessageDriver.php | 1 + .../Driver/Interaction/InteractionDriver.php | 1 + src/PhpPact/Consumer/InteractionBuilder.php | 10 ++++++ src/PhpPact/Consumer/Model/Interaction.php | 2 ++ .../Model/Interaction/PendingTrait.php | 20 +++++++++++ src/PhpPact/Consumer/Model/Message.php | 2 ++ .../Interaction/InteractionDriverTest.php | 32 +++++++++++++++++ .../Driver/Interaction/MessageDriverTest.php | 34 +++++++++++++++++++ .../Interaction/SyncMessageDriverTest.php | 34 +++++++++++++++++++ 15 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Exception/InteractionPendingNotSetException.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/PendingTrait.php diff --git a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php index 69075a63..85904be5 100644 --- a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php @@ -69,7 +69,7 @@ public function theFirstInteractionInThePactFileWillHave(string $name, string $v */ public function theHttpInteractionIsMarkedAsPending(): void { - throw new PendingException("Can't set interaction's pending using FFI call"); + $this->interaction->setPending(true); } /** diff --git a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php index cf9df913..408ebf97 100644 --- a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php @@ -59,7 +59,7 @@ public function aKeyOfIsSpecifiedForTheMessageInteraction(string $key): void */ public function theMessageInteractionIsMarkedAsPending(): void { - throw new PendingException("Can't set message's pending using FFI call"); + $this->message->setPending(true); } /** diff --git a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php index 727ce937..d52fdd7e 100644 --- a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php @@ -61,7 +61,7 @@ public function aKeyOfIsSpecifiedForTheSynchronousMessageInteraction(string $key */ public function theSynchronousMessageInteractionIsMarkedAsPending(): void { - throw new PendingException("Can't set sync message's pending using FFI call"); + $this->message->setPending(true); } /** diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index e84f9e17..95ef2e05 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -66,4 +66,14 @@ public function key(?string $key): self return $this; } + + /** + * Mark the message interaction as pending. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + */ + public function pending(?bool $pending): self + { + $this->message->setPending($pending); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Driver/Exception/InteractionPendingNotSetException.php b/src/PhpPact/Consumer/Driver/Exception/InteractionPendingNotSetException.php new file mode 100644 index 00000000..76f0ec04 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Exception/InteractionPendingNotSetException.php @@ -0,0 +1,7 @@ +getDescription())); } } + + protected function setPending(Interaction|Message $interaction): void + { + $pending = $interaction->getPending(); + if (null === $pending) { + return; + } + $success = $this->client->call('pactffi_set_pending', $interaction->getHandle(), $pending); + if (!$success) { + throw new InteractionPendingNotSetException(sprintf("Can not mark interaction '%s' as pending", $interaction->getDescription())); + } + } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php index 55a5fdc1..094d4809 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php @@ -29,6 +29,7 @@ public function registerMessage(Message $message): void $this->withMetadata($message); $this->withContents($message); $this->setKey($message); + $this->setPending($message); } public function writePactAndCleanUp(): void diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index f10829e0..7bee5ef4 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -42,6 +42,7 @@ public function registerInteraction(Interaction $interaction, bool $startMockSer $this->withRequest($interaction); $this->willRespondWith($interaction); $this->setKey($interaction); + $this->setPending($interaction); if ($startMockServer) { $this->mockServer->start(); diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 5249ba03..edd14d5b 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -85,4 +85,14 @@ public function key(?string $key): self return $this; } + + /** + * Mark the interaction as pending. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + */ + public function pending(?bool $pending): self + { + $this->interaction->setPending($pending); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 97aa0c7f..1dd564e8 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -9,6 +9,7 @@ use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; use PhpPact\Consumer\Model\Interaction\KeyTrait; +use PhpPact\Consumer\Model\Interaction\PendingTrait; /** * Request/Response Pair to be posted to the Mock Server for PACT tests. @@ -19,6 +20,7 @@ class Interaction use DescriptionTrait; use HandleTrait; use KeyTrait; + use PendingTrait; private ConsumerRequest $request; diff --git a/src/PhpPact/Consumer/Model/Interaction/PendingTrait.php b/src/PhpPact/Consumer/Model/Interaction/PendingTrait.php new file mode 100644 index 00000000..1123c7cf --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/PendingTrait.php @@ -0,0 +1,20 @@ +pending; + } + + public function setPending(?bool $pending): self + { + $this->pending = $pending; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 43e24bb6..2765af83 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -11,6 +11,7 @@ use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; use PhpPact\Consumer\Model\Interaction\KeyTrait; +use PhpPact\Consumer\Model\Interaction\PendingTrait; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -21,6 +22,7 @@ class Message use DescriptionTrait; use HandleTrait; use KeyTrait; + use PendingTrait; /** * @var array diff --git a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php index 59cc2eb3..8cbe4c02 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; +use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Interaction\InteractionDriver; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Driver\InteractionPart\RequestDriverInterface; @@ -133,4 +134,35 @@ public function testSetKey(?string $key, $success): void $this->assertClientCalls($calls); $this->driver->registerInteraction($this->interaction, false); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith([true, false])] + #[TestWith([true, true])] + #[TestWith([false, false])] + #[TestWith([false, true])] + public function testSetPending(?bool $pending, $success): void + { + $this->interaction->setPending($pending); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_interaction', $this->pactHandle, $this->description, $this->interactionHandle], + ['pactffi_given', $this->interactionHandle, 'item exist', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'name', 'abc', null], + ['pactffi_upon_receiving', $this->interactionHandle, $this->description, null], + ]; + if (is_bool($pending)) { + $calls[] = ['pactffi_set_pending', $this->interactionHandle, $pending, $success]; + } + if (!$success) { + $this->expectException(InteractionPendingNotSetException::class); + $this->expectExceptionMessage("Can not mark interaction '{$this->description}' as pending"); + } + $this->assertClientCalls($calls); + $this->driver->registerInteraction($this->interaction, false); + } } diff --git a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php index 91b851fb..1bac2093 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php @@ -4,6 +4,7 @@ use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; +use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Interaction\MessageDriver; use PhpPact\Consumer\Driver\Interaction\MessageDriverInterface; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; @@ -128,4 +129,37 @@ public function testSetKey(?string $key, $success): void $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith([true, false])] + #[TestWith([true, true])] + #[TestWith([false, false])] + #[TestWith([false, true])] + public function testSetPending(?bool $pending, $success): void + { + $this->message->setPending($pending); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_message_given', $this->messageHandle, 'item exist', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + if (is_bool($pending)) { + $calls[] = ['pactffi_set_pending', $this->messageHandle, $pending, $success]; + } + if (!$success) { + $this->expectException(InteractionPendingNotSetException::class); + $this->expectExceptionMessage("Can not mark interaction '{$this->description}' as pending"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } diff --git a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php index e5240489..80efef5a 100644 --- a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php +++ b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php @@ -4,6 +4,7 @@ use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; +use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\Pact\Pact; @@ -130,4 +131,37 @@ public function testSetKey(?string $key, $success): void $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); } + + #[TestWith([null, true])] + #[TestWith([null, true])] + #[TestWith([true, false])] + #[TestWith([true, true])] + #[TestWith([false, false])] + #[TestWith([false, true])] + public function testSetPending(?bool $pending, $success): void + { + $this->message->setPending($pending); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_sync_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_given', $this->messageHandle, 'item exist', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + if (is_bool($pending)) { + $calls[] = ['pactffi_set_pending', $this->messageHandle, $pending, $success]; + } + if (!$success) { + $this->expectException(InteractionPendingNotSetException::class); + $this->expectExceptionMessage("Can not mark interaction '{$this->description}' as pending"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } From 6366b408c2220c1fc4909c157eb16bddd98d48a6 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 1 Mar 2024 23:16:13 +0700 Subject: [PATCH 256/298] feat: Allow set comments for interaction in v4 --- .../tests/Context/V4/Http/ConsumerContext.php | 4 +-- .../Context/V4/Message/ConsumerContext.php | 4 +-- .../V4/SyncMessage/ConsumerContext.php | 4 +-- .../Consumer/AbstractMessageBuilder.php | 12 +++++++ .../InteractionCommentNotSetException.php | 7 ++++ .../Driver/Interaction/AbstractDriver.php | 11 +++++++ .../Interaction/AbstractMessageDriver.php | 1 + .../Driver/Interaction/InteractionDriver.php | 1 + src/PhpPact/Consumer/InteractionBuilder.php | 12 +++++++ src/PhpPact/Consumer/Model/Interaction.php | 2 ++ .../Model/Interaction/CommentsTrait.php | 32 +++++++++++++++++++ src/PhpPact/Consumer/Model/Message.php | 2 ++ .../Interaction/InteractionDriverTest.php | 29 +++++++++++++++++ .../Driver/Interaction/MessageDriverTest.php | 31 ++++++++++++++++++ .../Interaction/SyncMessageDriverTest.php | 31 ++++++++++++++++++ 15 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Exception/InteractionCommentNotSetException.php create mode 100644 src/PhpPact/Consumer/Model/Interaction/CommentsTrait.php diff --git a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php index 85904be5..46d4de04 100644 --- a/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Http/ConsumerContext.php @@ -61,7 +61,7 @@ public function aKeyOfIsSpecifiedForTheHttpInteraction(string $key): void public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); + Assert::assertJsonStringEqualsJsonString($value, json_encode($pact['interactions'][0][$name])); } /** @@ -77,7 +77,7 @@ public function theHttpInteractionIsMarkedAsPending(): void */ public function aCommentIsAddedToTheHttpInteraction(string $value): void { - throw new PendingException("Can't set interaction's comment using FFI call"); + $this->interaction->setComments(['text' => json_encode([$value])]); } /** diff --git a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php index 408ebf97..21cd5799 100644 --- a/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/Message/ConsumerContext.php @@ -67,7 +67,7 @@ public function theMessageInteractionIsMarkedAsPending(): void */ public function aCommentIsAddedToTheMessageInteraction(string $value): void { - throw new PendingException("Can't set message's comment using FFI call"); + $this->message->setComments(['text' => json_encode([$value])]); } /** @@ -76,6 +76,6 @@ public function aCommentIsAddedToTheMessageInteraction(string $value): void public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); + Assert::assertJsonStringEqualsJsonString($value, json_encode($pact['interactions'][0][$name])); } } diff --git a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php index d52fdd7e..c812108c 100644 --- a/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php +++ b/compatibility-suite/tests/Context/V4/SyncMessage/ConsumerContext.php @@ -69,7 +69,7 @@ public function theSynchronousMessageInteractionIsMarkedAsPending(): void */ public function aCommentIsAddedToTheSynchronousMessageInteraction(string $value): void { - throw new PendingException("Can't set message's comment using FFI call"); + $this->message->setComments(['text' => json_encode([$value])]); } /** @@ -78,7 +78,7 @@ public function aCommentIsAddedToTheSynchronousMessageInteraction(string $value) public function theFirstInteractionInThePactFileWillHave(string $name, string $value): void { $pact = json_decode(file_get_contents($this->pactPath), true); - Assert::assertSame(json_decode($value), $pact['interactions'][0][$name]); + Assert::assertJsonStringEqualsJsonString($value, json_encode($pact['interactions'][0][$name])); } /** diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index 95ef2e05..68d5613b 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -76,4 +76,16 @@ public function pending(?bool $pending): self return $this; } + + /** + * Add comments to the message interaction. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + * + * @param array $comments + */ + public function comments(array $comments): self + { + $this->message->setComments($comments); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Driver/Exception/InteractionCommentNotSetException.php b/src/PhpPact/Consumer/Driver/Exception/InteractionCommentNotSetException.php new file mode 100644 index 00000000..9649ac68 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Exception/InteractionCommentNotSetException.php @@ -0,0 +1,7 @@ +getDescription())); } } + + protected function setComments(Interaction|Message $interaction): void + { + foreach ($interaction->getComments() as $key => $value) { + $success = $this->client->call('pactffi_set_comment', $interaction->getHandle(), $key, $value); + if (!$success) { + throw new InteractionCommentNotSetException(sprintf("Can add comment '%s' to the interaction '%s'", $key, $interaction->getDescription())); + } + } + } } diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php index 094d4809..938ef19d 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php @@ -30,6 +30,7 @@ public function registerMessage(Message $message): void $this->withContents($message); $this->setKey($message); $this->setPending($message); + $this->setComments($message); } public function writePactAndCleanUp(): void diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index 7bee5ef4..f76d8187 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -43,6 +43,7 @@ public function registerInteraction(Interaction $interaction, bool $startMockSer $this->willRespondWith($interaction); $this->setKey($interaction); $this->setPending($interaction); + $this->setComments($interaction); if ($startMockServer) { $this->mockServer->start(); diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index edd14d5b..728ca7a3 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -95,4 +95,16 @@ public function pending(?bool $pending): self return $this; } + + /** + * Add comments to the interaction. This feature only work with specification v4. It doesn't affect pact file with specification <= v3. + * + * @param array $comments + */ + public function comments(array $comments): self + { + $this->interaction->setComments($comments); + + return $this; + } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 1dd564e8..ae2617de 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -6,6 +6,7 @@ use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; +use PhpPact\Consumer\Model\Interaction\CommentsTrait; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; use PhpPact\Consumer\Model\Interaction\KeyTrait; @@ -21,6 +22,7 @@ class Interaction use HandleTrait; use KeyTrait; use PendingTrait; + use CommentsTrait; private ConsumerRequest $request; diff --git a/src/PhpPact/Consumer/Model/Interaction/CommentsTrait.php b/src/PhpPact/Consumer/Model/Interaction/CommentsTrait.php new file mode 100644 index 00000000..fa0e3ed2 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/CommentsTrait.php @@ -0,0 +1,32 @@ + + */ + private array $comments = []; + + /** + * @return array + */ + public function getComments(): array + { + return $this->comments; + } + + /** + * @param array $comments + */ + public function setComments(array $comments): self + { + $this->comments = []; + foreach ($comments as $key => $value) { + $this->comments[$key] = $value; + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 2765af83..dd4b8e0e 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -8,6 +8,7 @@ use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; +use PhpPact\Consumer\Model\Interaction\CommentsTrait; use PhpPact\Consumer\Model\Interaction\DescriptionTrait; use PhpPact\Consumer\Model\Interaction\HandleTrait; use PhpPact\Consumer\Model\Interaction\KeyTrait; @@ -23,6 +24,7 @@ class Message use HandleTrait; use KeyTrait; use PendingTrait; + use CommentsTrait; /** * @var array diff --git a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php index 8cbe4c02..49513a52 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/InteractionDriverTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; +use PhpPact\Consumer\Driver\Exception\InteractionCommentNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Interaction\InteractionDriver; @@ -165,4 +166,32 @@ public function testSetPending(?bool $pending, $success): void $this->assertClientCalls($calls); $this->driver->registerInteraction($this->interaction, false); } + + #[TestWith([[], true])] + #[TestWith([['key1' => 'value1'], false])] + #[TestWith([['key2' => 'value2', 'key3' => 'value3'], true])] + public function testSetComments(array $comments, $success): void + { + $this->interaction->setComments($comments); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_interaction', $this->pactHandle, $this->description, $this->interactionHandle], + ['pactffi_given', $this->interactionHandle, 'item exist', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->interactionHandle, 'item exist', 'name', 'abc', null], + ['pactffi_upon_receiving', $this->interactionHandle, $this->description, null], + ]; + foreach ($comments as $key => $value) { + $calls[] = ['pactffi_set_comment', $this->interactionHandle, $key, $value, $success]; + } + if (!$success) { + $this->expectException(InteractionCommentNotSetException::class); + $this->expectExceptionMessage("Can add comment '$key' to the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerInteraction($this->interaction, false); + } } diff --git a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php index 1bac2093..ebde5c30 100644 --- a/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Interaction/MessageDriverTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; +use PhpPact\Consumer\Driver\Exception\InteractionCommentNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Interaction\MessageDriver; @@ -162,4 +163,34 @@ public function testSetPending(?bool $pending, $success): void $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); } + + #[TestWith([[], true])] + #[TestWith([['key1' => 'value1'], false])] + #[TestWith([['key2' => 'value2', 'key3' => 'value3'], true])] + public function testSetComments(array $comments, $success): void + { + $this->message->setComments($comments); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_message_given', $this->messageHandle, 'item exist', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_message_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + foreach ($comments as $key => $value) { + $calls[] = ['pactffi_set_comment', $this->messageHandle, $key, $value, $success]; + } + if (!$success) { + $this->expectException(InteractionCommentNotSetException::class); + $this->expectExceptionMessage("Can add comment '$key' to the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } diff --git a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php index 80efef5a..e449af2b 100644 --- a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php +++ b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Driver\Interaction; use PhpPact\Consumer\Driver\Body\MessageBodyDriverInterface; +use PhpPact\Consumer\Driver\Exception\InteractionCommentNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionKeyNotSetException; use PhpPact\Consumer\Driver\Exception\InteractionPendingNotSetException; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; @@ -164,4 +165,34 @@ public function testSetPending(?bool $pending, $success): void $this->assertClientCalls($calls); $this->driver->registerMessage($this->message); } + + #[TestWith([[], true])] + #[TestWith([['key1' => 'value1'], false])] + #[TestWith([['key2' => 'value2', 'key3' => 'value3'], true])] + public function testSetComments(array $comments, $success): void + { + $this->message->setComments($comments); + $this->pactDriver + ->expects($this->once()) + ->method('getPact') + ->willReturn(new Pact($this->pactHandle)); + $calls = [ + ['pactffi_new_sync_message_interaction', $this->pactHandle, $this->description, $this->messageHandle], + ['pactffi_given', $this->messageHandle, 'item exist', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'id', '12', null], + ['pactffi_given_with_param', $this->messageHandle, 'item exist', 'name', 'abc', null], + ['pactffi_message_expects_to_receive', $this->messageHandle, $this->description, null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key1', 'value1', null], + ['pactffi_message_with_metadata_v2', $this->messageHandle, 'key2', 'value2', null], + ]; + foreach ($comments as $key => $value) { + $calls[] = ['pactffi_set_comment', $this->messageHandle, $key, $value, $success]; + } + if (!$success) { + $this->expectException(InteractionCommentNotSetException::class); + $this->expectExceptionMessage("Can add comment '$key' to the interaction '{$this->description}'"); + } + $this->assertClientCalls($calls); + $this->driver->registerMessage($this->message); + } } From 8a927101e21fa5a7e89b85296dcd2c0d4b06b87c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 22 Feb 2024 15:16:45 +0700 Subject: [PATCH 257/298] ci: Cache pact plugins --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index feceb4fb..825d77c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,6 +111,13 @@ jobs: run: composer gen-lib if: ${{ !matrix.example || contains(matrix.example, 'protobuf') }} + - name: Cache Pact Plugins + uses: actions/cache@v4 + with: + path: ~/.pact/plugins + key: ${{ matrix.os }}-pact-plugins + if: ${{ !matrix.example || matrix.example == 'csv' || contains(matrix.example, 'protobuf') }} + - name: Run example(s) run: composer test -- --exclude-testsuite unit ${{ matrix.example && format('--testsuite {0}-example', matrix.example) || '' }} env: From c30911dc76f4d4a0a3744d6570ce5c51e74ca637 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 3 Mar 2024 07:51:20 +0700 Subject: [PATCH 258/298] ci: Update composer install action to avoid deprecation notices --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index feceb4fb..2f287a86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: php-version: 8.1 coverage: none - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 with: dependency-versions: 'locked' @@ -97,7 +97,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Composer install - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 with: dependency-versions: 'locked' @@ -138,7 +138,7 @@ jobs: coverage: pcov - name: Composer install - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 with: dependency-versions: ${{ matrix.dependencies }} From 07029ba5bd64ff6277e20c8eb1fa548f49264e27 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sun, 3 Mar 2024 23:49:17 +0700 Subject: [PATCH 259/298] ci: Update PHP to 8.3 for MacOS 12 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5465c529..5ef61d52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: - os: ubuntu-latest php: 8.1 - os: macos-12 - php: 8.2 + php: 8.3 - os: macos-14 php: 8.2 - os: windows-latest From 6d58cf25c49613f13df3eabb4d22a09ecb34673a Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 4 Mar 2024 13:40:05 +0700 Subject: [PATCH 260/298] fix: Fix unknown specification not working --- src/PhpPact/Consumer/Driver/Pact/PactDriver.php | 4 ++-- tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index a7baf923..9a120f32 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -63,11 +63,11 @@ protected function getSpecification(): int $this->versionEqualTo('2.0.0') => $this->client->get('PactSpecification_V2'), $this->versionEqualTo('3.0.0') => $this->client->get('PactSpecification_V3'), $this->versionEqualTo('4.0.0') => $this->client->get('PactSpecification_V4'), - default => function () { + default => call_user_func(function () { trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); return $this->client->get('PactSpecification_Unknown'); - }, + }), }; } diff --git a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php index 4bd0d342..5d8b7219 100644 --- a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php @@ -55,6 +55,8 @@ public function setUp(): void #[TestWith(['error', '1.0.0', self::SPEC_V1])] #[TestWith(['off' , '1.1.0', self::SPEC_V1_1])] #[TestWith(['none' , '2.0.0', self::SPEC_V2])] + #[TestWith([null , '0.1.2', self::SPEC_UNKNOWN])] + #[TestWith([null , 'x.y.z', self::SPEC_UNKNOWN])] public function testSetUp(?string $logLevel, string $version, int $specificationHandle): void { $this->assertConfig($logLevel, $version); From 64a699d9685aa6d84032e3172944266e442dbb12 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 6 Feb 2024 19:28:43 +0700 Subject: [PATCH 261/298] test(compatibility-suite): Fix compatibility suite v1 hang --- compatibility-suite/tests/Service/PactBroker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compatibility-suite/tests/Service/PactBroker.php b/compatibility-suite/tests/Service/PactBroker.php index 4887e749..192ccce9 100644 --- a/compatibility-suite/tests/Service/PactBroker.php +++ b/compatibility-suite/tests/Service/PactBroker.php @@ -26,7 +26,7 @@ public function publish(int $id): void public function start(): void { - exec('docker run --rm --publish 9292:9292 --detach --env PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 --name pact-broker pactfoundation/pact-broker:latest'); + exec('docker run --rm --publish 9292:9292 --detach --env PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 --name pact-broker pactfoundation/pact-broker:2.117.1-pactbroker2.109.1'); while (true) { try { $response = $this->client->get('http://localhost:9292/diagnostic/status/heartbeat', ['http_errors' => false]); From 62d40f05bfa432692e8e701e968ba8404019f682 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 26 Feb 2024 12:07:38 +0700 Subject: [PATCH 262/298] feat: Allow sending csv body in request --- .../tests/Service/HttpClientServiceTest.php | 3 +- .../Plugins/Csv/Exception/CsvException.php | 9 ++ .../Exception/MissingPluginPartsException.php | 7 ++ .../Factory/CsvInteractionDriverFactory.php | 21 ++++- .../CsvInteractionDriverFactoryTest.php | 94 +++++++++++++++++++ 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/PhpPact/Plugins/Csv/Exception/CsvException.php create mode 100644 src/PhpPact/Plugins/Csv/Exception/MissingPluginPartsException.php create mode 100644 tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php diff --git a/example/csv/consumer/tests/Service/HttpClientServiceTest.php b/example/csv/consumer/tests/Service/HttpClientServiceTest.php index 7f7a135c..153fa029 100644 --- a/example/csv/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/csv/consumer/tests/Service/HttpClientServiceTest.php @@ -3,6 +3,7 @@ namespace CsvConsumer\Tests\Service; use CsvConsumer\Service\HttpClientService; +use PhpPact\Consumer\Driver\Enum\InteractionPart; use PhpPact\Consumer\InteractionBuilder; use PhpPact\Consumer\Matcher\Formatters\PluginFormatter; use PhpPact\Consumer\Matcher\Matcher; @@ -49,7 +50,7 @@ public function testGetCsvFile(): void if ($logLevel = \getenv('PACT_LOGLEVEL')) { $config->setLogLevel($logLevel); } - $builder = new InteractionBuilder($config, new CsvInteractionDriverFactory()); + $builder = new InteractionBuilder($config, new CsvInteractionDriverFactory(InteractionPart::RESPONSE)); $builder ->given('report.csv file exist') ->uponReceiving('request for a report.csv') diff --git a/src/PhpPact/Plugins/Csv/Exception/CsvException.php b/src/PhpPact/Plugins/Csv/Exception/CsvException.php new file mode 100644 index 00000000..70708791 --- /dev/null +++ b/src/PhpPact/Plugins/Csv/Exception/CsvException.php @@ -0,0 +1,9 @@ +pluginParts = $pluginParts; + } + public function create(MockServerConfigInterface $config): InteractionDriverInterface { $client = new Client(); $pactDriver = new CsvPactDriver($client, $config); $mockServer = new MockServer($client, $pactDriver, $config); $csvBodyDriver = new CsvBodyDriver(new PluginBodyDriver($client)); - $responseDriver = new ResponseDriver($client, $csvBodyDriver); + $requestDriver = in_array(InteractionPart::REQUEST, $this->pluginParts) ? new RequestDriver($client, $csvBodyDriver) : null; + $responseDriver = in_array(InteractionPart::RESPONSE, $this->pluginParts) ? new ResponseDriver($client, $csvBodyDriver) : null; - return new InteractionDriver($client, $mockServer, $pactDriver, null, $responseDriver); + return new InteractionDriver($client, $mockServer, $pactDriver, $requestDriver, $responseDriver); } } diff --git a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php new file mode 100644 index 00000000..7e4b81c7 --- /dev/null +++ b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php @@ -0,0 +1,94 @@ +config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('4.0.0'); + } + + #[TestWith([InteractionPart::REQUEST])] + #[TestWith([InteractionPart::RESPONSE])] + #[TestWith([InteractionPart::REQUEST, InteractionPart::RESPONSE])] + public function testCreate(InteractionPart ...$pluginParts): void + { + $this->factory = new CsvInteractionDriverFactory(...$pluginParts); + $driver = $this->factory->create($this->config); + $requestBodyDriver = $this->getBodyDriver($this->getRequestDriver($driver)); + if (in_array(InteractionPart::REQUEST, $pluginParts)) { + $this->assertCsvBodyDriver($requestBodyDriver); + } else { + $this->assertNotCsvBodyDriver($requestBodyDriver); + } + $responseBodyDriver = $this->getBodyDriver($this->getResponseDriver($driver)); + if (in_array(InteractionPart::RESPONSE, $pluginParts)) { + $this->assertCsvBodyDriver($responseBodyDriver); + } else { + $this->assertNotCsvBodyDriver($responseBodyDriver); + } + } + + public function testMissingPluginPartsException(): void + { + $this->expectException(MissingPluginPartsException::class); + $this->expectExceptionMessage('At least 1 interaction part must be csv'); + $this->factory = new CsvInteractionDriverFactory(); + $this->factory->create($this->config); + } + + private function getRequestDriver(InteractionDriverInterface $driver): RequestDriverInterface + { + $reflection = new ReflectionProperty($driver, 'requestDriver'); + + return $reflection->getValue($driver); + } + + private function getResponseDriver(InteractionDriverInterface $driver): ResponseDriverInterface + { + $reflection = new ReflectionProperty($driver, 'responseDriver'); + + return $reflection->getValue($driver); + } + + private function assertCsvBodyDriver(InteractionBodyDriverInterface $bodyDriver): void + { + $this->assertInstanceOf(CsvBodyDriver::class, $bodyDriver); + } + + private function assertNotCsvBodyDriver(InteractionBodyDriverInterface $bodyDriver): void + { + $this->assertNotInstanceOf(CsvBodyDriver::class, $bodyDriver); + } + + private function getBodyDriver(RequestDriverInterface|ResponseDriverInterface $interactionPartDriver): InteractionBodyDriverInterface + { + $reflection = new ReflectionProperty(AbstractInteractionPartDriver::class, 'bodyDriver'); + + return $reflection->getValue($interactionPartDriver); + } +} From 7476d675dbfb44854aa20a32b44decd9baedeaf1 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 5 Mar 2024 11:24:34 +0700 Subject: [PATCH 263/298] test: Move reusable test method into trait --- tests/PhpPact/Helper/FactoryTrait.php | 20 +++++++ .../CsvInteractionDriverFactoryTest.php | 56 +++++++++---------- 2 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 tests/PhpPact/Helper/FactoryTrait.php diff --git a/tests/PhpPact/Helper/FactoryTrait.php b/tests/PhpPact/Helper/FactoryTrait.php new file mode 100644 index 00000000..7b2c1cb0 --- /dev/null +++ b/tests/PhpPact/Helper/FactoryTrait.php @@ -0,0 +1,20 @@ + $properties + */ + private function assertPropertiesInstanceOf(object $object, ?string $class, array $properties): void + { + foreach ($properties as $property => $propertyClass) { + $reflection = new ReflectionProperty($class ?? $object, $property); + $value = $reflection->getValue($object); + $this->assertInstanceOf($propertyClass, $value); + } + } +} diff --git a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php index 7e4b81c7..ebed1674 100644 --- a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php +++ b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php @@ -2,17 +2,23 @@ namespace PhpPactTest\Plugins\Csv\Factory; -use PhpPact\Consumer\Driver\Body\InteractionBodyDriverInterface; +use PhpPact\Consumer\Driver\Body\InteractionBodyDriver; use PhpPact\Consumer\Driver\Enum\InteractionPart; use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; use PhpPact\Consumer\Driver\InteractionPart\AbstractInteractionPartDriver; +use PhpPact\Consumer\Driver\InteractionPart\RequestDriver; use PhpPact\Consumer\Driver\InteractionPart\RequestDriverInterface; +use PhpPact\Consumer\Driver\InteractionPart\ResponseDriver; use PhpPact\Consumer\Driver\InteractionPart\ResponseDriverInterface; use PhpPact\Consumer\Factory\InteractionDriverFactoryInterface; +use PhpPact\Consumer\Service\MockServer; +use PhpPact\FFI\Client; use PhpPact\Plugins\Csv\Driver\Body\CsvBodyDriver; +use PhpPact\Plugins\Csv\Driver\Pact\CsvPactDriver; use PhpPact\Plugins\Csv\Exception\MissingPluginPartsException; use PhpPact\Plugins\Csv\Factory\CsvInteractionDriverFactory; use PhpPact\Standalone\MockService\MockServerConfigInterface; +use PhpPactTest\Helper\FactoryTrait; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -20,6 +26,8 @@ class CsvInteractionDriverFactoryTest extends TestCase { + use FactoryTrait; + private InteractionDriverFactoryInterface $factory; private MockServerConfigInterface|MockObject $config; @@ -39,18 +47,23 @@ public function testCreate(InteractionPart ...$pluginParts): void { $this->factory = new CsvInteractionDriverFactory(...$pluginParts); $driver = $this->factory->create($this->config); - $requestBodyDriver = $this->getBodyDriver($this->getRequestDriver($driver)); - if (in_array(InteractionPart::REQUEST, $pluginParts)) { - $this->assertCsvBodyDriver($requestBodyDriver); - } else { - $this->assertNotCsvBodyDriver($requestBodyDriver); - } - $responseBodyDriver = $this->getBodyDriver($this->getResponseDriver($driver)); - if (in_array(InteractionPart::RESPONSE, $pluginParts)) { - $this->assertCsvBodyDriver($responseBodyDriver); - } else { - $this->assertNotCsvBodyDriver($responseBodyDriver); - } + $this->assertPropertiesInstanceOf($driver, null, [ + 'client' => Client::class, + 'mockServer' => MockServer::class, + 'pactDriver' => CsvPactDriver::class, + 'requestDriver' => RequestDriver::class, + 'responseDriver' => ResponseDriver::class, + ]); + $requestDriver = $this->getRequestDriver($driver); + $this->assertPropertiesInstanceOf($requestDriver, AbstractInteractionPartDriver::class, [ + 'client' => Client::class, + 'bodyDriver' => in_array(InteractionPart::REQUEST, $pluginParts) ? CsvBodyDriver::class : InteractionBodyDriver::class, + ]); + $responseDriver = $this->getResponseDriver($driver); + $this->assertPropertiesInstanceOf($responseDriver, AbstractInteractionPartDriver::class, [ + 'client' => Client::class, + 'bodyDriver' => in_array(InteractionPart::RESPONSE, $pluginParts) ? CsvBodyDriver::class : InteractionBodyDriver::class, + ]); } public function testMissingPluginPartsException(): void @@ -74,21 +87,4 @@ private function getResponseDriver(InteractionDriverInterface $driver): Response return $reflection->getValue($driver); } - - private function assertCsvBodyDriver(InteractionBodyDriverInterface $bodyDriver): void - { - $this->assertInstanceOf(CsvBodyDriver::class, $bodyDriver); - } - - private function assertNotCsvBodyDriver(InteractionBodyDriverInterface $bodyDriver): void - { - $this->assertNotInstanceOf(CsvBodyDriver::class, $bodyDriver); - } - - private function getBodyDriver(RequestDriverInterface|ResponseDriverInterface $interactionPartDriver): InteractionBodyDriverInterface - { - $reflection = new ReflectionProperty(AbstractInteractionPartDriver::class, 'bodyDriver'); - - return $reflection->getValue($interactionPartDriver); - } } From 0ac6e12ee586425b4e8ebe8166c26d237de71e7f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 26 Feb 2024 11:21:27 +0700 Subject: [PATCH 264/298] feat: Allow to build multiple interactions --- src/PhpPact/Consumer/InteractionBuilder.php | 10 +- .../Consumer/InteractionBuilderTest.php | 200 ++++++++++-------- 2 files changed, 114 insertions(+), 96 deletions(-) diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 728ca7a3..614eb8f2 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -21,6 +21,11 @@ class InteractionBuilder implements BuilderInterface public function __construct(MockServerConfigInterface $config, ?InteractionDriverFactoryInterface $driverFactory = null) { $this->driver = ($driverFactory ?? new InteractionDriverFactory())->create($config); + $this->newInteraction(); + } + + public function newInteraction(): void + { $this->interaction = new Interaction(); } @@ -58,14 +63,15 @@ public function with(ConsumerRequest $request): self /** * @param ProviderResponse $response mock of response received + * @param bool $startMockServer start mock server. Can't register more interaction if mock server is started * * @return bool returns true on success */ - public function willRespondWith(ProviderResponse $response): bool + public function willRespondWith(ProviderResponse $response, bool $startMockServer = true): bool { $this->interaction->setResponse($response); - return $this->driver->registerInteraction($this->interaction); + return $this->driver->registerInteraction($this->interaction, $startMockServer); } /** diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index 6d97ccbf..a067cea2 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -2,124 +2,136 @@ namespace PhpPactTest\Consumer; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\Consumer\Factory\InteractionDriverFactoryInterface; use PhpPact\Consumer\InteractionBuilder; -use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\ConsumerRequest; +use PhpPact\Consumer\Model\Interaction; use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Standalone\Exception\MissingEnvVariableException; -use PhpPact\Standalone\MockService\MockServerEnvConfig; +use PhpPact\Consumer\Model\ProviderState; +use PhpPact\Standalone\MockService\MockServerConfigInterface; +use PhpPact\Standalone\MockService\Model\VerifyResult; +use PHPUnit\Framework\Attributes\TestWith; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ReflectionProperty; class InteractionBuilderTest extends TestCase { - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - public function testSimpleGet() - { - $matcher = new Matcher(); - - $request = new ConsumerRequest(); - $request - ->setPath('/something') - ->setMethod('GET') - ->addHeader('Content-Type', 'application/json'); + private InteractionBuilder $builder; + private InteractionDriverInterface|MockObject $driver; + private MockServerConfigInterface|MockObject $config; + private InteractionDriverFactoryInterface|MockObject $driverFactory; - $response = new ProviderResponse(); - $response - ->setStatus(200) - ->setBody([ - 'message' => 'Hello, world!', - 'age' => $matcher->like(73), - ]) - ->addHeader('Content-Type', 'application/json'); + public function setUp(): void + { + $this->driver = $this->createMock(InteractionDriverInterface::class); + $this->config = $this->createMock(MockServerConfigInterface::class); + $this->driverFactory = $this->createMock(InteractionDriverFactoryInterface::class); + $this->driverFactory + ->expects($this->once()) + ->method('create') + ->with($this->config) + ->willReturn($this->driver); + $this->builder = new InteractionBuilder($this->config, $this->driverFactory); + } - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $builder - ->given('A test request.', ['key' => 'value']) - ->uponReceiving('A test response.') - ->with($request) - ->willRespondWith($response); + public function testNewInteraction(): void + { + $oldInteraction = $this->getInteraction(); + $this->builder->newInteraction(); + $newInteraction = $this->getInteraction(); + $this->assertNotSame($oldInteraction, $newInteraction); + } - $verifyResult = $builder->verify(); + public function testGiven(): void + { + $this->assertSame($this->builder, $this->builder->given('test', ['key' => 'value'])); + $interaction = $this->getInteraction(); + $providerStates = $interaction->getProviderStates(); + $this->assertCount(1, $providerStates); + $providerState = $providerStates[0]; + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('test', $providerState->getName()); + $this->assertSame(['key' => 'value'], $providerState->getParams()); + } - $this->assertFalse($verifyResult); + public function testUponReceiving(): void + { + $description = 'interaction description'; + $this->assertSame($this->builder, $this->builder->uponReceiving($description)); + $interaction = $this->getInteraction(); + $this->assertSame($description, $interaction->getDescription()); } - /** - * @throws MissingEnvVariableException - */ - public function testPostWithBody() + public function testWithRequest(): void { $request = new ConsumerRequest(); - $request - ->setPath('/something') - ->setMethod('POST') - ->addHeader('Content-Type', 'application/json') - ->setBody([ - 'someStuff' => 'someOtherStuff', - 'someNumber' => 12, - 'anArray' => [ - 12, - 'words here', - 493.5, - ], - ]); + $this->assertSame($this->builder, $this->builder->with($request)); + $interaction = $this->getInteraction(); + $this->assertSame($request, $interaction->getRequest()); + } + #[TestWith([false, true])] + #[TestWith([true, true])] + #[TestWith([false, false])] + #[TestWith([true, false])] + public function testWillRespondWith(bool $startMockServer, bool $result): void + { $response = new ProviderResponse(); - $response - ->setStatus(200) - ->addHeader('Content-Type', 'application/json') - ->setBody([ - 'message' => 'Hello, world!', - ]); - - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $builder - ->given('A test request.', ['key' => 'value']) - ->uponReceiving('A test response.') - ->with($request) - ->willRespondWith($response); - - $verifyResult = $builder->verify(); - - $this->assertFalse($verifyResult); + $interaction = $this->getInteraction(); + $this->driver + ->expects($this->once()) + ->method('registerInteraction') + ->with($interaction, $startMockServer) + ->willReturn($result); + $this->assertSame($result, $this->builder->willRespondWith($response, $startMockServer)); + $this->assertSame($response, $interaction->getResponse()); } - /** - * @throws MissingEnvVariableException - */ - public function testBuildWithEachLikeMatcher() + #[TestWith([false])] + #[TestWith([true])] + public function testVerify(bool $matched): void { - $matcher = new Matcher(); + $this->driver + ->expects($this->once()) + ->method('verifyInteractions') + ->willReturn(new VerifyResult($matched, '')); + $this->assertSame($matched, $this->builder->verify()); + } - $request = new ConsumerRequest(); - $request - ->setPath('/something') - ->setMethod('GET') - ->addHeader('Content-Type', 'application/json'); + #[TestWith([null])] + #[TestWith(['key'])] + public function testSetKey(?string $key): void + { + $this->assertSame($this->builder, $this->builder->key($key)); + $interaction = $this->getInteraction(); + $this->assertSame($key, $interaction->getKey()); + } - $response = new ProviderResponse(); - $response - ->setStatus(200) - ->addHeader('Content-Type', 'application/json') - ->setBody([ - 'list' => $matcher->eachLike([ - 'test' => 1, - 'another' => 2, - ]), - ]); + #[TestWith([null])] + #[TestWith([false])] + #[TestWith([true])] + public function testSetPending(?bool $pending): void + { + $this->assertSame($this->builder, $this->builder->pending($pending)); + $interaction = $this->getInteraction(); + $this->assertSame($pending, $interaction->getPending()); + } - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $builder - ->given('A test request.', ['key' => 'value']) - ->uponReceiving('A test response.') - ->with($request) - ->willRespondWith($response); + #[TestWith([[]])] + #[TestWith([['key' => 'value']])] + public function testSetComments(array $comments): void + { + $this->assertSame($this->builder, $this->builder->comments($comments)); + $interaction = $this->getInteraction(); + $this->assertSame($comments, $interaction->getComments()); + } - $verifyResult = $builder->verify(); + private function getInteraction(): Interaction + { + $reflection = new ReflectionProperty($this->builder, 'interaction'); - $this->assertFalse($verifyResult); + return $reflection->getValue($this->builder); } } From d7d25883a3e7b61dcee590c2bc2dc31569ceafdd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 5 Mar 2024 08:11:40 +0700 Subject: [PATCH 265/298] ci(compatibility-suite): Use default PHP for ubuntu-latest (8.1) --- .github/workflows/compatibility-suite.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/compatibility-suite.yml b/.github/workflows/compatibility-suite.yml index 54b1194d..62a9abbe 100644 --- a/.github/workflows/compatibility-suite.yml +++ b/.github/workflows/compatibility-suite.yml @@ -15,7 +15,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.1 coverage: none - uses: ramsey/composer-install@v3 @@ -31,7 +31,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.1 coverage: none - uses: ramsey/composer-install@v3 @@ -47,7 +47,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.1 coverage: none - uses: ramsey/composer-install@v3 @@ -63,7 +63,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.1 coverage: none - uses: ramsey/composer-install@v3 From 1d5d1c1e6e3dd466e6b35f1493dd1aa6a6909d50 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 5 Mar 2024 13:47:46 +0000 Subject: [PATCH 266/298] Revert "test(compatibility-suite): Fix compatibility suite v1 hang" --- compatibility-suite/tests/Service/PactBroker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compatibility-suite/tests/Service/PactBroker.php b/compatibility-suite/tests/Service/PactBroker.php index 192ccce9..4887e749 100644 --- a/compatibility-suite/tests/Service/PactBroker.php +++ b/compatibility-suite/tests/Service/PactBroker.php @@ -26,7 +26,7 @@ public function publish(int $id): void public function start(): void { - exec('docker run --rm --publish 9292:9292 --detach --env PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 --name pact-broker pactfoundation/pact-broker:2.117.1-pactbroker2.109.1'); + exec('docker run --rm --publish 9292:9292 --detach --env PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 --name pact-broker pactfoundation/pact-broker:latest'); while (true) { try { $response = $this->client->get('http://localhost:9292/diagnostic/status/heartbeat', ['http_errors' => false]); From df463626c09c58688f4a34562446aabd9f7e8c74 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 6 Mar 2024 10:01:12 +0700 Subject: [PATCH 267/298] test: Test remaining factories --- .../Factory/InteractionDriverFactoryTest.php | 43 +++++++++++++++++ .../Factory/MessageDriverFactoryTest.php | 41 ++++++++++++++++ tests/PhpPact/Helper/FactoryTrait.php | 9 ++++ .../CsvInteractionDriverFactoryTest.php | 1 + .../Body/ProtobufMessageBodyDriverTest.php | 2 +- .../ProtobufMessageDriverFactoryTest.php | 43 +++++++++++++++++ .../ProtobufSyncMessageDriverFactoryTest.php | 47 +++++++++++++++++++ .../Interaction/SyncMessageDriverTest.php | 2 +- .../Factory/SyncMessageDriverFactoryTest.php | 43 +++++++++++++++++ 9 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php create mode 100644 tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php create mode 100644 tests/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactoryTest.php create mode 100644 tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php create mode 100644 tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php diff --git a/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php b/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php new file mode 100644 index 00000000..7e670bc3 --- /dev/null +++ b/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php @@ -0,0 +1,43 @@ +config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('3.0.0'); + } + + public function testCreate(): void + { + $this->factory = new InteractionDriverFactory(); + $driver = $this->factory->create($this->config); + $this->assertPropertiesInstanceOf($driver, null, [ + 'client' => Client::class, + 'mockServer' => MockServer::class, + 'pactDriver' => PactDriver::class, + ]); + $this->cleanUp($driver); + } +} diff --git a/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php b/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php new file mode 100644 index 00000000..d2c20c6e --- /dev/null +++ b/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php @@ -0,0 +1,41 @@ +config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('3.0.0'); + } + + public function testCreate(): void + { + $this->factory = new MessageDriverFactory(); + $driver = $this->factory->create($this->config); + $this->assertPropertiesInstanceOf($driver, null, [ + 'client' => Client::class, + 'pactDriver' => PactDriver::class, + ]); + $this->cleanUp($driver); + } +} diff --git a/tests/PhpPact/Helper/FactoryTrait.php b/tests/PhpPact/Helper/FactoryTrait.php index 7b2c1cb0..2e913e03 100644 --- a/tests/PhpPact/Helper/FactoryTrait.php +++ b/tests/PhpPact/Helper/FactoryTrait.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Helper; +use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use ReflectionProperty; trait FactoryTrait @@ -17,4 +18,12 @@ private function assertPropertiesInstanceOf(object $object, ?string $class, arra $this->assertInstanceOf($propertyClass, $value); } } + + private function cleanUp(object $driver): void + { + $reflection = new ReflectionProperty($driver, 'pactDriver'); + $pactDriver = $reflection->getValue($driver); + $this->assertInstanceOf(PactDriverInterface::class, $pactDriver); + $pactDriver->cleanUp(); + } } diff --git a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php index ebed1674..ba7048d4 100644 --- a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php +++ b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php @@ -64,6 +64,7 @@ public function testCreate(InteractionPart ...$pluginParts): void 'client' => Client::class, 'bodyDriver' => in_array(InteractionPart::RESPONSE, $pluginParts) ? CsvBodyDriver::class : InteractionBodyDriver::class, ]); + $this->cleanUp($driver); } public function testMissingPluginPartsException(): void diff --git a/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php b/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php index 4c72a0b2..d2b4f681 100644 --- a/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php +++ b/tests/PhpPact/Plugins/Protobuf/Driver/Body/ProtobufMessageBodyDriverTest.php @@ -1,6 +1,6 @@ config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('4.0.0'); + } + + public function testCreate(): void + { + $this->factory = new ProtobufMessageDriverFactory(); + $driver = $this->factory->create($this->config); + $this->assertPropertiesInstanceOf($driver, AbstractMessageDriver::class, [ + 'client' => Client::class, + 'pactDriver' => PactDriver::class, + 'messageBodyDriver' => ProtobufMessageBodyDriver::class, + ]); + $this->cleanUp($driver); + } +} diff --git a/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php new file mode 100644 index 00000000..654bd9cd --- /dev/null +++ b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php @@ -0,0 +1,47 @@ +config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('4.0.0'); + } + + public function testCreate(): void + { + $this->factory = new ProtobufSyncMessageDriverFactory(); + $driver = $this->factory->create($this->config); + $this->assertPropertiesInstanceOf($driver, null, [ + 'client' => Client::class, + 'pactDriver' => ProtobufPactDriver::class, + 'mockServer' => GrpcMockServer::class, + ]); + $this->assertPropertiesInstanceOf($driver, AbstractMessageDriver::class, [ + 'messageBodyDriver' => ProtobufMessageBodyDriver::class, + ]); + $this->cleanUp($driver); + } +} diff --git a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php index e449af2b..de252d19 100644 --- a/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php +++ b/tests/PhpPact/SyncMessage/Driver/Interaction/SyncMessageDriverTest.php @@ -1,6 +1,6 @@ config = $this->createMock(MockServerConfigInterface::class); + $this->config + ->expects($this->any()) + ->method('getPactSpecificationVersion') + ->willReturn('3.0.0'); + } + + public function testCreate(): void + { + $this->factory = new SyncMessageDriverFactory(); + $driver = $this->factory->create($this->config); + $this->assertPropertiesInstanceOf($driver, null, [ + 'client' => Client::class, + 'mockServer' => MockServer::class, + 'pactDriver' => PactDriver::class, + ]); + $this->cleanUp($driver); + } +} From d2c8f80095a5696da077b2a6375e5720527c9057 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 6 Mar 2024 18:27:27 +0700 Subject: [PATCH 268/298] test: Test Verifier --- .../Standalone/ProviderVerifier/Verifier.php | 59 ++---- tests/PhpPact/Helper/FFI/ClientTrait.php | 6 +- .../ProviderVerifier/VerifierTest.php | 183 ++++++++++++++++-- tests/_public/index.php | 10 - 4 files changed, 188 insertions(+), 70 deletions(-) delete mode 100644 tests/_public/index.php diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index ca4662af..daa3efa6 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -16,33 +16,30 @@ class Verifier protected ClientInterface $client; protected CData $handle; - public function __construct(VerifierConfigInterface $config, private ?LoggerInterface $logger = null) + public function __construct(VerifierConfigInterface $config, private ?LoggerInterface $logger = null, ?ClientInterface $client = null) { - $this->client = new Client(); - $this - ->newHandle($config) - ->setProviderInfo($config) - ->setProviderTransports($config) - ->setFilterInfo($config) - ->setProviderState($config) - ->setVerificationOptions($config) - ->setPublishOptions($config) - ->setConsumerFilters($config) - ->setLogLevel($config); + $this->client = $client ?? new Client(); + $this->newHandle($config); + $this->setProviderInfo($config); + $this->setProviderTransports($config); + $this->setFilterInfo($config); + $this->setProviderState($config); + $this->setVerificationOptions($config); + $this->setPublishOptions($config); + $this->setConsumerFilters($config); + $this->setLogLevel($config); } - private function newHandle(VerifierConfigInterface $config): self + private function newHandle(VerifierConfigInterface $config): void { $this->handle = $this->client->call( 'pactffi_verifier_new_for_application', $config->getCallingApp()->getName(), $config->getCallingApp()->getVersion() ); - - return $this; } - private function setProviderInfo(VerifierConfigInterface $config): self + private function setProviderInfo(VerifierConfigInterface $config): void { $this->client->call( 'pactffi_verifier_set_provider_info', @@ -53,11 +50,9 @@ private function setProviderInfo(VerifierConfigInterface $config): self $config->getProviderInfo()->getPort(), $config->getProviderInfo()->getPath() ); - - return $this; } - private function setProviderTransports(VerifierConfigInterface $config): self + private function setProviderTransports(VerifierConfigInterface $config): void { foreach ($config->getProviderTransports() as $transport) { $this->client->call( @@ -69,11 +64,9 @@ private function setProviderTransports(VerifierConfigInterface $config): self $transport->getScheme() ); } - - return $this; } - private function setFilterInfo(VerifierConfigInterface $config): self + private function setFilterInfo(VerifierConfigInterface $config): void { $this->client->call( 'pactffi_verifier_set_provider_state', @@ -82,11 +75,9 @@ private function setFilterInfo(VerifierConfigInterface $config): self $config->getProviderState()->isStateChangeTeardown(), $config->getProviderState()->isStateChangeAsBody() ); - - return $this; } - private function setProviderState(VerifierConfigInterface $config): self + private function setProviderState(VerifierConfigInterface $config): void { $this->client->call( 'pactffi_verifier_set_filter_info', @@ -95,11 +86,9 @@ private function setProviderState(VerifierConfigInterface $config): self $config->getFilterInfo()->getFilterState(), $config->getFilterInfo()->getFilterNoState() ); - - return $this; } - private function setVerificationOptions(VerifierConfigInterface $config): self + private function setVerificationOptions(VerifierConfigInterface $config): void { $this->client->call( 'pactffi_verifier_set_verification_options', @@ -107,11 +96,9 @@ private function setVerificationOptions(VerifierConfigInterface $config): self $config->getVerificationOptions()->isDisableSslVerification(), $config->getVerificationOptions()->getRequestTimeout() ); - - return $this; } - private function setPublishOptions(VerifierConfigInterface $config): self + private function setPublishOptions(VerifierConfigInterface $config): void { if ($config->isPublishResults()) { $providerTags = ArrayData::createFrom($config->getPublishOptions()->getProviderTags()); @@ -125,11 +112,9 @@ private function setPublishOptions(VerifierConfigInterface $config): self $config->getPublishOptions()->getProviderBranch() ); } - - return $this; } - private function setConsumerFilters(VerifierConfigInterface $config): self + private function setConsumerFilters(VerifierConfigInterface $config): void { $filterConsumerNames = ArrayData::createFrom($config->getConsumerFilters()->getFilterConsumerNames()); $this->client->call( @@ -138,17 +123,13 @@ private function setConsumerFilters(VerifierConfigInterface $config): self $filterConsumerNames?->getItems(), $filterConsumerNames?->getSize() ); - - return $this; } - private function setLogLevel(VerifierConfigInterface $config): self + private function setLogLevel(VerifierConfigInterface $config): void { if ($logLevel = $config->getLogLevel()) { $this->client->call('pactffi_init_with_log_level', $logLevel); } - - return $this; } public function addFile(string $file): self diff --git a/tests/PhpPact/Helper/FFI/ClientTrait.php b/tests/PhpPact/Helper/FFI/ClientTrait.php index 1faa1683..2aaaf11a 100644 --- a/tests/PhpPact/Helper/FFI/ClientTrait.php +++ b/tests/PhpPact/Helper/FFI/ClientTrait.php @@ -3,6 +3,8 @@ namespace PhpPactTest\Helper\FFI; use PhpPact\FFI\ClientInterface; +use PHPUnit\Framework\Constraint\Constraint; +use PHPUnit\Framework\Constraint\IsIdentical; use PHPUnit\Framework\MockObject\MockObject; trait ClientTrait @@ -17,7 +19,9 @@ protected function assertClientCalls(array $calls): void ->willReturnCallback(function (...$args) use (&$calls) { $call = array_shift($calls); $return = array_pop($call); - $this->assertSame($call, $args); + foreach ($args as $key => $arg) { + $this->assertThat($arg, $call[$key] instanceof Constraint ? $call[$key] : new IsIdentical($call[$key])); + } return $return; }); diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index c66fc75c..472dc1a8 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -2,44 +2,187 @@ namespace PhpPactTest\Standalone\ProviderVerifier; +use FFI; +use FFI\CData; +use GuzzleHttp\Psr7\Uri; +use PhpPact\FFI\ClientInterface; +use PhpPact\Service\LoggerInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; +use PhpPact\Standalone\ProviderVerifier\Model\Config\PublishOptions; +use PhpPact\Standalone\ProviderVerifier\Model\ConsumerVersionSelectors; +use PhpPact\Standalone\ProviderVerifier\Model\Selector\Selector; +use PhpPact\Standalone\ProviderVerifier\Model\Source\Broker; +use PhpPact\Standalone\ProviderVerifier\Model\Source\Url; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; +use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; use PhpPact\Standalone\ProviderVerifier\Verifier; +use PhpPactTest\Helper\FFI\ClientTrait; use PhpPactTest\Helper\PhpProcess; +use PHPUnit\Framework\Attributes\TestWith; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class VerifierTest extends TestCase { - private PhpProcess $process; + use ClientTrait; + + private Verifier $verifier; + private VerifierConfigInterface $config; + private LoggerInterface|MockObject $logger; + private CData $handle; + private array $calls; protected function setUp(): void { - $this->process = new PhpProcess(__DIR__ . '/../../../_public/'); - $this->process->start(); + $this->config = new VerifierConfig(); + $this->logger = $this->createMock(LoggerInterface::class); + $this->client = $this->createMock(ClientInterface::class); + $this->handle = FFI::new('int'); + $this->config->getCallingApp() + ->setName($callingAppName = 'calling app name') + ->setVersion($callingAppVersion = '1.2.3'); + $this->config->getProviderInfo() + ->setName($providerName = 'provider name') + ->setScheme($providerScheme = 'https') + ->setHost($providerHost = 'provider.domain') + ->setPort($providerPort = 123) + ->setPath($providerPath = '/provider/path'); + $transport = new ProviderTransport(); + $transport->setProtocol($transportProtocol = 'message') + ->setPort($transportPort = 234) + ->setPath($transportPath = '/provider-messages') + ->setScheme($transportScheme = 'http'); + $this->config->getProviderState() + ->setStateChangeUrl($stateChangeUrl = new Uri('http://provider.host:432/pact-change-state')) + ->setStateChangeTeardown($stateChangeTearDown = true) + ->setStateChangeAsBody($stateChangeAsBody = true); + $this->config->addProviderTransport($transport); + $this->config->getFilterInfo() + ->setFilterDescription($filterDescription = 'request to /hello') + ->setFilterNoState($filterNoState = true) + ->setFilterState($filterState = 'given state'); + $this->config->getVerificationOptions() + ->setRequestTimeout($requestTimeout = 500) + ->setDisableSslVerification($disableSslVerification = true); + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderTags($providerTags = ['feature-x', 'master', 'test', 'prod']) + ->setProviderVersion($providerVersion = '1.2.3') + ->setBuildUrl($buildUrl = new Uri('http://ci/build/1')) + ->setProviderBranch($providerBranch = 'some-branch'); + $this->config->setPublishOptions($publishOptions); + $this->config->getConsumerFilters() + ->setFilterConsumerNames($filterConsumerNames = ['http-consumer-1', 'http-consumer-2', 'message-consumer-2']); + $this->config->setLogLevel($logLevel = 'info'); + $this->calls = [ + ['pactffi_verifier_new_for_application', $callingAppName, $callingAppVersion, $this->handle], + ['pactffi_verifier_set_provider_info', $this->handle, $providerName, $providerScheme, $providerHost, $providerPort, $providerPath, null], + ['pactffi_verifier_add_provider_transport', $this->handle, $transportProtocol, $transportPort, $transportPath, $transportScheme, null], + ['pactffi_verifier_set_provider_state', $this->handle, (string) $stateChangeUrl, $stateChangeTearDown, $stateChangeAsBody, null], + ['pactffi_verifier_set_filter_info', $this->handle, $filterDescription, $filterState, $filterNoState, null], + ['pactffi_verifier_set_verification_options', $this->handle, $disableSslVerification, $requestTimeout, null], + ['pactffi_verifier_set_publish_options', $this->handle, $providerVersion, $buildUrl, $this->isInstanceOf(CData::class), count($providerTags), $providerBranch, null], + ['pactffi_verifier_set_consumer_filters', $this->handle, $this->isInstanceOf(CData::class), count($filterConsumerNames), null], + ['pactffi_init_with_log_level', strtoupper($logLevel), null], + ]; } - protected function tearDown(): void + public function testConstruct(): void { - $this->process->stop(); + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $this->logger, $this->client); } - public function testVerify(): void + public function testAddFile(): void { - $config = new VerifierConfig(); - $config->getProviderInfo() - ->setName('someProvider') - ->setHost('localhost') - ->setPort($this->process->getPort()) - ->setScheme('http') - ->setPath('/'); - if ($level = \getenv('PACT_LOGLEVEL')) { - $config->setLogLevel($level); - } + $file = '/path/to/file.json'; + $this->calls[] = ['pactffi_verifier_add_file_source', $this->handle, $file, null]; + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $this->logger, $this->client); + $this->assertSame($this->verifier, $this->verifier->addFile($file)); + } + + public function testAddDirectory(): void + { + $directory = '/path/to/directory'; + $this->calls[] = ['pactffi_verifier_add_directory_source', $this->handle, $directory, null]; + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $this->logger, $this->client); + $this->assertSame($this->verifier, $this->verifier->addDirectory($directory)); + } - $verifier = new Verifier($config); - $verifier->addFile(__DIR__ . '/../../../_resources/someconsumer-someprovider.json'); + public function testAddUrl(): void + { + $source = new Url(); + $source + ->setUrl($url = new Uri('http://example.test/path/to/file.json')) + ->setToken($token = 'secret token') + ->setUsername($username = 'my username') + ->setPassword($password = 'secret password'); + $this->calls[] = ['pactffi_verifier_url_source', $this->handle, (string) $url, $username, $password, $token, null]; + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $this->logger, $this->client); + $this->assertSame($this->verifier, $this->verifier->addUrl($source)); + } - $verifyResult = $verifier->verify(); + public function testAddBroker(): void + { + $consumerVersionSelectors = (new ConsumerVersionSelectors()) + ->addSelector(new Selector(tag: 'foo', latest: true)) + ->addSelector('{"tag":"bar","latest":true}'); + $source = new Broker(); + $source + ->setUrl($url = new Uri('http://example.test/path/to/file.json')) + ->setToken($token = 'secret token') + ->setUsername($username = 'my username') + ->setPassword($password = 'secret password') + ->setEnablePending($enablePending = true) + ->setIncludeWipPactSince($wipPactSince = '2020-01-30') + ->setProviderTags($providerTags = ['prod', 'staging']) + ->setProviderBranch($providerBranch = 'main') + ->setConsumerVersionSelectors($consumerVersionSelectors) + ->setConsumerVersionTags($consumerVersionTags = ['dev']); + $this->calls[] = [ + 'pactffi_verifier_broker_source_with_selectors', + $this->handle, + (string) $url, + $username, + $password, + $token, + $enablePending, + $wipPactSince, + $this->isInstanceOf(CData::class), + count($providerTags), + $providerBranch, + $this->isInstanceOf(CData::class), + count($consumerVersionSelectors), + $this->isInstanceOf(CData::class), + count($consumerVersionTags), + null + ]; + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $this->logger, $this->client); + $this->assertSame($this->verifier, $this->verifier->addBroker($source)); + } - $this->assertTrue($verifyResult); + #[TestWith([0, true, false])] + #[TestWith([0, true, true])] + #[TestWith([1, false, false])] + #[TestWith([2, false, false])] + public function testVerify(int $error, bool $success, bool $hasLogger): void + { + $json = '{"key": "value"}'; + $this->calls[] = ['pactffi_verifier_execute', $this->handle, $error]; + $this->logger + ->expects($this->exactly($hasLogger)) + ->method('log') + ->with($json); + if ($hasLogger) { + $this->calls[] = ['pactffi_verifier_json', $this->handle, $json]; + } + $this->calls[] = ['pactffi_verifier_shutdown', $this->handle, null]; + $this->assertClientCalls($this->calls); + $this->verifier = new Verifier($this->config, $hasLogger ? $this->logger : null, $this->client); + $this->assertSame($success, $this->verifier->verify()); } } diff --git a/tests/_public/index.php b/tests/_public/index.php deleted file mode 100644 index cc71ddcc..00000000 --- a/tests/_public/index.php +++ /dev/null @@ -1,10 +0,0 @@ - [ - [ - 'name' => 'g', - ], - ], -]); From 5cae96e5b2a571bf98d8afdf4d8675fca63f3910 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 14:50:38 +0700 Subject: [PATCH 269/298] test: Test MessageBuilder --- .../Exception/MissingCallbackException.php | 7 + src/PhpPact/Consumer/MessageBuilder.php | 10 +- tests/PhpPact/Consumer/MessageBuilderTest.php | 228 ++++++++++++++++++ 3 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 src/PhpPact/Consumer/Exception/MissingCallbackException.php create mode 100644 tests/PhpPact/Consumer/MessageBuilderTest.php diff --git a/src/PhpPact/Consumer/Exception/MissingCallbackException.php b/src/PhpPact/Consumer/Exception/MissingCallbackException.php new file mode 100644 index 00000000..7651b97c --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MissingCallbackException.php @@ -0,0 +1,7 @@ + */ - protected array $callback; + protected array $callback = []; public function __construct(PactConfigInterface $config, ?MessageDriverFactoryInterface $driverFactory = null) { @@ -55,7 +56,8 @@ public function reify(): string * Wrapper around verify() * * @param null|string $description description of the pact and thus callback - * @throws \Exception + * + * @throws MissingCallbackException */ public function verifyMessage(callable $callback, ?string $description = null): bool { @@ -68,12 +70,12 @@ public function verifyMessage(callable $callback, ?string $description = null): * Verify the use of the pact by calling the callback * It also calls finalize to write the pact * - * @throws \Exception if callback is not set + * @throws MissingCallbackException if callback is not set */ public function verify(): bool { if (\count($this->callback) < 1) { - throw new \Exception('Callbacks need to exist to run verify.'); + throw new MissingCallbackException('Callbacks need to exist to run verify.'); } $pactJson = $this->reify(); diff --git a/tests/PhpPact/Consumer/MessageBuilderTest.php b/tests/PhpPact/Consumer/MessageBuilderTest.php new file mode 100644 index 00000000..3cacfd66 --- /dev/null +++ b/tests/PhpPact/Consumer/MessageBuilderTest.php @@ -0,0 +1,228 @@ +driver = $this->createMock(MessageDriverInterface::class); + $this->config = $this->createMock(PactConfigInterface::class); + $this->driverFactory = $this->createMock(MessageDriverFactoryInterface::class); + $this->driverFactory + ->expects($this->once()) + ->method('create') + ->with($this->config) + ->willReturn($this->driver); + $this->builder = new MessageBuilder($this->config, $this->driverFactory); + } + + public function testGiven(): void + { + $this->assertSame($this->builder, $this->builder->given('test', ['key' => 'value'])); + $message = $this->getMessage(); + $providerStates = $message->getProviderStates(); + $this->assertCount(1, $providerStates); + $providerState = $providerStates[0]; + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('test', $providerState->getName()); + $this->assertSame(['key' => 'value'], $providerState->getParams()); + } + + public function testExpectsToReceive(): void + { + $description = 'message description'; + $this->assertSame($this->builder, $this->builder->expectsToReceive($description)); + $message = $this->getMessage(); + $this->assertSame($description, $message->getDescription()); + } + + public function testWithMetadata(): void + { + $metadata = ['key' => 'value']; + $this->assertSame($this->builder, $this->builder->withMetadata($metadata)); + $message = $this->getMessage(); + $this->assertSame($metadata, $message->getMetadata()); + } + + #[TestWith([null , null])] + #[TestWith([new Text('example', 'text/plain') , null])] + #[TestWith([new Binary('/path/to/image.jpg', 'image/jpeg'), null])] + #[TestWith(['example text' , Text::class])] + #[TestWith([['key' => 'value'] , Text::class])] + public function testWithContent(mixed $content, ?string $contentClass): void + { + $this->assertSame($this->builder, $this->builder->withContent($content)); + $message = $this->getMessage(); + if ($contentClass) { + $this->assertInstanceOf($contentClass, $message->getContents()); + } else { + $this->assertSame($content, $message->getContents()); + } + } + + #[TestWith([null])] + #[TestWith(['key'])] + public function testSetKey(?string $key): void + { + $this->assertSame($this->builder, $this->builder->key($key)); + $message = $this->getMessage(); + $this->assertSame($key, $message->getKey()); + } + + #[TestWith([null])] + #[TestWith([false])] + #[TestWith([true])] + public function testSetPending(?bool $pending): void + { + $this->assertSame($this->builder, $this->builder->pending($pending)); + $message = $this->getMessage(); + $this->assertSame($pending, $message->getPending()); + } + + #[TestWith([[]])] + #[TestWith([['key' => 'value']])] + public function testSetComments(array $comments): void + { + $this->assertSame($this->builder, $this->builder->comments($comments)); + $message = $this->getMessage(); + $this->assertSame($comments, $message->getComments()); + } + + public function testSetSingleCallback(): void + { + $callbacks = [ + fn () => 'first', + fn () => 'second', + fn () => 'third', + fn () => 'fourth', + ]; + foreach ($callbacks as $callback) { + $this->assertSame($this->builder, $this->builder->setCallback($callback)); + } + $builderCallbacks = $this->getCallbacks(); + $this->assertSame([end($callbacks)], $builderCallbacks); + } + + public function testSetMultipleCallbacks(): void + { + $callbacks = [ + 'first callback' => fn () => 'first', + 'second callback' => fn () => 'second', + 'third callback' => fn () => 'third', + 'fourth callback' => fn () => 'fourth', + ]; + foreach ($callbacks as $description => $callback) { + $this->assertSame($this->builder, $this->builder->setCallback($callback, $description)); + } + $builderCallbacks = $this->getCallbacks(); + $this->assertSame($callbacks, $builderCallbacks); + } + + public function testReify(): void + { + $jsonMessage = '{"key": "value"}'; + $message = $this->getMessage(); + $this->driver + ->expects($this->once()) + ->method('registerMessage') + ->with($message); + $this->driver + ->expects($this->once()) + ->method('reify') + ->with($message) + ->willReturn($jsonMessage); + $this->assertSame($jsonMessage, $this->builder->reify()); + } + + public function testVerifyWithoutCallback(): void + { + $this->expectException(MissingCallbackException::class); + $this->expectExceptionMessage('Callbacks need to exist to run verify.'); + $this->builder->verify(); + } + + #[TestWith([false])] + #[TestWith([true])] + public function testVerifyMessage(bool $callbackThrowException): void + { + $jsonMessage = '{"key": "value"}'; + $callback = $this->getMockBuilder(stdClass::class) + ->addMethods(['__invoke']) + ->getMock(); + $mocker = $callback + ->expects($this->once()) + ->method('__invoke'); + if ($callbackThrowException) { + $mocker->willThrowException(new Exception('something wrong')); + } + $this->driver + ->expects($this->once()) + ->method('reify') + ->willReturn($jsonMessage); + $this->driver + ->expects($this->exactly(!$callbackThrowException)) + ->method('writePactAndCleanUp'); + $this->assertSame(!$callbackThrowException, $this->builder->verifyMessage($callback, 'a callback')); + } + + #[TestWith([false])] + #[TestWith([true])] + public function testVerify(bool $callbackThrowException): void + { + $jsonMessage = '{"key": "value"}'; + $callback = $this->getMockBuilder(stdClass::class) + ->addMethods(['__invoke']) + ->getMock(); + $mocker = $callback + ->expects($this->once()) + ->method('__invoke'); + if ($callbackThrowException) { + $mocker->willThrowException(new Exception('something wrong')); + } + $this->driver + ->expects($this->once()) + ->method('reify') + ->willReturn($jsonMessage); + $this->driver + ->expects($this->exactly(!$callbackThrowException)) + ->method('writePactAndCleanUp'); + $this->builder->setCallback($callback); + $this->assertSame(!$callbackThrowException, $this->builder->verify()); + } + + private function getMessage(): Message + { + $reflection = new ReflectionProperty($this->builder, 'message'); + + return $reflection->getValue($this->builder); + } + + private function getCallbacks(): array + { + $reflection = new ReflectionProperty($this->builder, 'callback'); + + return $reflection->getValue($this->builder); + } +} From b249af02c63376ceaa8a8177c9346eb732efeed4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 15:13:22 +0700 Subject: [PATCH 270/298] test: Test SyncMessageBuilder --- .../Factory/SyncMessageDriverFactoryTest.php | 1 - .../SyncMessage/SyncMessageBuilderTest.php | 138 ++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/PhpPact/SyncMessage/SyncMessageBuilderTest.php diff --git a/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php b/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php index dc13ab77..dc6985ed 100644 --- a/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php +++ b/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php @@ -9,7 +9,6 @@ use PhpPact\SyncMessage\Factory\SyncMessageDriverFactory; use PhpPact\SyncMessage\Factory\SyncMessageDriverFactoryInterface; use PhpPactTest\Helper\FactoryTrait; -use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/tests/PhpPact/SyncMessage/SyncMessageBuilderTest.php b/tests/PhpPact/SyncMessage/SyncMessageBuilderTest.php new file mode 100644 index 00000000..6559009b --- /dev/null +++ b/tests/PhpPact/SyncMessage/SyncMessageBuilderTest.php @@ -0,0 +1,138 @@ +driver = $this->createMock(SyncMessageDriverInterface::class); + $this->config = $this->createMock(MockServerConfigInterface::class); + $this->driverFactory = $this->createMock(SyncMessageDriverFactoryInterface::class); + $this->driverFactory + ->expects($this->once()) + ->method('create') + ->with($this->config) + ->willReturn($this->driver); + $this->builder = new SyncMessageBuilder($this->config, $this->driverFactory); + } + + public function testGiven(): void + { + $this->assertSame($this->builder, $this->builder->given('test', ['key' => 'value'])); + $message = $this->getMessage(); + $providerStates = $message->getProviderStates(); + $this->assertCount(1, $providerStates); + $providerState = $providerStates[0]; + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('test', $providerState->getName()); + $this->assertSame(['key' => 'value'], $providerState->getParams()); + } + + public function testExpectsToReceive(): void + { + $description = 'message description'; + $this->assertSame($this->builder, $this->builder->expectsToReceive($description)); + $message = $this->getMessage(); + $this->assertSame($description, $message->getDescription()); + } + + public function testWithMetadata(): void + { + $metadata = ['key' => 'value']; + $this->assertSame($this->builder, $this->builder->withMetadata($metadata)); + $message = $this->getMessage(); + $this->assertSame($metadata, $message->getMetadata()); + } + + #[TestWith([null , null])] + #[TestWith([new Text('example', 'text/plain') , null])] + #[TestWith([new Binary('/path/to/image.jpg', 'image/jpeg'), null])] + #[TestWith(['example text' , Text::class])] + #[TestWith([['key' => 'value'] , Text::class])] + public function testWithContent(mixed $content, ?string $contentClass): void + { + $this->assertSame($this->builder, $this->builder->withContent($content)); + $message = $this->getMessage(); + if ($contentClass) { + $this->assertInstanceOf($contentClass, $message->getContents()); + } else { + $this->assertSame($content, $message->getContents()); + } + } + + #[TestWith([null])] + #[TestWith(['key'])] + public function testSetKey(?string $key): void + { + $this->assertSame($this->builder, $this->builder->key($key)); + $message = $this->getMessage(); + $this->assertSame($key, $message->getKey()); + } + + #[TestWith([null])] + #[TestWith([false])] + #[TestWith([true])] + public function testSetPending(?bool $pending): void + { + $this->assertSame($this->builder, $this->builder->pending($pending)); + $message = $this->getMessage(); + $this->assertSame($pending, $message->getPending()); + } + + #[TestWith([[]])] + #[TestWith([['key' => 'value']])] + public function testSetComments(array $comments): void + { + $this->assertSame($this->builder, $this->builder->comments($comments)); + $message = $this->getMessage(); + $this->assertSame($comments, $message->getComments()); + } + + public function testRegisterMessage(): void + { + $message = $this->getMessage(); + $this->driver + ->expects($this->once()) + ->method('registerMessage') + ->with($message); + $this->builder->registerMessage(); + } + + #[TestWith([false])] + #[TestWith([true])] + public function testVerify(bool $matched): void + { + $this->driver + ->expects($this->once()) + ->method('verifyMessage') + ->willReturn(new VerifyResult($matched, '')); + $this->assertSame($matched, $this->builder->verify()); + } + + private function getMessage(): Message + { + $reflection = new ReflectionProperty($this->builder, 'message'); + + return $reflection->getValue($this->builder); + } +} From 2778c3c588f0e590748c195792716fe542a67584 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 15:55:55 +0700 Subject: [PATCH 271/298] test: Test MockServerConfig::getBaseUri() --- .../MockServer/MockServerConfigTest.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php index 29d8fd52..b8f02c7f 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php @@ -3,11 +3,12 @@ namespace PhpPactTest\Standalone\MockServer; use PhpPact\Standalone\MockService\MockServerConfig; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class MockServerConfigTest extends TestCase { - public function testSetters() + public function testSetters(): void { $host = 'test-host'; $port = 1234; @@ -42,4 +43,16 @@ public function testSetters() static::assertSame($logLevel, $subject->getLogLevel()); static::assertSame($pactSpecificationVersion, $subject->getPactSpecificationVersion()); } + + #[TestWith([false, 'http://example.test:123'])] + #[TestWith([true, 'https://example.test:123'])] + public function testGetBaseUri(bool $secure, string $baseUri): void + { + $config = new MockServerConfig(); + $config + ->setHost('example.test') + ->setPort(123) + ->setSecure($secure); + $this->assertEquals($baseUri, $config->getBaseUri()); + } } From 1318de8ea414d06faaa54e443789000653e2f1c0 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 16:16:00 +0700 Subject: [PATCH 272/298] test: Test MockServerEnvConfig --- .../MockServer/MockServerEnvConfigTest.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php diff --git a/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php new file mode 100644 index 00000000..6b0cd155 --- /dev/null +++ b/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php @@ -0,0 +1,92 @@ +getHost()); + } + + #[TestWith(['PACT_MOCK_SERVER_PORT', 0])] + #[TestWith(['PACT_MOCK_SERVER_PORT=123', 123])] + public function testPort(string $assignment, int $port): void + { + putenv($assignment); + $config = new MockServerEnvConfig(); + static::assertSame($port, $config->getPort()); + } + + #[TestWith(['PACT_CONSUMER_NAME', null])] + #[TestWith(['PACT_CONSUMER_NAME=consumer', 'consumer'])] + public function testConsumer(string $assignment, ?string $consumer): void + { + putenv($assignment); + if (!$consumer) { + $this->expectException(MissingEnvVariableException::class); + $this->expectExceptionMessage('Please provide required environmental variable PACT_CONSUMER_NAME!'); + } + $config = new MockServerEnvConfig(); + static::assertSame($consumer, $config->getConsumer()); + } + + #[TestWith(['PACT_PROVIDER_NAME', null])] + #[TestWith(['PACT_PROVIDER_NAME=provider', 'provider'])] + public function testProvider(string $assignment, ?string $provider): void + { + putenv($assignment); + if (!$provider) { + $this->expectException(MissingEnvVariableException::class); + $this->expectExceptionMessage('Please provide required environmental variable PACT_PROVIDER_NAME!'); + } + $config = new MockServerEnvConfig(); + static::assertSame($provider, $config->getProvider()); + } + + #[TestWith(['PACT_OUTPUT_DIR', null])] + #[TestWith(['PACT_OUTPUT_DIR=/path/to/pact/dir', '/path/to/pact/dir'])] + public function testPactDir(string $assignment, ?string $pactDir): void + { + $pactDir ??= \sys_get_temp_dir(); + putenv($assignment); + $config = new MockServerEnvConfig(); + static::assertSame($pactDir, $config->getPactDir()); + } + + #[TestWith(['PACT_LOG', null])] + #[TestWith(['PACT_LOG=/path/to/log/dir', '/path/to/log/dir'])] + public function testLog(string $assignment, ?string $logDir): void + { + putenv($assignment); + $config = new MockServerEnvConfig(); + static::assertSame($logDir, $config->getLog()); + } + + #[TestWith(['PACT_LOGLEVEL', null])] + #[TestWith(['PACT_LOGLEVEL=trace', 'TRACE'])] + public function testLogLevel(string $assignment, ?string $logLevel): void + { + putenv($assignment); + $config = new MockServerEnvConfig(); + static::assertSame($logLevel, $config->getLogLevel()); + } + + #[TestWith(['PACT_SPECIFICATION_VERSION', '3.0.0'])] + #[TestWith(['PACT_SPECIFICATION_VERSION=1.1.0', '1.1.0'])] + public function testPactSpecificationVersion(string $assignment, ?string $specificationVersion): void + { + putenv($assignment); + $config = new MockServerEnvConfig(); + static::assertSame($specificationVersion, $config->getPactSpecificationVersion()); + } +} From 08ac678ba884da5986b364dfc25a72f9a94cbd1f Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 17:08:13 +0700 Subject: [PATCH 273/298] test: Test more cases for PactConfig::setLogLevel() --- tests/PhpPact/Config/PactConfigTest.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/PhpPact/Config/PactConfigTest.php b/tests/PhpPact/Config/PactConfigTest.php index cb01ee86..ceb7da85 100644 --- a/tests/PhpPact/Config/PactConfigTest.php +++ b/tests/PhpPact/Config/PactConfigTest.php @@ -4,6 +4,7 @@ use PhpPact\Config\PactConfig; use PhpPact\Config\PactConfigInterface; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class PactConfigTest extends TestCase @@ -50,11 +51,22 @@ public function testInvalidPactSpecificationVersion(): void $this->config->setPactSpecificationVersion('invalid'); } - public function testInvalidLogLevel(): void + #[TestWith(['trace', 'TRACE'])] + #[TestWith(['debug', 'DEBUG'])] + #[TestWith(['info', 'INFO'])] + #[TestWith(['warn', 'WARN'])] + #[TestWith(['error', 'ERROR'])] + #[TestWith(['off', 'OFF'])] + #[TestWith(['none', 'NONE'])] + #[TestWith(['verbose', null])] + public function testLogLevel(string $logLevel, ?string $result): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('LogLevel VERBOSE not supported.'); - $this->config->setLogLevel('VERBOSE'); + if (!$result) { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('LogLevel VERBOSE not supported.'); + } + $this->config->setLogLevel($logLevel); + $this->assertSame($result, $this->config->getLogLevel()); } public function testInvalidPactFileWriteMode(): void From 90a078020bdc8740abce11a72f40a8e74e967507 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 7 Mar 2024 22:36:44 +0700 Subject: [PATCH 274/298] test: Test stub server --- .github/workflows/build.yml | 3 + composer.json | 4 +- .../_resources/someconsumer-someprovider.json | 0 example/stub-server/consumer/phpunit.xml | 11 ++ .../src/Service/HttpClientService.php | 28 ++++ .../consumer/tests/StubServerTest.php | 42 ++++++ phpunit.xml | 3 + .../Standalone/StubService/StubServer.php | 20 ++- .../StubService/StubServerConfig.php | 2 +- .../Standalone/StubServer/StubServerTest.php | 126 +++++++++++++----- 10 files changed, 190 insertions(+), 49 deletions(-) rename {tests => example/stub-server/consumer}/_resources/someconsumer-someprovider.json (100%) create mode 100644 example/stub-server/consumer/phpunit.xml create mode 100644 example/stub-server/consumer/src/Service/HttpClientService.php create mode 100644 example/stub-server/consumer/tests/StubServerTest.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ef61d52..db2ab5b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,6 +81,9 @@ jobs: - os: windows-latest example: 'protobuf-async-message' php: 8.2 + - os: windows-latest + example: 'stub-server' + php: 8.2 timeout-minutes: 5 steps: diff --git a/composer.json b/composer.json index ec7f1620..f42d59ca 100644 --- a/composer.json +++ b/composer.json @@ -91,7 +91,9 @@ "ProtobufSyncMessageProvider\\Tests\\": "example/protobuf-sync-message/provider/tests", "ProtobufAsyncMessageConsumer\\": "example/protobuf-async-message/consumer/src", "ProtobufAsyncMessageConsumer\\Tests\\": "example/protobuf-async-message/consumer/tests", - "ProtobufAsyncMessageProvider\\Tests\\": "example/protobuf-async-message/provider/tests" + "ProtobufAsyncMessageProvider\\Tests\\": "example/protobuf-async-message/provider/tests", + "StubServerConsumer\\": "example/stub-server/consumer/src", + "StubServerConsumer\\Tests\\": "example/stub-server/consumer/tests" } }, "scripts": { diff --git a/tests/_resources/someconsumer-someprovider.json b/example/stub-server/consumer/_resources/someconsumer-someprovider.json similarity index 100% rename from tests/_resources/someconsumer-someprovider.json rename to example/stub-server/consumer/_resources/someconsumer-someprovider.json diff --git a/example/stub-server/consumer/phpunit.xml b/example/stub-server/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/stub-server/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/stub-server/consumer/src/Service/HttpClientService.php b/example/stub-server/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..0dbe59a6 --- /dev/null +++ b/example/stub-server/consumer/src/Service/HttpClientService.php @@ -0,0 +1,28 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getResults(): array + { + $response = $this->httpClient->get(new Uri("{$this->baseUri}/test")); + $body = $response->getBody(); + $object = \json_decode($body, null, 512, JSON_THROW_ON_ERROR); + + return $object->results; + } +} diff --git a/example/stub-server/consumer/tests/StubServerTest.php b/example/stub-server/consumer/tests/StubServerTest.php new file mode 100644 index 00000000..f39d22f3 --- /dev/null +++ b/example/stub-server/consumer/tests/StubServerTest.php @@ -0,0 +1,42 @@ +setDirs($dirs) + ->setExtension($extension) + ->setPort($port) + ->setLogLevel($logLevel); + + $stubServer = new StubServer($config); + $pid = $stubServer->start(); + $this->assertIsInt($pid); + + $service = new HttpClientService($config->getBaseUri()); + $results = $service->getResults(); + $this->assertEquals([ + (object) [ + 'name' => 'Games' + ] + ], $results); + } finally { + $result = $stubServer->stop(); + $this->assertTrue($result); + } + } +} diff --git a/phpunit.xml b/phpunit.xml index 97bc65b3..c44f4f24 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -40,6 +40,9 @@ ./example/protobuf-async-message + + ./example/stub-server + diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index 81934bdf..84dd0400 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -7,18 +7,16 @@ use PhpPact\Standalone\StubService\Exception\LogLevelNotSupportedException; use Symfony\Component\Process\Process; -/** - * Ruby Standalone Stub Server Wrapper - */ class StubServer { private StubServerConfigInterface $config; private Process $process; - public function __construct(StubServerConfigInterface $config) + public function __construct(StubServerConfigInterface $config, ?Process $process = null) { $this->config = $config; + $this->process = $process ?? new Process([Scripts::getStubService(), ...$this->getArguments()], null, ['PACT_BROKER_BASE_URL' => false]); } /** @@ -30,14 +28,14 @@ public function __construct(StubServerConfigInterface $config) */ public function start(): ?int { - $this->process = new Process([Scripts::getStubService(), ...$this->getArguments()], null, ['PACT_BROKER_BASE_URL' => false]); - - $this->process->start(function (string $type, string $buffer) { - echo $buffer; - }); - $logLevel = $this->config->getLogLevel(); - if (is_null($logLevel) || in_array($logLevel, ['info', 'debug', 'trace'])) { + if (is_null($logLevel) || \strtoupper($logLevel) !== 'NONE') { + $callback = function (string $type, string $buffer): void { + echo "\n$type > $buffer"; + }; + } + $this->process->start($callback ?? null); + if (is_null($logLevel) || in_array(\strtoupper($logLevel), ['INFO', 'DEBUG', 'TRACE'])) { $this->process->waitUntil(function (string $type, string $output) { $result = preg_match('/Server started on port (\d+)/', $output, $matches); if ($result === 1 && $this->config->getPort() === 0) { diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index 9258eada..91eedb41 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -94,7 +94,7 @@ public function getFiles(): array return $this->files; } - public function setLogLevel(string $logLevel): StubServerConfigInterface + public function setLogLevel(?string $logLevel): StubServerConfigInterface { $this->logLevel = $logLevel; diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubServer/StubServerTest.php index 82410525..defb0901 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerTest.php @@ -2,51 +2,105 @@ namespace PhpPactTest\Standalone\StubServer; +use PhpPact\Standalone\StubService\Exception\LogLevelNotSupportedException; use PhpPact\Standalone\StubService\StubServer; use PhpPact\Standalone\StubService\StubServerConfig; +use PhpPact\Standalone\StubService\StubServerConfigInterface; +use PHPUnit\Framework\Attributes\TestWith; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; class StubServerTest extends TestCase { - /** - * @throws \Exception - */ - public function testStartAndStop() + protected StubServer $server; + protected StubServerConfigInterface $config; + protected Process|MockObject $process; + + public function setUp(): void + { + $this->process = $this->createMock(Process::class); + $this->config = new StubServerConfig(); + $this->server = new StubServer($this->config, $this->process); + } + + #[TestWith([null , true])] + #[TestWith(['trace', true])] + #[TestWith(['debug', true])] + #[TestWith(['info' , true])] + #[TestWith(['warn' , true])] + #[TestWith(['error', true])] + #[TestWith(['none' , false])] + public function testStart(?string $logLevel, bool $hasCallback): void + { + $this->config->setLogLevel($logLevel); + $this->config->setPort(123); + $this->process + ->expects($this->once()) + ->method('start') + ->with($hasCallback ? $this->callback(fn ($callback) => is_callable($callback)) : null); + $this->process + ->expects($this->once()) + ->method('getPid') + ->willReturn($pid = 1234); + $this->assertSame($pid, $this->server->start()); + } + + #[TestWith([null , false])] + #[TestWith(['trace', false])] + #[TestWith(['debug', false])] + #[TestWith(['info' , false])] + #[TestWith([null , true])] + #[TestWith(['trace', true])] + #[TestWith(['debug', true])] + #[TestWith(['info' , true])] + public function testStartWithSupportedLogLevelOnRandomPort(?string $logLevel, bool $started): void + { + $this->config->setLogLevel($logLevel); + $this->config->setPort(0); + $this->process + ->expects($this->once()) + ->method('start'); + $this->process + ->expects($this->once()) + ->method('getPid') + ->willReturn($pid = 1234); + $this->process + ->expects($this->once()) + ->method('waitUntil') + ->with($this->callback(function (callable $callback) use ($started) { + $port = 123; + $this->assertSame($started ? 1 : 0, $callback('out', $started ? "Server started on port $port" : 'not started')); + $this->assertSame($started ? $port : 0, $this->config->getPort()); + + return true; + })); + $this->assertSame($pid, $this->server->start()); + } + + #[TestWith(['warn'])] + #[TestWith(['error'])] + #[TestWith(['none'])] + public function testStartWithUnsupportedLogLevelOnRandomPort(string $logLevel): void { - try { - $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; - $port = 7201; - - $subject = (new StubServerConfig()) - ->setFiles($files) - ->setPort($port); - - $stubServer = new StubServer($subject); - $pid = $stubServer->start(); - $this->assertTrue(\is_int($pid)); - } finally { - $result = $stubServer->stop(); - $this->assertTrue($result); - } + $this->config->setLogLevel($logLevel); + $this->config->setPort(0); + $this->process + ->expects($this->once()) + ->method('start'); + $this->process + ->expects($this->never()) + ->method('getPid'); + $this->expectException(LogLevelNotSupportedException::class); + $this->expectExceptionMessage("Setting random port for stub server required log level 'info', 'debug' or 'trace'. '$logLevel' given."); + $this->server->start(); } - /** - * @throws \Exception - */ - public function testRandomPort(): void + public function testStop(): void { - try { - $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; - - $subject = (new StubServerConfig()) - ->setFiles($files); - - $stubServer = new StubServer($subject); - $stubServer->start(); - $this->assertGreaterThan(0, $subject->getPort()); - } finally { - $result = $stubServer->stop(); - $this->assertTrue($result); - } + $this->process + ->expects($this->once()) + ->method('stop'); + $this->assertTrue($this->server->stop()); } } From b3067b8a6fad783be78aab239ea72de8f37c1a56 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 09:18:00 +0700 Subject: [PATCH 275/298] revert: Set up pact on demand to speed up factory's unit tests --- .../Interaction/AbstractMessageDriver.php | 1 + .../Driver/Interaction/InteractionDriver.php | 1 + .../Consumer/Driver/Pact/PactDriver.php | 6 +- .../Driver/Pact/PactDriverInterface.php | 2 + .../Driver/Pact/AbstractPluginPactDriver.php | 2 +- .../Consumer/Driver/Pact/PactDriverTest.php | 64 ++++++++++++++----- .../Factory/InteractionDriverFactoryTest.php | 2 - .../Factory/MessageDriverFactoryTest.php | 2 - tests/PhpPact/Helper/FactoryTrait.php | 9 --- .../Pact/AbstractPluginPactDriverTestCase.php | 9 +++ .../CsvInteractionDriverFactoryTest.php | 2 - .../ProtobufMessageDriverFactoryTest.php | 1 - .../ProtobufSyncMessageDriverFactoryTest.php | 1 - .../Factory/SyncMessageDriverFactoryTest.php | 1 - 14 files changed, 67 insertions(+), 36 deletions(-) diff --git a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php index 938ef19d..6025ec62 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/AbstractMessageDriver.php @@ -23,6 +23,7 @@ public function __construct( public function registerMessage(Message $message): void { + $this->pactDriver->setUp(); $this->newInteraction($message); $this->given($message); $this->expectsToReceive($message); diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php index f76d8187..1de90ee9 100644 --- a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -36,6 +36,7 @@ public function verifyInteractions(): VerifyResult public function registerInteraction(Interaction $interaction, bool $startMockServer = true): bool { + $this->pactDriver->setUp(); $this->newInteraction($interaction); $this->given($interaction); $this->uponReceiving($interaction); diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index 9a120f32..50a170f1 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -17,7 +17,6 @@ public function __construct( protected ClientInterface $client, protected PactConfigInterface $config ) { - $this->setUp(); } public function cleanUp(): void @@ -48,8 +47,11 @@ public function getPact(): Pact return $this->pact; } - protected function setUp(): void + public function setUp(): void { + if ($this->pact) { + return; + } $this->initWithLogLevel(); $this->newPact(); $this->withSpecification(); diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php index 729d1140..15f1b90a 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php @@ -6,6 +6,8 @@ interface PactDriverInterface { + public function setUp(): void; + public function cleanUp(): void; public function writePact(): void; diff --git a/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php index 7844f5a3..bd56f797 100644 --- a/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php +++ b/src/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriver.php @@ -14,7 +14,7 @@ public function cleanUp(): void parent::cleanUp(); } - protected function setUp(): void + public function setUp(): void { parent::setUp(); $this->usingPlugin(); diff --git a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php index 5d8b7219..a050da54 100644 --- a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php @@ -45,6 +45,7 @@ public function setUp(): void ['PactSpecification_V3', self::SPEC_V3], ['PactSpecification_V4', self::SPEC_V4], ]); + $this->driver = new PactDriver($this->client, $this->config); } #[TestWith([null , '1.0.0', self::SPEC_V1])] @@ -69,14 +70,24 @@ public function testSetUp(?string $logLevel, string $version, int $specification ['pactffi_with_specification', $this->pactHandle, $specificationHandle, null], ]; $this->assertClientCalls($calls); - $this->driver = new PactDriver($this->client, $this->config); + $this->driver->setUp(); $this->assertSame($this->pactHandle, $this->driver->getPact()->handle); } - #[TestWith([false, false])] - #[TestWith([true, false])] - #[TestWith([false, true])] - public function testCleanUp(bool $getPactAfterCleanUp, bool $cleanUpAfterCleanUp): void + public function testSetUpMultipleTimes(): void + { + $this->assertConfig(null, '1.0.0'); + $calls = [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, self::SPEC_V1, null], + ]; + $this->assertClientCalls($calls); + $this->driver->setUp(); + $this->driver->setUp(); + $this->driver->setUp(); + } + + public function testCleanUp(): void { $this->assertConfig(null, '1.0.0'); $calls = [ @@ -85,16 +96,33 @@ public function testCleanUp(bool $getPactAfterCleanUp, bool $cleanUpAfterCleanUp ['pactffi_free_pact_handle', $this->pactHandle, null], ]; $this->assertClientCalls($calls); - $this->driver = new PactDriver($this->client, $this->config); + $this->driver->setUp(); + $this->driver->cleanUp(); + } + + public function testCleanUpWithoutPact(): void + { + $this->expectException(MissingPactException::class); $this->driver->cleanUp(); - if ($getPactAfterCleanUp || $cleanUpAfterCleanUp) { - $this->expectException(MissingPactException::class); - if ($getPactAfterCleanUp) { - $this->driver->getPact(); - } else { - $this->driver->cleanUp(); - } - } + } + + public function testGetPact(): void + { + $this->assertConfig(null, '1.0.0'); + $calls = [ + ['pactffi_new_pact', $this->consumer, $this->provider, $this->pactHandle], + ['pactffi_with_specification', $this->pactHandle, self::SPEC_V1, null], + ]; + $this->assertClientCalls($calls); + $this->driver->setUp(); + $pact = $this->driver->getPact(); + $this->assertSame($this->pactHandle, $pact->handle); + } + + public function testGetPactWithoutPact(): void + { + $this->expectException(MissingPactException::class); + $this->driver->getPact(); } #[TestWith([0, PactConfigInterface::MODE_OVERWRITE])] @@ -124,7 +152,7 @@ public function testWritePact(int $error, string $writeMode): void ['pactffi_pact_handle_write_file', $this->pactHandle, $this->pactDir, $writeMode === PactConfigInterface::MODE_OVERWRITE, $error], ]; $this->assertClientCalls($calls); - $this->driver = new PactDriver($this->client, $this->config); + $this->driver->setUp(); if ($error) { $this->expectException(PactFileNotWroteException::class); $this->expectExceptionMessage(match ($error) { @@ -137,6 +165,12 @@ public function testWritePact(int $error, string $writeMode): void $this->driver->writePact(); } + public function testWritePactWithoutPact(): void + { + $this->expectException(MissingPactException::class); + $this->driver->writePact(); + } + protected function assertConfig(?string $logLevel, string $version): void { $this->config diff --git a/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php b/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php index 7e670bc3..04b3e707 100644 --- a/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php +++ b/tests/PhpPact/Consumer/Factory/InteractionDriverFactoryTest.php @@ -9,7 +9,6 @@ use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPactTest\Helper\FactoryTrait; -use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -38,6 +37,5 @@ public function testCreate(): void 'mockServer' => MockServer::class, 'pactDriver' => PactDriver::class, ]); - $this->cleanUp($driver); } } diff --git a/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php b/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php index d2c20c6e..9701637e 100644 --- a/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php +++ b/tests/PhpPact/Consumer/Factory/MessageDriverFactoryTest.php @@ -8,7 +8,6 @@ use PhpPact\FFI\Client; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPactTest\Helper\FactoryTrait; -use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -36,6 +35,5 @@ public function testCreate(): void 'client' => Client::class, 'pactDriver' => PactDriver::class, ]); - $this->cleanUp($driver); } } diff --git a/tests/PhpPact/Helper/FactoryTrait.php b/tests/PhpPact/Helper/FactoryTrait.php index 2e913e03..7b2c1cb0 100644 --- a/tests/PhpPact/Helper/FactoryTrait.php +++ b/tests/PhpPact/Helper/FactoryTrait.php @@ -2,7 +2,6 @@ namespace PhpPactTest\Helper; -use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use ReflectionProperty; trait FactoryTrait @@ -18,12 +17,4 @@ private function assertPropertiesInstanceOf(object $object, ?string $class, arra $this->assertInstanceOf($propertyClass, $value); } } - - private function cleanUp(object $driver): void - { - $reflection = new ReflectionProperty($driver, 'pactDriver'); - $pactDriver = $reflection->getValue($driver); - $this->assertInstanceOf(PactDriverInterface::class, $pactDriver); - $pactDriver->cleanUp(); - } } diff --git a/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php b/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php index ebdaf972..56537349 100644 --- a/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php +++ b/tests/PhpPact/Plugin/Driver/Pact/AbstractPluginPactDriverTestCase.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Plugin\Driver\Pact; +use PhpPact\Consumer\Driver\Exception\MissingPactException; use PhpPact\Plugin\Driver\Pact\AbstractPluginPactDriver; use PhpPact\Plugin\Exception\PluginNotSupportedBySpecificationException; use PhpPactTest\Consumer\Driver\Pact\PactDriverTest; @@ -34,6 +35,7 @@ public function testSetUpUsingPlugin(string $version, int $specificationHandle, )); } $this->driver = $this->createPactDriver(); + $this->driver->setUp(); } public function testCleanUpPlugin(): void @@ -48,6 +50,13 @@ public function testCleanUpPlugin(): void ]; $this->assertClientCalls($calls); $this->driver = $this->createPactDriver(); + $this->driver->setUp(); + $this->driver->cleanUp(); + } + + public function testCleanUpPluginWithoutPact(): void + { + $this->expectException(MissingPactException::class); $this->driver->cleanUp(); } diff --git a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php index ba7048d4..ce5e4b63 100644 --- a/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php +++ b/tests/PhpPact/Plugins/Csv/Factory/CsvInteractionDriverFactoryTest.php @@ -64,7 +64,6 @@ public function testCreate(InteractionPart ...$pluginParts): void 'client' => Client::class, 'bodyDriver' => in_array(InteractionPart::RESPONSE, $pluginParts) ? CsvBodyDriver::class : InteractionBodyDriver::class, ]); - $this->cleanUp($driver); } public function testMissingPluginPartsException(): void @@ -72,7 +71,6 @@ public function testMissingPluginPartsException(): void $this->expectException(MissingPluginPartsException::class); $this->expectExceptionMessage('At least 1 interaction part must be csv'); $this->factory = new CsvInteractionDriverFactory(); - $this->factory->create($this->config); } private function getRequestDriver(InteractionDriverInterface $driver): RequestDriverInterface diff --git a/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactoryTest.php b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactoryTest.php index 6708eb0b..cc8cb94b 100644 --- a/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactoryTest.php +++ b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufMessageDriverFactoryTest.php @@ -38,6 +38,5 @@ public function testCreate(): void 'pactDriver' => PactDriver::class, 'messageBodyDriver' => ProtobufMessageBodyDriver::class, ]); - $this->cleanUp($driver); } } diff --git a/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php index 654bd9cd..a31e2faa 100644 --- a/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php +++ b/tests/PhpPact/Plugins/Protobuf/Factory/ProtobufSyncMessageDriverFactoryTest.php @@ -42,6 +42,5 @@ public function testCreate(): void $this->assertPropertiesInstanceOf($driver, AbstractMessageDriver::class, [ 'messageBodyDriver' => ProtobufMessageBodyDriver::class, ]); - $this->cleanUp($driver); } } diff --git a/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php b/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php index dc13ab77..387f576a 100644 --- a/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php +++ b/tests/PhpPact/SyncMessage/Factory/SyncMessageDriverFactoryTest.php @@ -38,6 +38,5 @@ public function testCreate(): void 'mockServer' => MockServer::class, 'pactDriver' => PactDriver::class, ]); - $this->cleanUp($driver); } } From abd85056332922869c107ab22dfa5effd1a86f25 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 10:07:47 +0700 Subject: [PATCH 276/298] test: Preserve env while testing MockServerEnvConfig --- .../MockServer/MockServerEnvConfigTest.php | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php index 6b0cd155..5a054b80 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerEnvConfigTest.php @@ -13,80 +13,96 @@ class MockServerEnvConfigTest extends TestCase #[TestWith(['PACT_MOCK_SERVER_HOST=example.test', 'example.test'])] public function testHost(string $assignment, string $host): void { - putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($host, $config->getHost()); + $this->preserveEnv('PACT_MOCK_SERVER_PORT', $assignment, function () use ($host) { + $config = new MockServerEnvConfig(); + $this->assertSame($host, $config->getHost()); + }); } #[TestWith(['PACT_MOCK_SERVER_PORT', 0])] #[TestWith(['PACT_MOCK_SERVER_PORT=123', 123])] public function testPort(string $assignment, int $port): void { - putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($port, $config->getPort()); + $this->preserveEnv('PACT_MOCK_SERVER_PORT', $assignment, function () use ($port) { + $config = new MockServerEnvConfig(); + $this->assertSame($port, $config->getPort()); + }); } #[TestWith(['PACT_CONSUMER_NAME', null])] #[TestWith(['PACT_CONSUMER_NAME=consumer', 'consumer'])] public function testConsumer(string $assignment, ?string $consumer): void { - putenv($assignment); - if (!$consumer) { - $this->expectException(MissingEnvVariableException::class); - $this->expectExceptionMessage('Please provide required environmental variable PACT_CONSUMER_NAME!'); - } - $config = new MockServerEnvConfig(); - static::assertSame($consumer, $config->getConsumer()); + $this->preserveEnv('PACT_CONSUMER_NAME', $assignment, function () use ($consumer) { + if (!$consumer) { + $this->expectException(MissingEnvVariableException::class); + $this->expectExceptionMessage('Please provide required environmental variable PACT_CONSUMER_NAME!'); + } + $config = new MockServerEnvConfig(); + $this->assertSame($consumer, $config->getConsumer()); + }); } #[TestWith(['PACT_PROVIDER_NAME', null])] #[TestWith(['PACT_PROVIDER_NAME=provider', 'provider'])] public function testProvider(string $assignment, ?string $provider): void { - putenv($assignment); - if (!$provider) { - $this->expectException(MissingEnvVariableException::class); - $this->expectExceptionMessage('Please provide required environmental variable PACT_PROVIDER_NAME!'); - } - $config = new MockServerEnvConfig(); - static::assertSame($provider, $config->getProvider()); + $this->preserveEnv('PACT_PROVIDER_NAME', $assignment, function () use ($provider) { + if (!$provider) { + $this->expectException(MissingEnvVariableException::class); + $this->expectExceptionMessage('Please provide required environmental variable PACT_PROVIDER_NAME!'); + } + $config = new MockServerEnvConfig(); + $this->assertSame($provider, $config->getProvider()); + }); } #[TestWith(['PACT_OUTPUT_DIR', null])] #[TestWith(['PACT_OUTPUT_DIR=/path/to/pact/dir', '/path/to/pact/dir'])] public function testPactDir(string $assignment, ?string $pactDir): void { - $pactDir ??= \sys_get_temp_dir(); - putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($pactDir, $config->getPactDir()); + $this->preserveEnv('PACT_OUTPUT_DIR', $assignment, function () use ($pactDir) { + $pactDir ??= \sys_get_temp_dir(); + $config = new MockServerEnvConfig(); + $this->assertSame($pactDir, $config->getPactDir()); + }); } #[TestWith(['PACT_LOG', null])] #[TestWith(['PACT_LOG=/path/to/log/dir', '/path/to/log/dir'])] public function testLog(string $assignment, ?string $logDir): void { - putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($logDir, $config->getLog()); + $this->preserveEnv('PACT_LOG', $assignment, function () use ($logDir) { + $config = new MockServerEnvConfig(); + $this->assertSame($logDir, $config->getLog()); + }); } #[TestWith(['PACT_LOGLEVEL', null])] #[TestWith(['PACT_LOGLEVEL=trace', 'TRACE'])] public function testLogLevel(string $assignment, ?string $logLevel): void { - putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($logLevel, $config->getLogLevel()); + $this->preserveEnv('PACT_LOGLEVEL', $assignment, function () use ($logLevel) { + $config = new MockServerEnvConfig(); + $this->assertSame($logLevel, $config->getLogLevel()); + }); } #[TestWith(['PACT_SPECIFICATION_VERSION', '3.0.0'])] #[TestWith(['PACT_SPECIFICATION_VERSION=1.1.0', '1.1.0'])] public function testPactSpecificationVersion(string $assignment, ?string $specificationVersion): void { + $this->preserveEnv('PACT_SPECIFICATION_VERSION', $assignment, function () use ($specificationVersion) { + $config = new MockServerEnvConfig(); + $this->assertSame($specificationVersion, $config->getPactSpecificationVersion()); + }); + } + + private function preserveEnv(string $env, string $assignment, callable $callback): void + { + $value = getenv($env); putenv($assignment); - $config = new MockServerEnvConfig(); - static::assertSame($specificationVersion, $config->getPactSpecificationVersion()); + $callback(); + putenv("$env=$value"); } } From e9675ee5e84d8d94104e5a6e9009948f928dbdc7 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 11:04:53 +0700 Subject: [PATCH 277/298] refactor: Remove file_exists checking --- composer.json | 1 - .../Driver/Body/InteractionBodyDriver.php | 2 +- .../Exception/PartNotExistException.php | 9 --------- src/PhpPact/Consumer/Model/Body/Part.php | 5 ----- .../Driver/Body/InteractionBodyDriverTest.php | 18 +++++------------- 5 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 src/PhpPact/Consumer/Exception/PartNotExistException.php diff --git a/composer.json b/composer.json index ec7f1620..211eca2e 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,6 @@ "behat/behat": "^3.13", "galbar/jsonpath": "^3.0", "ramsey/uuid": "^4.7", - "mikey179/vfsstream": "^1.6.7", "pact-foundation/example-protobuf-sync-message-provider": "@dev" }, "autoload": { diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php index c8f36525..3950bdb1 100644 --- a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php @@ -40,7 +40,7 @@ public function registerBody(Interaction $interaction, InteractionPart $interact foreach ($body->getParts() as $part) { $result = $this->client->call('pactffi_with_multipart_file_v2', $interaction->getHandle(), $partId, $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); if ($result->failed instanceof CData) { - throw new PartNotAddedException(FFI::string($result->failed)); + throw new PartNotAddedException(sprintf("Can not add part '%s': %s", $part->getName(), FFI::string($result->failed))); } } $success = true; diff --git a/src/PhpPact/Consumer/Exception/PartNotExistException.php b/src/PhpPact/Consumer/Exception/PartNotExistException.php deleted file mode 100644 index f4aec861..00000000 --- a/src/PhpPact/Consumer/Exception/PartNotExistException.php +++ /dev/null @@ -1,9 +0,0 @@ -setContentType($contentType); } diff --git a/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php index c13503e6..09a5ebb7 100644 --- a/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php @@ -4,8 +4,6 @@ use FFI; use FFI\CData; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamDirectory; use PhpPact\Consumer\Driver\Body\InteractionBodyDriver; use PhpPact\Consumer\Driver\Body\InteractionBodyDriverInterface; use PhpPact\Consumer\Driver\Enum\InteractionPart; @@ -38,15 +36,9 @@ class InteractionBodyDriverTest extends TestCase private string $boundary = 'abcde12345'; private CData $failed; private string $message = 'error'; - private vfsStreamDirectory $root; public function setUp(): void { - $this->root = vfsStream::setup(); - file_put_contents($this->root->url() . '/id.txt', 'text'); - file_put_contents($this->root->url() . '/address.json', 'json'); - file_put_contents($this->root->url() . '/image.png', 'image'); - $this->client = $this->createMock(ClientInterface::class); $this->client ->expects($this->once()) @@ -63,9 +55,9 @@ public function setUp(): void $this->binary = new Binary(__DIR__ . '/../../../../_resources/image.jpg', 'image/jpeg'); $this->text = new Text('example', 'text/plain'); $this->parts = [ - new Part($this->root->url() . '/id.txt', 'id', 'text/plain'), - new Part($this->root->url() . '/address.json', 'address', 'application/json'), - new Part($this->root->url() . '/image.png', 'profileImage', 'image/png'), + new Part('/path/to/id.txt', 'id', 'text/plain'), + new Part('/path/to//address.json', 'address', 'application/json'), + new Part('/path/to//image.png', 'profileImage', 'image/png'), ]; $this->multipart = new Multipart($this->parts, $this->boundary); $this->failed = FFI::new('char[5]'); @@ -156,7 +148,7 @@ public function testRequestMultipartBody(bool $success): void ); if (!$success) { $this->expectException(PartNotAddedException::class); - $this->expectExceptionMessage($this->message); + $this->expectExceptionMessage("Can not add part '{$this->parts[2]->getName()}': {$this->message}"); } $this->driver->registerBody($this->interaction, InteractionPart::REQUEST); } @@ -179,7 +171,7 @@ public function testResponseMultipartBody(bool $success): void ); if (!$success) { $this->expectException(PartNotAddedException::class); - $this->expectExceptionMessage($this->message); + $this->expectExceptionMessage("Can not add part '{$this->parts[2]->getName()}': {$this->message}"); } $this->driver->registerBody($this->interaction, InteractionPart::RESPONSE); } From 5099734f344a0ad8d877d9ffce71e7afb0eb12aa Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 12:25:34 +0700 Subject: [PATCH 278/298] test: Test body models --- .../Consumer/Model/Body/MultipartTest.php | 32 ++++++++++++++++ .../PhpPact/Consumer/Model/Body/PartTest.php | 37 +++++++++++++++++++ .../PhpPact/Consumer/Model/Body/TextTest.php | 30 +++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/PhpPact/Consumer/Model/Body/MultipartTest.php create mode 100644 tests/PhpPact/Consumer/Model/Body/PartTest.php create mode 100644 tests/PhpPact/Consumer/Model/Body/TextTest.php diff --git a/tests/PhpPact/Consumer/Model/Body/MultipartTest.php b/tests/PhpPact/Consumer/Model/Body/MultipartTest.php new file mode 100644 index 00000000..fde002b6 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/Body/MultipartTest.php @@ -0,0 +1,32 @@ +multipart = new Multipart([], '2a8ae6ad'); + } + + public function testParts(): void + { + $this->assertSame([], $this->multipart->getParts()); + $this->multipart->setParts($parts = [ + new Part('/path/to/file1.txt', 'file1', 'text/plain'), + new Part('/path/to/file2.csv', 'file2', 'text/csv'), + ]); + $this->assertSame($parts, $this->multipart->getParts()); + } + + public function testBoundary(): void + { + $this->assertSame('2a8ae6ad', $this->multipart->getBoundary()); + } +} diff --git a/tests/PhpPact/Consumer/Model/Body/PartTest.php b/tests/PhpPact/Consumer/Model/Body/PartTest.php new file mode 100644 index 00000000..9ecdde80 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/Body/PartTest.php @@ -0,0 +1,37 @@ +part = new Part('/path/to/file.txt', 'id', 'text/plain'); + } + + public function testPart(): void + { + $this->assertSame('/path/to/file.txt', $this->part->getPath()); + $this->part->setPath('/other/path/to/file.jpg'); + $this->assertSame('/other/path/to/file.jpg', $this->part->getPath()); + } + + public function testName(): void + { + $this->assertSame('id', $this->part->getName()); + $this->part->setName('profilePicture'); + $this->assertSame('profilePicture', $this->part->getName()); + } + + public function testContentType(): void + { + $this->assertSame('text/plain', $this->part->getContentType()); + $this->part->setContentType('application/json'); + $this->assertSame('application/json', $this->part->getContentType()); + } +} diff --git a/tests/PhpPact/Consumer/Model/Body/TextTest.php b/tests/PhpPact/Consumer/Model/Body/TextTest.php new file mode 100644 index 00000000..c2546a99 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/Body/TextTest.php @@ -0,0 +1,30 @@ +text = new Text('example', 'text/plain'); + } + + public function testContents(): void + { + $this->assertSame('example', $this->text->getContents()); + $this->text->setContents($csv = 'column1,column2,column3'); + $this->assertSame($csv, $this->text->getContents()); + } + + public function testContentType(): void + { + $this->assertSame('text/plain', $this->text->getContentType()); + $this->text->setContentType('application/json'); + $this->assertSame('application/json', $this->text->getContentType()); + } +} From eb1abfb0b134799442f547b4d8a7d1bd4d332f13 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 15:10:45 +0700 Subject: [PATCH 279/298] test: Test ArrayData::createFrom() with empty array --- tests/PhpPact/FFI/Model/ArrayDataTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/PhpPact/FFI/Model/ArrayDataTest.php b/tests/PhpPact/FFI/Model/ArrayDataTest.php index b55a5f65..3de17e15 100644 --- a/tests/PhpPact/FFI/Model/ArrayDataTest.php +++ b/tests/PhpPact/FFI/Model/ArrayDataTest.php @@ -8,6 +8,11 @@ class ArrayDataTest extends TestCase { + public function testCreateFromEmptyArray(): void + { + $this->assertNull(ArrayData::createFrom([])); + } + public function testCreateFromArray() { $branches = ['feature-x', 'master', 'test', 'prod']; From 74a2d3338381fc478c66e524b744badeb6a5182c Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 15:22:43 +0700 Subject: [PATCH 280/298] test: Test FFI Client --- tests/PhpPact/FFI/ClientTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/PhpPact/FFI/ClientTest.php diff --git a/tests/PhpPact/FFI/ClientTest.php b/tests/PhpPact/FFI/ClientTest.php new file mode 100644 index 00000000..5676f02b --- /dev/null +++ b/tests/PhpPact/FFI/ClientTest.php @@ -0,0 +1,29 @@ +client = new Client(); + } + + public function testGet(): void + { + $this->assertSame(5, $this->client->get('LevelFilter_Trace')); + } + + public function testCall(): void + { + $this->expectNotToPerformAssertions(); + $this->client->call('pactffi_string_delete', null); + } +} From a2c67bb27251779e4a2902429f4f5c5c4faafc34 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 17:20:03 +0700 Subject: [PATCH 281/298] test: Test xml's mixed content --- .../xml/consumer/tests/Service/HttpClientServiceTest.php | 1 + example/xml/pacts/xmlConsumer-xmlProvider.json | 2 +- example/xml/provider/public/index.php | 1 + tests/PhpPact/Xml/XmlBuilderTest.php | 8 ++++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/example/xml/consumer/tests/Service/HttpClientServiceTest.php b/example/xml/consumer/tests/Service/HttpClientServiceTest.php index 62d691a6..d00993b7 100644 --- a/example/xml/consumer/tests/Service/HttpClientServiceTest.php +++ b/example/xml/consumer/tests/Service/HttpClientServiceTest.php @@ -27,6 +27,7 @@ public function testGetMovies() $xmlBuilder ->root( $xmlBuilder->name('movies'), + $xmlBuilder->content('List of movies'), $xmlBuilder->eachLike( $xmlBuilder->examples(1), $xmlBuilder->name('movie'), diff --git a/example/xml/pacts/xmlConsumer-xmlProvider.json b/example/xml/pacts/xmlConsumer-xmlProvider.json index 68d11370..94856eb6 100644 --- a/example/xml/pacts/xmlConsumer-xmlProvider.json +++ b/example/xml/pacts/xmlConsumer-xmlProvider.json @@ -31,7 +31,7 @@ "path": "/movies" }, "response": { - "body": "Big Buck BunnyBig Buck BunnyJan MorgensternThe plot follows a day in the life of Big Buck Bunny, during which time he meets three bullying rodents: the leader, Frank the flying squirrel, and his sidekicks Rinky the red squirrel and Gimera the chinchilla.\nThe rodents amuse themselves by harassing helpless creatures of the forest by throwing fruits, nuts, and rocks at them.Open source movie6", + "body": "Big Buck BunnyBig Buck BunnyJan MorgensternThe plot follows a day in the life of Big Buck Bunny, during which time he meets three bullying rodents: the leader, Frank the flying squirrel, and his sidekicks Rinky the red squirrel and Gimera the chinchilla.\nThe rodents amuse themselves by harassing helpless creatures of the forest by throwing fruits, nuts, and rocks at them.Open source movie6List of movies", "headers": { "Content-Type": "application/xml" }, diff --git a/example/xml/provider/public/index.php b/example/xml/provider/public/index.php index b3a81eb7..f9878e33 100644 --- a/example/xml/provider/public/index.php +++ b/example/xml/provider/public/index.php @@ -13,6 +13,7 @@ << + List of movies PHP: Behind the Parser diff --git a/tests/PhpPact/Xml/XmlBuilderTest.php b/tests/PhpPact/Xml/XmlBuilderTest.php index abc731c7..0c5de8d6 100644 --- a/tests/PhpPact/Xml/XmlBuilderTest.php +++ b/tests/PhpPact/Xml/XmlBuilderTest.php @@ -42,6 +42,7 @@ public function testJsonSerialize(): void $this->builder->name('ns1:projects'), $this->builder->attribute('id', '1234'), $this->builder->attribute('xmlns:ns1', 'http://some.namespace/and/more/stuff'), + $this->builder->content('List of projects'), $this->builder->eachLike( $this->builder->examples(2), $this->builder->name('ns1:project'), @@ -52,6 +53,7 @@ public function testJsonSerialize(): void $this->builder->contentLike('Project 1 description'), $this->builder->add( $this->builder->name('ns1:tasks'), + $this->builder->content('List of tasks'), $this->builder->eachLike( $this->builder->examples(5), $this->builder->name('ns1:task'), @@ -110,6 +112,9 @@ public function testJsonSerialize(): void ], 'examples' => 5, ], + [ + 'content' => 'List of tasks', + ], ], 'attributes' => [], ], @@ -137,6 +142,9 @@ public function testJsonSerialize(): void ], 'examples' => 2, ], + [ + 'content' => 'List of projects', + ], ], 'attributes' => [ 'id' => '1234', From cf213a9faab4964d0b5c36fa31a835c0c7824165 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 17:24:37 +0700 Subject: [PATCH 282/298] chore: Remove unused exception PactNotRegisteredException --- .../Consumer/Exception/PactNotRegisteredException.php | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/PhpPact/Consumer/Exception/PactNotRegisteredException.php diff --git a/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php b/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php deleted file mode 100644 index 8af9a00b..00000000 --- a/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php +++ /dev/null @@ -1,9 +0,0 @@ - Date: Fri, 8 Mar 2024 19:00:16 +0700 Subject: [PATCH 283/298] test: Test add/set provider state for interaction/message --- .../Consumer/Model/InteractionTest.php | 52 ++++++++++++++++ tests/PhpPact/Consumer/Model/MessageTest.php | 61 ++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/PhpPact/Consumer/Model/InteractionTest.php b/tests/PhpPact/Consumer/Model/InteractionTest.php index 92c53e67..1458a760 100644 --- a/tests/PhpPact/Consumer/Model/InteractionTest.php +++ b/tests/PhpPact/Consumer/Model/InteractionTest.php @@ -9,6 +9,7 @@ use PhpPact\Consumer\Model\Interaction; use PhpPact\Consumer\Model\ProviderResponse; use PhpPact\Consumer\Model\ProviderState; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class InteractionTest extends TestCase @@ -72,4 +73,55 @@ public function testGetHeaders(): void $this->assertSame($requestHeaders, $this->interaction->getHeaders(InteractionPart::REQUEST)); $this->assertSame($responseHeaders, $this->interaction->getHeaders(InteractionPart::RESPONSE)); } + + #[TestWith([false])] + #[TestWith([true])] + public function testSetProviderState(bool $overwrite): void + { + $this->interaction->setProviderState('provider state 1', ['key 1' => 'value 1'], true); + $providerStates = $this->interaction->setProviderState('provider state 2', ['key 2' => 'value 2'], $overwrite); + if ($overwrite) { + $this->assertCount(1, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } else { + $this->assertCount(2, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 1', $providerState->getName()); + $this->assertSame(['key 1' => 'value 1'], $providerState->getParams()); + $providerState = end($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } + } + + #[TestWith([false])] + #[TestWith([true])] + public function testAddProviderState(bool $overwrite): void + { + $this->assertSame($this->interaction, $this->interaction->addProviderState('provider state 1', ['key 1' => 'value 1'], true)); + $this->assertSame($this->interaction, $this->interaction->addProviderState('provider state 2', ['key 2' => 'value 2'], $overwrite)); + $providerStates = $this->interaction->getProviderStates(); + if ($overwrite) { + $this->assertCount(1, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } else { + $this->assertCount(2, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 1', $providerState->getName()); + $this->assertSame(['key 1' => 'value 1'], $providerState->getParams()); + $providerState = end($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } + } } diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index d18fd22f..14fd20bc 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -5,10 +5,18 @@ use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class MessageTest extends TestCase { + private Message $message; + + public function setUp(): void + { + $this->message = new Message(); + } + public function testSetters() { $handle = 123; @@ -18,7 +26,7 @@ public function testSetters() $metadata = ['queue' => 'foo', 'routing_key' => 'bar']; $contents = 'test'; - $subject = (new Message()) + $subject = $this->message ->setHandle($handle) ->setDescription($description) ->addProviderState($providerStateName, $providerStateParams) @@ -39,4 +47,55 @@ public function testSetters() $this->assertEquals($contents, $messageContents->getContents()); $this->assertEquals('text/plain', $messageContents->getContentType()); } + + #[TestWith([false])] + #[TestWith([true])] + public function testSetProviderState(bool $overwrite): void + { + $this->message->setProviderState('provider state 1', ['key 1' => 'value 1'], true); + $providerStates = $this->message->setProviderState('provider state 2', ['key 2' => 'value 2'], $overwrite); + if ($overwrite) { + $this->assertCount(1, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } else { + $this->assertCount(2, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 1', $providerState->getName()); + $this->assertSame(['key 1' => 'value 1'], $providerState->getParams()); + $providerState = end($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } + } + + #[TestWith([false])] + #[TestWith([true])] + public function testAddProviderState(bool $overwrite): void + { + $this->assertSame($this->message, $this->message->addProviderState('provider state 1', ['key 1' => 'value 1'], true)); + $this->assertSame($this->message, $this->message->addProviderState('provider state 2', ['key 2' => 'value 2'], $overwrite)); + $providerStates = $this->message->getProviderStates(); + if ($overwrite) { + $this->assertCount(1, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } else { + $this->assertCount(2, $providerStates); + $providerState = reset($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 1', $providerState->getName()); + $this->assertSame(['key 1' => 'value 1'], $providerState->getParams()); + $providerState = end($providerStates); + $this->assertInstanceOf(ProviderState::class, $providerState); + $this->assertSame('provider state 2', $providerState->getName()); + $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); + } + } } From 6f31f4ba7fe8a4f28da2987c2218448fdeb44f2e Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 21:07:05 +0700 Subject: [PATCH 284/298] test: Test VerifierConfig --- .../Model/VerifierConfigTest.php | 105 ++++++------------ .../ProviderVerifier/VerifierTest.php | 1 - 2 files changed, 35 insertions(+), 71 deletions(-) diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php index 48446b0a..4ad73247 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php @@ -2,8 +2,14 @@ namespace PhpPactTest\Standalone\ProviderVerifier\Model; -use GuzzleHttp\Psr7\Uri; +use PhpPact\Standalone\ProviderVerifier\Model\Config\CallingApp; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ConsumerFilters; +use PhpPact\Standalone\ProviderVerifier\Model\Config\FilterInfo; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderInfo; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderState; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\Config\PublishOptions; +use PhpPact\Standalone\ProviderVerifier\Model\Config\VerificationOptions; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PHPUnit\Framework\TestCase; @@ -11,78 +17,37 @@ class VerifierConfigTest extends TestCase { public function testSetters() { - $providerName = 'someProvider'; - $scheme = 'https'; - $host = 'test-host'; - $port = 7202; - $basePath = '/path'; - $filterDescription = 'request to /hello'; - $filterNoState = true; - $filterState = 'given state'; - $stateChangeUrl = new Uri('http://domain.com/change'); - $stateChangeAsBody = true; - $stateChangeTeardown = true; - $requestTimeout = 500; - $disableSslVerification = true; - $publishResults = true; - $providerTags = ['feature-x', 'master', 'test', 'prod']; - $providerVersion = '1.2.3'; - $buildUrl = new Uri('http://ci/build/1'); - $providerBranch = 'some-branch'; - $filterConsumerNames = ['http-consumer-1', 'http-consumer-2', 'message-consumer-2']; + $callingApp = new CallingApp(); + $providerInfo = new ProviderInfo(); + $filterInfo = new FilterInfo(); + $providerState = new ProviderState(); + $verificationOptions = new VerificationOptions(); + $publishOptions = new PublishOptions(); + $consumerFilters = new ConsumerFilters(); + $providerTransports = [ + new ProviderTransport(), + new ProviderTransport(), + new ProviderTransport(), + ]; $subject = new VerifierConfig(); - $subject->getProviderInfo() - ->setName($providerName) - ->setScheme($scheme) - ->setHost($host) - ->setPort($port) - ->setPath($basePath); - $subject->getFilterInfo() - ->setFilterDescription($filterDescription) - ->setFilterNoState($filterNoState) - ->setFilterState($filterState); - $subject->getProviderState() - ->setStateChangeUrl($stateChangeUrl) - ->setStateChangeAsBody($stateChangeAsBody) - ->setStateChangeTeardown($stateChangeTeardown); - $subject->getVerificationOptions() - ->setRequestTimeout($requestTimeout) - ->setDisableSslVerification($disableSslVerification); - $publishOptions = new PublishOptions(); - $publishOptions - ->setProviderTags($providerTags) - ->setProviderVersion($providerVersion) - ->setBuildUrl($buildUrl) - ->setProviderBranch($providerBranch); + $subject->setCallingApp($callingApp); + $subject->setProviderInfo($providerInfo); + $subject->setFilterInfo($filterInfo); + $subject->setProviderState($providerState); + $subject->setVerificationOptions($verificationOptions); $subject->setPublishOptions($publishOptions); - $subject->getConsumerFilters() - ->setFilterConsumerNames($filterConsumerNames); + $subject->setConsumerFilters($consumerFilters); + $subject->setProviderTransports($providerTransports); - $providerInfo = $subject->getProviderInfo(); - static::assertSame($providerName, $providerInfo->getName()); - static::assertSame($scheme, $providerInfo->getScheme()); - static::assertSame($host, $providerInfo->getHost()); - static::assertSame($port, $providerInfo->getPort()); - static::assertSame($basePath, $providerInfo->getPath()); - $filterInfo = $subject->getFilterInfo(); - static::assertSame($filterDescription, $filterInfo->getFilterDescription()); - static::assertSame($filterNoState, $filterInfo->getFilterNoState()); - static::assertSame($filterState, $filterInfo->getFilterState()); - $providerState = $subject->getProviderState(); - static::assertSame($stateChangeUrl, $providerState->getStateChangeUrl()); - static::assertSame($stateChangeAsBody, $providerState->isStateChangeAsBody()); - static::assertSame($stateChangeTeardown, $providerState->isStateChangeTeardown()); - $verificationOptions = $subject->getVerificationOptions(); - static::assertSame($requestTimeout, $verificationOptions->getRequestTimeout()); - static::assertSame($disableSslVerification, $verificationOptions->isDisableSslVerification()); - static::assertSame($publishResults, $subject->isPublishResults()); - $publishOptions = $subject->getPublishOptions(); - static::assertSame($providerTags, $publishOptions->getProviderTags()); - static::assertSame($providerVersion, $publishOptions->getProviderVersion()); - static::assertSame($buildUrl, $publishOptions->getBuildUrl()); - static::assertSame($providerBranch, $publishOptions->getProviderBranch()); - $consumerFilters = $subject->getConsumerFilters(); - static::assertSame($filterConsumerNames, $consumerFilters->getFilterConsumerNames()); + $this->assertSame($callingApp, $subject->getCallingApp()); + $this->assertSame($providerInfo, $subject->getProviderInfo()); + $this->assertSame($filterInfo, $subject->getFilterInfo()); + $this->assertSame($providerState, $subject->getProviderState()); + $this->assertSame($verificationOptions, $subject->getVerificationOptions()); + $this->assertTrue($subject->isPublishResults()); + $this->assertSame($publishOptions, $subject->getPublishOptions()); + $this->assertSame($consumerFilters, $subject->getConsumerFilters()); + $this->assertSame($providerTransports, $subject->getProviderTransports()); } } diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 472dc1a8..52a3ebbf 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -17,7 +17,6 @@ use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; use PhpPact\Standalone\ProviderVerifier\Verifier; use PhpPactTest\Helper\FFI\ClientTrait; -use PhpPactTest\Helper\PhpProcess; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; From 079100b2ceb9f8199c6ab7c3d28c0f2ce72da9ea Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 23:50:25 +0700 Subject: [PATCH 285/298] refactor: Update exception's parent --- .../Consumer/Driver/Body/InteractionBodyDriver.php | 4 ++-- src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php | 2 +- .../Exception/InteractionBodyNotAddedException.php | 7 +++++++ .../Exception/MessageContentsNotAddedException.php | 7 +++++++ .../{ => Driver}/Exception/PactFileNotWroteException.php | 6 ++---- .../Consumer/Driver/Exception/PartNotAddedException.php | 7 +++++++ src/PhpPact/Consumer/Driver/Pact/PactDriver.php | 2 +- .../Consumer/Exception/BinaryFileNotExistException.php | 4 +--- .../Consumer/Exception/BinaryFileReadException.php | 4 +--- .../Consumer/Exception/BodyNotSupportedException.php | 4 +--- .../Exception/InteractionBodyNotAddedException.php | 9 --------- .../Exception/MessageContentsNotAddedException.php | 9 --------- .../Consumer/Exception/MockServerNotStartedException.php | 4 +--- .../Exception/MockServerNotWrotePactFileException.php | 4 +--- src/PhpPact/Consumer/Exception/PartNotAddedException.php | 9 --------- .../Plugin/Exception/PluginBodyNotAddedException.php | 4 +--- src/PhpPact/Plugin/Exception/PluginException.php | 9 +++++++++ .../PluginNotSupportedBySpecificationException.php | 4 +--- .../Exception/LogLevelNotSupportedException.php | 4 +--- .../StubService/Exception/StubServerException.php | 9 +++++++++ .../Consumer/Driver/Body/InteractionBodyDriverTest.php | 4 ++-- .../Consumer/Driver/Body/MessageBodyDriverTest.php | 2 +- tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php | 2 +- tests/PhpPact/Helper/Exception/HelperException.php | 4 ++-- 24 files changed, 59 insertions(+), 65 deletions(-) create mode 100644 src/PhpPact/Consumer/Driver/Exception/InteractionBodyNotAddedException.php create mode 100644 src/PhpPact/Consumer/Driver/Exception/MessageContentsNotAddedException.php rename src/PhpPact/Consumer/{ => Driver}/Exception/PactFileNotWroteException.php (78%) create mode 100644 src/PhpPact/Consumer/Driver/Exception/PartNotAddedException.php delete mode 100644 src/PhpPact/Consumer/Exception/InteractionBodyNotAddedException.php delete mode 100644 src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php delete mode 100644 src/PhpPact/Consumer/Exception/PartNotAddedException.php create mode 100644 src/PhpPact/Plugin/Exception/PluginException.php create mode 100644 src/PhpPact/Standalone/StubService/Exception/StubServerException.php diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php index 3950bdb1..77bad1ec 100644 --- a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php @@ -5,8 +5,8 @@ use FFI; use FFI\CData; use PhpPact\Consumer\Driver\Enum\InteractionPart; -use PhpPact\Consumer\Exception\InteractionBodyNotAddedException; -use PhpPact\Consumer\Exception\PartNotAddedException; +use PhpPact\Consumer\Driver\Exception\InteractionBodyNotAddedException; +use PhpPact\Consumer\Driver\Exception\PartNotAddedException; use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; diff --git a/src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php index d829eec1..58eea209 100644 --- a/src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Body/MessageBodyDriver.php @@ -2,7 +2,7 @@ namespace PhpPact\Consumer\Driver\Body; -use PhpPact\Consumer\Exception\MessageContentsNotAddedException; +use PhpPact\Consumer\Driver\Exception\MessageContentsNotAddedException; use PhpPact\Consumer\Model\Body\Binary; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; diff --git a/src/PhpPact/Consumer/Driver/Exception/InteractionBodyNotAddedException.php b/src/PhpPact/Consumer/Driver/Exception/InteractionBodyNotAddedException.php new file mode 100644 index 00000000..72d7dcaf --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Exception/InteractionBodyNotAddedException.php @@ -0,0 +1,7 @@ + Date: Fri, 8 Mar 2024 23:10:57 +0700 Subject: [PATCH 286/298] test: Test body for interaction/message --- .../Consumer/Model/ConsumerRequestTest.php | 40 +++++++++++++++++++ tests/PhpPact/Consumer/Model/MessageTest.php | 40 +++++++++++++++++++ .../Consumer/Model/ProviderResponseTest.php | 40 +++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 9ef45bb7..b22bff7d 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -3,12 +3,22 @@ namespace PhpPactTest\Consumer\Model; use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ConsumerRequest; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class ConsumerRequestTest extends TestCase { + private ConsumerRequest $request; + + public function setUp(): void + { + $this->request = new ConsumerRequest(); + } + public function testSerializing() { $model = new ConsumerRequest(); @@ -56,4 +66,34 @@ public function testSerializingWhenPathUsingMatcher() $this->assertEquals('{"status":"finished"}', $body->getContents()); $this->assertEquals('application/json', $body->getContentType()); } + + #[TestWith([null])] + #[TestWith([new Text('column1,column2,column3', 'text/csv')])] + #[TestWith([new Binary('/path/to/image.png', 'image/png')])] + #[TestWith([new Multipart([], 'abc123')])] + public function testBody(mixed $body): void + { + $this->assertSame($this->request, $this->request->setBody($body)); + $this->assertSame($body, $this->request->getBody()); + } + + public function testTextBody(): void + { + $text = 'example text'; + $this->assertSame($this->request, $this->request->setBody($text)); + $body = $this->request->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertSame($text, $body->getContents()); + $this->assertSame('text/plain', $body->getContentType()); + } + + public function testJsonBody(): void + { + $array = ['key' => 'value']; + $this->assertSame($this->request, $this->request->setBody($array)); + $body = $this->request->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertSame('{"key":"value"}', $body->getContents()); + $this->assertSame('application/json', $body->getContentType()); + } } diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index 14fd20bc..b426a3a7 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -2,6 +2,9 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Exception\BodyNotSupportedException; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; @@ -98,4 +101,41 @@ public function testAddProviderState(bool $overwrite): void $this->assertSame(['key 2' => 'value 2'], $providerState->getParams()); } } + + #[TestWith([null])] + #[TestWith([new Text('column1,column2,column3', 'text/csv')])] + #[TestWith([new Binary('/path/to/image.png', 'image/png')])] + public function testContents(mixed $contents): void + { + $this->assertSame($this->message, $this->message->setContents($contents)); + $this->assertSame($contents, $this->message->getContents()); + } + + public function testTextContents(): void + { + $text = 'example text'; + $this->assertSame($this->message, $this->message->setContents($text)); + $contents = $this->message->getContents(); + $this->assertInstanceOf(Text::class, $contents); + $this->assertSame($text, $contents->getContents()); + $this->assertSame('text/plain', $contents->getContentType()); + } + + public function testJsonContents(): void + { + $array = ['key' => 'value']; + $this->assertSame($this->message, $this->message->setContents($array)); + $contents = $this->message->getContents(); + $this->assertInstanceOf(Text::class, $contents); + $this->assertSame('{"key":"value"}', $contents->getContents()); + $this->assertSame('application/json', $contents->getContentType()); + } + + public function testMultipartContents(): void + { + $this->expectException(BodyNotSupportedException::class); + $this->expectExceptionMessage('Message does not support multipart'); + $multipart = new Multipart([], 'abc123'); + $this->message->setContents($multipart); + } } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 315322e7..d7517ad8 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -2,12 +2,22 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Multipart; use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ProviderResponse; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class ProviderResponseTest extends TestCase { + private ProviderResponse $response; + + public function setUp(): void + { + $this->response = new ProviderResponse(); + } + public function testSerializing() { $model = new ProviderResponse(); @@ -26,4 +36,34 @@ public function testSerializing() $this->assertEquals('{"currentCity":"Austin"}', $body->getContents()); $this->assertEquals('application/json', $body->getContentType()); } + + #[TestWith([null])] + #[TestWith([new Text('column1,column2,column3', 'text/csv')])] + #[TestWith([new Binary('/path/to/image.png', 'image/png')])] + #[TestWith([new Multipart([], 'abc123')])] + public function testBody(mixed $body): void + { + $this->assertSame($this->response, $this->response->setBody($body)); + $this->assertSame($body, $this->response->getBody()); + } + + public function testTextBody(): void + { + $text = 'example text'; + $this->assertSame($this->response, $this->response->setBody($text)); + $body = $this->response->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertSame($text, $body->getContents()); + $this->assertSame('text/plain', $body->getContentType()); + } + + public function testJsonBody(): void + { + $array = ['key' => 'value']; + $this->assertSame($this->response, $this->response->setBody($array)); + $body = $this->response->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertSame('{"key":"value"}', $body->getContents()); + $this->assertSame('application/json', $body->getContentType()); + } } From 8e5a3c50d1930e3d4d3491c123d9305ea2cc1ee4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 9 Mar 2024 12:39:24 +0700 Subject: [PATCH 287/298] test: Change ffi method to test. Assert result --- tests/PhpPact/FFI/ClientTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/PhpPact/FFI/ClientTest.php b/tests/PhpPact/FFI/ClientTest.php index 5676f02b..9aca443c 100644 --- a/tests/PhpPact/FFI/ClientTest.php +++ b/tests/PhpPact/FFI/ClientTest.php @@ -5,6 +5,7 @@ use FFI; use PhpPact\FFI\Client; use PhpPact\FFI\ClientInterface; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; class ClientTest extends TestCase @@ -21,9 +22,10 @@ public function testGet(): void $this->assertSame(5, $this->client->get('LevelFilter_Trace')); } - public function testCall(): void + #[TestWith(['abc123', true])] + #[TestWith(['testing', false])] + public function testCall(string $example, bool $result): void { - $this->expectNotToPerformAssertions(); - $this->client->call('pactffi_string_delete', null); + $this->assertSame($result, $this->client->call('pactffi_check_regex', '\w{3}\d+', $example)); } } From 75d2967cfe7cf009248ea33b8afe559500f5494d Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 9 Mar 2024 13:58:05 +0700 Subject: [PATCH 288/298] refactor(test): Reuse ClientTrait --- .../Consumer/Service/MockServerTest.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/PhpPact/Consumer/Service/MockServerTest.php b/tests/PhpPact/Consumer/Service/MockServerTest.php index 0d623ab6..84ef0c5b 100644 --- a/tests/PhpPact/Consumer/Service/MockServerTest.php +++ b/tests/PhpPact/Consumer/Service/MockServerTest.php @@ -13,14 +13,16 @@ use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\MockServerConfig; use PhpPact\Standalone\MockService\MockServerConfigInterface; +use PhpPactTest\Helper\FFI\ClientTrait; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class MockServerTest extends TestCase { + use ClientTrait; + protected MockServerInterface $mockServer; - protected ClientInterface|MockObject $client; protected PactDriverInterface|MockObject $pactDriver; protected MockServerConfigInterface $config; protected int $pactHandle = 123; @@ -140,18 +142,4 @@ public function testCleanUp(): void ->method('cleanUp'); $this->mockServer->cleanUp(); } - - protected function assertClientCalls(array $calls): void - { - $this->client - ->expects($this->exactly(count($calls))) - ->method('call') - ->willReturnCallback(function (...$args) use (&$calls) { - $call = array_shift($calls); - $return = array_pop($call); - $this->assertSame($call, $args); - - return $return; - }); - } } From 4f0685cd9c0f13c4a3f82ceba3c066780c1008ab Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 8 Mar 2024 15:40:36 +0700 Subject: [PATCH 289/298] test: Test GrpcMockServer --- .../Consumer/Service/MockServerTest.php | 29 +++++++++++-------- .../Protobuf/Service/GrpcMockServerTest.php | 20 +++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 tests/PhpPact/Plugins/Protobuf/Service/GrpcMockServerTest.php diff --git a/tests/PhpPact/Consumer/Service/MockServerTest.php b/tests/PhpPact/Consumer/Service/MockServerTest.php index 84ef0c5b..50b83c6b 100644 --- a/tests/PhpPact/Consumer/Service/MockServerTest.php +++ b/tests/PhpPact/Consumer/Service/MockServerTest.php @@ -1,6 +1,6 @@ mockServer = new MockServer($this->client, $this->pactDriver, $this->config); } - #[TestWith([234, true, 'https'])] - #[TestWith([234, false, 'http'])] - #[TestWith([0, true, 'https'])] - #[TestWith([-1, true, 'https'])] - #[TestWith([-2, true, 'https'])] - #[TestWith([-3, true, 'https'])] - #[TestWith([-4, true, 'https'])] - #[TestWith([-5, true, 'https'])] - #[TestWith([-6, true, 'https'])] - public function testStart(int $returnedPort, bool $secure, string $transport): void + #[TestWith([234, true])] + #[TestWith([234, false])] + #[TestWith([0, true])] + #[TestWith([-1, true])] + #[TestWith([-2, true])] + #[TestWith([-3, true])] + #[TestWith([-4, true])] + #[TestWith([-5, true])] + #[TestWith([-6, true])] + public function testStart(int $returnedPort, bool $secure): void { $this->config->setHost($this->host); $this->config->setPort($this->port); @@ -57,7 +57,7 @@ public function testStart(int $returnedPort, bool $secure, string $transport): v ->method('getPact') ->willReturn(new Pact($this->pactHandle)); $calls = [ - ['pactffi_create_mock_server_for_transport', $this->pactHandle, $this->host, $this->port, $transport, null, $returnedPort], + ['pactffi_create_mock_server_for_transport', $this->pactHandle, $this->host, $this->port, $this->getTransport($secure), null, $returnedPort], ]; $this->assertClientCalls($calls); if ($returnedPort < 0) { @@ -142,4 +142,9 @@ public function testCleanUp(): void ->method('cleanUp'); $this->mockServer->cleanUp(); } + + protected function getTransport(bool $secure): string + { + return $secure ? 'https' : 'http'; + } } diff --git a/tests/PhpPact/Plugins/Protobuf/Service/GrpcMockServerTest.php b/tests/PhpPact/Plugins/Protobuf/Service/GrpcMockServerTest.php new file mode 100644 index 00000000..98bdc186 --- /dev/null +++ b/tests/PhpPact/Plugins/Protobuf/Service/GrpcMockServerTest.php @@ -0,0 +1,20 @@ +mockServer = new GrpcMockServer($this->client, $this->pactDriver, $this->config); + } + + protected function getTransport(bool $secure): string + { + return 'grpc'; + } +} From 9e715dfbc414862d1186018ef5381220f5edeba4 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 9 Mar 2024 18:58:23 +0700 Subject: [PATCH 290/298] test: Test verifier with empty array config --- .../ProviderVerifier/VerifierTest.php | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 52a3ebbf..1d4adc83 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -37,6 +37,10 @@ protected function setUp(): void $this->logger = $this->createMock(LoggerInterface::class); $this->client = $this->createMock(ClientInterface::class); $this->handle = FFI::new('int'); + } + + private function setUpCalls(bool $hasProviderTags = true, bool $hasFilterConsumerNames = true): void + { $this->config->getCallingApp() ->setName($callingAppName = 'calling app name') ->setVersion($callingAppVersion = '1.2.3'); @@ -65,13 +69,13 @@ protected function setUp(): void ->setDisableSslVerification($disableSslVerification = true); $publishOptions = new PublishOptions(); $publishOptions - ->setProviderTags($providerTags = ['feature-x', 'master', 'test', 'prod']) + ->setProviderTags($providerTags = $hasProviderTags ? ['feature-x', 'master', 'test', 'prod'] : []) ->setProviderVersion($providerVersion = '1.2.3') ->setBuildUrl($buildUrl = new Uri('http://ci/build/1')) ->setProviderBranch($providerBranch = 'some-branch'); $this->config->setPublishOptions($publishOptions); $this->config->getConsumerFilters() - ->setFilterConsumerNames($filterConsumerNames = ['http-consumer-1', 'http-consumer-2', 'message-consumer-2']); + ->setFilterConsumerNames($filterConsumerNames = $hasFilterConsumerNames ? ['http-consumer-1', 'http-consumer-2', 'message-consumer-2'] : []); $this->config->setLogLevel($logLevel = 'info'); $this->calls = [ ['pactffi_verifier_new_for_application', $callingAppName, $callingAppVersion, $this->handle], @@ -80,20 +84,41 @@ protected function setUp(): void ['pactffi_verifier_set_provider_state', $this->handle, (string) $stateChangeUrl, $stateChangeTearDown, $stateChangeAsBody, null], ['pactffi_verifier_set_filter_info', $this->handle, $filterDescription, $filterState, $filterNoState, null], ['pactffi_verifier_set_verification_options', $this->handle, $disableSslVerification, $requestTimeout, null], - ['pactffi_verifier_set_publish_options', $this->handle, $providerVersion, $buildUrl, $this->isInstanceOf(CData::class), count($providerTags), $providerBranch, null], - ['pactffi_verifier_set_consumer_filters', $this->handle, $this->isInstanceOf(CData::class), count($filterConsumerNames), null], + [ + 'pactffi_verifier_set_publish_options', + $this->handle, + $providerVersion, + $buildUrl, + $hasProviderTags ? $this->isInstanceOf(CData::class) : null, + $hasProviderTags ? count($providerTags) : null, + $providerBranch, + null + ], + [ + 'pactffi_verifier_set_consumer_filters', + $this->handle, + $hasFilterConsumerNames ? $this->isInstanceOf(CData::class) : null, + $hasFilterConsumerNames ? count($filterConsumerNames) : null, + null + ], ['pactffi_init_with_log_level', strtoupper($logLevel), null], ]; } - public function testConstruct(): void + #[TestWith([true, true])] + #[TestWith([false, false])] + #[TestWith([true, false])] + #[TestWith([false, true])] + public function testConstruct(bool $hasProviderTags, bool $hasFilterConsumerNames): void { + $this->setUpCalls($hasProviderTags, $hasFilterConsumerNames); $this->assertClientCalls($this->calls); $this->verifier = new Verifier($this->config, $this->logger, $this->client); } public function testAddFile(): void { + $this->setUpCalls(); $file = '/path/to/file.json'; $this->calls[] = ['pactffi_verifier_add_file_source', $this->handle, $file, null]; $this->assertClientCalls($this->calls); @@ -103,6 +128,7 @@ public function testAddFile(): void public function testAddDirectory(): void { + $this->setUpCalls(); $directory = '/path/to/directory'; $this->calls[] = ['pactffi_verifier_add_directory_source', $this->handle, $directory, null]; $this->assertClientCalls($this->calls); @@ -112,6 +138,7 @@ public function testAddDirectory(): void public function testAddUrl(): void { + $this->setUpCalls(); $source = new Url(); $source ->setUrl($url = new Uri('http://example.test/path/to/file.json')) @@ -124,11 +151,20 @@ public function testAddUrl(): void $this->assertSame($this->verifier, $this->verifier->addUrl($source)); } - public function testAddBroker(): void + #[TestWith([true, true, true])] + #[TestWith([false, false, false])] + #[TestWith([true, false, false])] + #[TestWith([false, true, false])] + #[TestWith([false, false, true])] + public function testAddBroker(bool $hasVersionSelectors, bool $hasProviderTags, bool $hasConsumerVersionTags): void { - $consumerVersionSelectors = (new ConsumerVersionSelectors()) - ->addSelector(new Selector(tag: 'foo', latest: true)) - ->addSelector('{"tag":"bar","latest":true}'); + $this->setUpCalls(); + $consumerVersionSelectors = (new ConsumerVersionSelectors()); + if ($hasVersionSelectors) { + $consumerVersionSelectors + ->addSelector(new Selector(tag: 'foo', latest: true)) + ->addSelector('{"tag":"bar","latest":true}'); + } $source = new Broker(); $source ->setUrl($url = new Uri('http://example.test/path/to/file.json')) @@ -137,10 +173,10 @@ public function testAddBroker(): void ->setPassword($password = 'secret password') ->setEnablePending($enablePending = true) ->setIncludeWipPactSince($wipPactSince = '2020-01-30') - ->setProviderTags($providerTags = ['prod', 'staging']) + ->setProviderTags($providerTags = $hasProviderTags ? ['prod', 'staging'] : []) ->setProviderBranch($providerBranch = 'main') ->setConsumerVersionSelectors($consumerVersionSelectors) - ->setConsumerVersionTags($consumerVersionTags = ['dev']); + ->setConsumerVersionTags($consumerVersionTags = $hasConsumerVersionTags ? ['dev'] : []); $this->calls[] = [ 'pactffi_verifier_broker_source_with_selectors', $this->handle, @@ -150,13 +186,13 @@ public function testAddBroker(): void $token, $enablePending, $wipPactSince, - $this->isInstanceOf(CData::class), - count($providerTags), + $hasProviderTags ? $this->isInstanceOf(CData::class) : null, + $hasProviderTags ? count($providerTags) : null, $providerBranch, - $this->isInstanceOf(CData::class), - count($consumerVersionSelectors), - $this->isInstanceOf(CData::class), - count($consumerVersionTags), + $hasVersionSelectors ? $this->isInstanceOf(CData::class) : null, + $hasVersionSelectors ? count($consumerVersionSelectors) : null, + $hasConsumerVersionTags ? $this->isInstanceOf(CData::class) : null, + $hasConsumerVersionTags ? count($consumerVersionTags) : null, null ]; $this->assertClientCalls($this->calls); @@ -170,6 +206,7 @@ public function testAddBroker(): void #[TestWith([2, false, false])] public function testVerify(int $error, bool $success, bool $hasLogger): void { + $this->setUpCalls(); $json = '{"key": "value"}'; $this->calls[] = ['pactffi_verifier_execute', $this->handle, $error]; $this->logger From 2e510a534cdf20c495d24bc49edbecacda00c794 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Mar 2024 00:51:10 +0700 Subject: [PATCH 291/298] deps: Upgrade symfony/process --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0d235c44..bbfca0c8 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "ext-ffi": "*", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", - "symfony/process": "^4.4|^5.4|^6.0", + "symfony/process": "^5.4|^6.0|^7.0", "guzzlehttp/psr7": "^2.4.5", "tienvx/composer-downloads-plugin": "^1.2.0" }, From ad0cdbff57414b35d42d4104bd77f264b33abf18 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Mon, 11 Mar 2024 01:05:42 +0700 Subject: [PATCH 292/298] deps: Remove slim/psr7 --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0d235c44..560656c3 100644 --- a/composer.json +++ b/composer.json @@ -29,8 +29,7 @@ "require-dev": { "ext-sockets": "*", "roave/security-advisories": "dev-latest", - "slim/slim": "^4.6", - "slim/psr7": "^1.2.0", + "slim/slim": "^4.13", "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", "phpstan/phpstan": "^1.9", From 39c9992399e31db37e93c38e8605083ce6abc3b2 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 13 Mar 2024 09:03:19 +0700 Subject: [PATCH 293/298] refactor: Rename PactFileNotWroteException to PactFileNotWrittenException --- ...eNotWroteException.php => PactFileNotWrittenException.php} | 2 +- src/PhpPact/Consumer/Driver/Pact/PactDriver.php | 4 ++-- tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/PhpPact/Consumer/Driver/Exception/{PactFileNotWroteException.php => PactFileNotWrittenException.php} (87%) diff --git a/src/PhpPact/Consumer/Driver/Exception/PactFileNotWroteException.php b/src/PhpPact/Consumer/Driver/Exception/PactFileNotWrittenException.php similarity index 87% rename from src/PhpPact/Consumer/Driver/Exception/PactFileNotWroteException.php rename to src/PhpPact/Consumer/Driver/Exception/PactFileNotWrittenException.php index e6e8a6ce..cad70dea 100644 --- a/src/PhpPact/Consumer/Driver/Exception/PactFileNotWroteException.php +++ b/src/PhpPact/Consumer/Driver/Exception/PactFileNotWrittenException.php @@ -2,7 +2,7 @@ namespace PhpPact\Consumer\Driver\Exception; -class PactFileNotWroteException extends DriverException +class PactFileNotWrittenException extends DriverException { public function __construct(int $code) { diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php index e334bcb8..d82c4d37 100644 --- a/src/PhpPact/Consumer/Driver/Pact/PactDriver.php +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriver.php @@ -5,7 +5,7 @@ use Composer\Semver\Comparator; use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Exception\MissingPactException; -use PhpPact\Consumer\Driver\Exception\PactFileNotWroteException; +use PhpPact\Consumer\Driver\Exception\PactFileNotWrittenException; use PhpPact\Consumer\Model\Pact\Pact; use PhpPact\FFI\ClientInterface; @@ -36,7 +36,7 @@ public function writePact(): void $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); if ($error) { - throw new PactFileNotWroteException($error); + throw new PactFileNotWrittenException($error); } } diff --git a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php index 73db909f..93401d4e 100644 --- a/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php +++ b/tests/PhpPact/Consumer/Driver/Pact/PactDriverTest.php @@ -4,7 +4,7 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Exception\MissingPactException; -use PhpPact\Consumer\Driver\Exception\PactFileNotWroteException; +use PhpPact\Consumer\Driver\Exception\PactFileNotWrittenException; use PhpPact\Consumer\Driver\Pact\PactDriver; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\FFI\ClientInterface; @@ -154,7 +154,7 @@ public function testWritePact(int $error, string $writeMode): void $this->assertClientCalls($calls); $this->driver->setUp(); if ($error) { - $this->expectException(PactFileNotWroteException::class); + $this->expectException(PactFileNotWrittenException::class); $this->expectExceptionMessage(match ($error) { 1 => 'The function panicked.', 2 => 'The pact file was not able to be written.', From 4d549da0814936b5e11f1ecb5a0f0d90d68ea3cd Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Wed, 13 Mar 2024 09:04:53 +0700 Subject: [PATCH 294/298] refactor: Rename MockServerNotWrotePactFileException to MockServerPactFileNotWrittenException --- ...xception.php => MockServerPactFileNotWrittenException.php} | 2 +- src/PhpPact/Consumer/Service/MockServer.php | 4 ++-- tests/PhpPact/Consumer/Service/MockServerTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/PhpPact/Consumer/Exception/{MockServerNotWrotePactFileException.php => MockServerPactFileNotWrittenException.php} (85%) diff --git a/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php b/src/PhpPact/Consumer/Exception/MockServerPactFileNotWrittenException.php similarity index 85% rename from src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php rename to src/PhpPact/Consumer/Exception/MockServerPactFileNotWrittenException.php index 7ec8254f..9f763d05 100644 --- a/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php +++ b/src/PhpPact/Consumer/Exception/MockServerPactFileNotWrittenException.php @@ -2,7 +2,7 @@ namespace PhpPact\Consumer\Exception; -class MockServerNotWrotePactFileException extends ConsumerException +class MockServerPactFileNotWrittenException extends ConsumerException { public function __construct(int $code) { diff --git a/src/PhpPact/Consumer/Service/MockServer.php b/src/PhpPact/Consumer/Service/MockServer.php index 6f2eac46..b34dd9fa 100644 --- a/src/PhpPact/Consumer/Service/MockServer.php +++ b/src/PhpPact/Consumer/Service/MockServer.php @@ -6,7 +6,7 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; -use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; +use PhpPact\Consumer\Exception\MockServerPactFileNotWrittenException; use PhpPact\FFI\ClientInterface; use PhpPact\Standalone\MockService\MockServerConfigInterface; use PhpPact\Standalone\MockService\Model\VerifyResult; @@ -73,7 +73,7 @@ public function writePact(): void $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE ); if ($error) { - throw new MockServerNotWrotePactFileException($error); + throw new MockServerPactFileNotWrittenException($error); } } diff --git a/tests/PhpPact/Consumer/Service/MockServerTest.php b/tests/PhpPact/Consumer/Service/MockServerTest.php index 0d623ab6..bfe6efc6 100644 --- a/tests/PhpPact/Consumer/Service/MockServerTest.php +++ b/tests/PhpPact/Consumer/Service/MockServerTest.php @@ -6,7 +6,7 @@ use PhpPact\Config\PactConfigInterface; use PhpPact\Consumer\Driver\Pact\PactDriverInterface; use PhpPact\Consumer\Exception\MockServerNotStartedException; -use PhpPact\Consumer\Exception\MockServerNotWrotePactFileException; +use PhpPact\Consumer\Exception\MockServerPactFileNotWrittenException; use PhpPact\Consumer\Model\Pact\Pact; use PhpPact\Consumer\Service\MockServer; use PhpPact\Consumer\Service\MockServerInterface; @@ -117,7 +117,7 @@ public function testWritePact(int $error, string $writeMode): void ]; $this->assertClientCalls($calls); if ($error) { - $this->expectException(MockServerNotWrotePactFileException::class); + $this->expectException(MockServerPactFileNotWrittenException::class); $this->expectExceptionMessage(match ($error) { 1 => 'A general panic was caught', 2 => 'The pact file was not able to be written', From dd7c3405d730ce30832d7128db3ad45914098702 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 14 Mar 2024 16:31:00 +0700 Subject: [PATCH 295/298] deps: Require ext-openssl --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index a99bed3d..15b41f10 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ ], "require": { "php": "^8.1", + "ext-openssl": "*", "ext-ffi": "*", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", From 1c98862aabbd21e984a593c21c149c2a965ff121 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Thu, 14 Mar 2024 17:26:13 +0700 Subject: [PATCH 296/298] fix: Swap implementation of setFilterInfo() and setProviderState() --- .../Standalone/ProviderVerifier/Verifier.php | 16 ++++++++-------- .../Standalone/ProviderVerifier/VerifierTest.php | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index daa3efa6..5d6918a8 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -69,22 +69,22 @@ private function setProviderTransports(VerifierConfigInterface $config): void private function setFilterInfo(VerifierConfigInterface $config): void { $this->client->call( - 'pactffi_verifier_set_provider_state', + 'pactffi_verifier_set_filter_info', $this->handle, - $config->getProviderState()->getStateChangeUrl() ? (string) $config->getProviderState()->getStateChangeUrl() : null, - $config->getProviderState()->isStateChangeTeardown(), - $config->getProviderState()->isStateChangeAsBody() + $config->getFilterInfo()->getFilterDescription(), + $config->getFilterInfo()->getFilterState(), + $config->getFilterInfo()->getFilterNoState() ); } private function setProviderState(VerifierConfigInterface $config): void { $this->client->call( - 'pactffi_verifier_set_filter_info', + 'pactffi_verifier_set_provider_state', $this->handle, - $config->getFilterInfo()->getFilterDescription(), - $config->getFilterInfo()->getFilterState(), - $config->getFilterInfo()->getFilterNoState() + $config->getProviderState()->getStateChangeUrl() ? (string) $config->getProviderState()->getStateChangeUrl() : null, + $config->getProviderState()->isStateChangeTeardown(), + $config->getProviderState()->isStateChangeAsBody() ); } diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 1d4adc83..aa4d17b8 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -81,8 +81,8 @@ private function setUpCalls(bool $hasProviderTags = true, bool $hasFilterConsume ['pactffi_verifier_new_for_application', $callingAppName, $callingAppVersion, $this->handle], ['pactffi_verifier_set_provider_info', $this->handle, $providerName, $providerScheme, $providerHost, $providerPort, $providerPath, null], ['pactffi_verifier_add_provider_transport', $this->handle, $transportProtocol, $transportPort, $transportPath, $transportScheme, null], - ['pactffi_verifier_set_provider_state', $this->handle, (string) $stateChangeUrl, $stateChangeTearDown, $stateChangeAsBody, null], ['pactffi_verifier_set_filter_info', $this->handle, $filterDescription, $filterState, $filterNoState, null], + ['pactffi_verifier_set_provider_state', $this->handle, (string) $stateChangeUrl, $stateChangeTearDown, $stateChangeAsBody, null], ['pactffi_verifier_set_verification_options', $this->handle, $disableSslVerification, $requestTimeout, null], [ 'pactffi_verifier_set_publish_options', From cc1cb750296fb4f0f16d25ccea6b6f9da6d87145 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Fri, 15 Mar 2024 17:42:54 +0700 Subject: [PATCH 297/298] chore: Keep start-provider script but return error --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 15b41f10..c707b702 100644 --- a/composer.json +++ b/composer.json @@ -96,6 +96,7 @@ } }, "scripts": { + "start-provider": "echo 'removed in 10.x' & exit 1", "static-code-analysis": "phpstan", "lint": "php-cs-fixer fix --dry-run", "fix": "php-cs-fixer fix", From dba67dd2d465f179c03ca05bb596521ba3808634 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Tue, 26 Mar 2024 10:25:28 +0700 Subject: [PATCH 298/298] deps: Use pact-foundation/composer-downloads-plugin --- composer.json | 8 ++++++-- example/protobuf-sync-message/provider/composer.json | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index c707b702..cd0bd71e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "composer/semver": "^1.4.0|^3.2.0", "symfony/process": "^5.4|^6.0|^7.0", "guzzlehttp/psr7": "^2.4.5", - "tienvx/composer-downloads-plugin": "^1.2.0" + "pact-foundation/composer-downloads-plugin": "^1.0.0" }, "require-dev": { "ext-sockets": "*", @@ -143,10 +143,14 @@ }, "config": { "allow-plugins": { - "tienvx/composer-downloads-plugin": true + "pact-foundation/composer-downloads-plugin": true } }, "repositories": [ + { + "type": "vcs", + "url": "https://github.com/pact-foundation/composer-downloads-plugin" + }, { "type": "path", "url": "example/protobuf-sync-message/provider" diff --git a/example/protobuf-sync-message/provider/composer.json b/example/protobuf-sync-message/provider/composer.json index a666485a..247c45fc 100644 --- a/example/protobuf-sync-message/provider/composer.json +++ b/example/protobuf-sync-message/provider/composer.json @@ -3,7 +3,7 @@ "require": { "grpc/grpc": "^1.57", "spiral/roadrunner-grpc": "^3.2", - "tienvx/composer-downloads-plugin": "^1.2" + "pact-foundation/composer-downloads-plugin": "^1.0" }, "require-dev": { "ext-grpc": "*" @@ -27,7 +27,7 @@ }, "config": { "allow-plugins": { - "tienvx/composer-downloads-plugin": true + "pact-foundation/composer-downloads-plugin": true } } }