From b45e9c011f62f2c08272f5f07c791e31afad1157 Mon Sep 17 00:00:00 2001 From: tienvx Date: Fri, 23 Dec 2022 21:47:55 +0700 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 01b069c08da56b3f88b0905585f80c2699a14444 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sat, 4 Mar 2023 21:25:50 +0700 Subject: [PATCH 10/11] Make tag optional --- src/PhpPact/Consumer/Listener/PactTestListener.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php index 6e25ccbf..15ab051a 100644 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ b/src/PhpPact/Consumer/Listener/PactTestListener.php @@ -73,16 +73,16 @@ public function endTestSuite(TestSuite $suite): void 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 ($tag = \getenv('PACT_CONSUMER_TAG')) { + $brokerConfig->setTag($tag); + } + if (($user = \getenv('PACT_BROKER_HTTP_AUTH_USER')) && ($pass = \getenv('PACT_BROKER_HTTP_AUTH_PASS')) ) { @@ -95,7 +95,6 @@ public function endTestSuite(TestSuite $suite): void } $broker = new Broker($brokerConfig); - $broker->createVersionTag(); $broker->publish(); print 'Pact file has been uploaded to the Broker successfully.'; } From b2b7b574f4af8a835a0504c5ffd36149beee2d53 Mon Sep 17 00:00:00 2001 From: tienvx Date: Mon, 6 Mar 2023 23:16:07 +0700 Subject: [PATCH 11/11] Support branch --- src/PhpPact/Consumer/Listener/PactTestListener.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php index 15ab051a..ba0a20d2 100644 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ b/src/PhpPact/Consumer/Listener/PactTestListener.php @@ -94,6 +94,10 @@ public function endTestSuite(TestSuite $suite): void $brokerConfig->setBrokerToken($bearerToken); } + if ($branch = \getenv('PACT_CONSUMER_BRANCH')) { + $brokerConfig->setBranch($branch); + } + $broker = new Broker($brokerConfig); $broker->publish(); print 'Pact file has been uploaded to the Broker successfully.';