From 8340058395829daf198580c937cf1a2a57451030 Mon Sep 17 00:00:00 2001 From: tienvx Date: Sun, 13 Jun 2021 22:55:27 +0700 Subject: [PATCH] feat: Use Rust FFI --- .github/workflows/build.yml | 6 +- README.md | 256 ++++++----- UPGRADE-8.0.md | 103 +++++ composer.json | 27 +- example/pacts/README.md | 11 - example/pacts/someconsumer-someprovider.json | 2 +- .../pacts/test_consumer-test_provider.json | 2 +- example/phpunit.all.xml | 8 - example/phpunit.consumer.xml | 4 - example/phpunit.core.xml | 5 - example/phpunit.message.provider.xml | 8 - .../Consumer/Service/HttpClientService.php | 9 +- example/src/Consumer/publish_json_example.php | 16 - .../ExampleMessageConsumer.php | 4 +- example/src/Provider/public/proxy.php | 70 +++ .../Service/ConsumerServiceGoodbyeTest.php | 18 +- .../Service/ConsumerServiceHelloTest.php | 15 +- .../ExampleMessageConsumerTest.php | 39 +- .../ExampleMessageProviderTest.php | 61 --- example/tests/Provider/PactVerifyTest.php | 28 +- phpstan.neon | 6 + phpunit.xml | 4 - .../Broker/Service/BrokerHttpClient.php | 112 ----- .../Service/BrokerHttpClientInterface.php | 61 --- src/PhpPact/Consumer/AbstractBuilder.php | 51 +++ src/PhpPact/Consumer/BuilderInterface.php | 7 - .../MockServerNotStartedException.php | 17 + .../Consumer/Hook/ContractDownloader.php | 98 ---- src/PhpPact/Consumer/InteractionBuilder.php | 120 +++-- .../Consumer/Listener/PactTestListener.php | 77 +--- src/PhpPact/Consumer/MessageBuilder.php | 111 +++-- .../Consumer/Model/ConsumerRequest.php | 115 ++--- src/PhpPact/Consumer/Model/Interaction.php | 131 ------ src/PhpPact/Consumer/Model/Message.php | 155 ------- .../Consumer/Model/ProviderResponse.php | 79 ++-- src/PhpPact/Provider/MessageVerifier.php | 245 ---------- src/PhpPact/Standalone/Broker/Broker.php | 22 +- .../Standalone/Broker/BrokerConfig.php | 92 ++-- .../Exception/HealthCheckFailedException.php | 17 - .../Standalone/Installer/InstallManager.php | 76 +--- .../Standalone/Installer/Model/Scripts.php | 71 +-- .../Installer/Service/AbstractInstaller.php | 176 ++++++++ .../Installer/Service/InstallerLinux.php | 121 ++--- .../Installer/Service/InstallerMac.php | 121 ++--- .../Installer/Service/InstallerWindows.php | 123 ++--- .../Standalone/MockService/MockServer.php | 170 ------- .../MockService/MockServerConfig.php | 286 +----------- .../MockService/MockServerConfigInterface.php | 75 +--- .../MockService/MockServerEnvConfig.php | 33 +- .../Service/MockServerHttpService.php | 173 ------- .../MockServerHttpServiceInterface.php | 51 --- .../Standalone/PactConfigInterface.php | 82 ---- .../Standalone/PactMessage/PactMessage.php | 76 ---- .../PactMessage/PactMessageConfig.php | 169 ------- .../Model/ConsumerVersionSelectors.php | 39 ++ .../ProviderVerifier/Model/VerifierConfig.php | 421 ++++++++++++------ .../Model/VerifierConfigInterface.php | 282 ++++++++---- .../ProviderVerifier/ProcessRunnerFactory.php | 25 -- .../Standalone/ProviderVerifier/Verifier.php | 301 +++++-------- .../ProviderVerifier/VerifierProcess.php | 103 ----- .../Standalone/Runner/ProcessRunner.php | 28 +- .../Service/StubServerHttpService.php | 29 +- .../StubServerHttpServiceInterface.php | 4 +- .../Standalone/StubService/StubServer.php | 82 +++- .../StubService/StubServerConfig.php | 233 ++++++++-- .../StubService/StubServerConfigInterface.php | 163 ++++++- .../Broker/Service/BrokerHttpClientTest.php | 53 --- .../Consumer/InteractionBuilderTest.php | 154 ++++--- .../PhpPact/Consumer/Matcher/MatcherTest.php | 3 +- tests/PhpPact/Consumer/MessageBuilderTest.php | 52 +++ .../Consumer/Model/ConsumerRequestTest.php | 10 +- .../Consumer/Model/ProviderResponseTest.php | 8 +- .../Standalone/Broker/BrokerConfigTest.php | 43 -- .../MockServer/MockServerConfigTest.php | 21 +- .../Standalone/MockServer/MockServerTest.php | 68 --- .../Service/MockServerHttpServiceTest.php | 210 --------- .../ProviderVerifier/VerifierProcessTest.php | 112 ----- .../ProviderVerifier/VerifierTest.php | 283 ++++-------- .../Standalone/ProviderVerifier/verifier.bat | 8 - .../Standalone/ProviderVerifier/verifier.sh | 10 - .../StubServer/StubServerConfigTest.php | 28 -- .../Service/StubServerHttpServiceTest.php | 23 +- .../StubService/StubServerConfigTest.php | 24 + .../StubServerTest.php | 12 +- tests/PhpPact/php.ini | 1 + tests/_output/.gitignore | 1 + tests/_public/index.php | 10 + 87 files changed, 2478 insertions(+), 4381 deletions(-) create mode 100644 UPGRADE-8.0.md delete mode 100644 example/pacts/README.md delete mode 100644 example/phpunit.message.provider.xml delete mode 100644 example/src/Consumer/publish_json_example.php create mode 100644 example/src/Provider/public/proxy.php delete mode 100644 example/tests/MessageProvider/ExampleMessageProviderTest.php create mode 100644 phpstan.neon delete mode 100644 src/PhpPact/Broker/Service/BrokerHttpClient.php delete mode 100644 src/PhpPact/Broker/Service/BrokerHttpClientInterface.php create mode 100644 src/PhpPact/Consumer/AbstractBuilder.php create mode 100644 src/PhpPact/Consumer/Exception/MockServerNotStartedException.php delete mode 100644 src/PhpPact/Consumer/Hook/ContractDownloader.php delete mode 100644 src/PhpPact/Consumer/Model/Interaction.php delete mode 100644 src/PhpPact/Consumer/Model/Message.php delete mode 100644 src/PhpPact/Provider/MessageVerifier.php delete mode 100644 src/PhpPact/Standalone/Exception/HealthCheckFailedException.php create mode 100644 src/PhpPact/Standalone/Installer/Service/AbstractInstaller.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 delete mode 100644 src/PhpPact/Standalone/PactConfigInterface.php delete mode 100644 src/PhpPact/Standalone/PactMessage/PactMessage.php delete mode 100644 src/PhpPact/Standalone/PactMessage/PactMessageConfig.php create mode 100644 src/PhpPact/Standalone/ProviderVerifier/Model/ConsumerVersionSelectors.php delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php delete mode 100644 src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php delete mode 100644 tests/PhpPact/Broker/Service/BrokerHttpClientTest.php create mode 100644 tests/PhpPact/Consumer/MessageBuilderTest.php delete mode 100644 tests/PhpPact/Standalone/Broker/BrokerConfigTest.php delete mode 100644 tests/PhpPact/Standalone/MockServer/MockServerTest.php delete mode 100644 tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php delete mode 100644 tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php delete mode 100755 tests/PhpPact/Standalone/ProviderVerifier/verifier.bat delete mode 100755 tests/PhpPact/Standalone/ProviderVerifier/verifier.sh delete mode 100644 tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php rename tests/PhpPact/Standalone/{StubServer => StubService}/Service/StubServerHttpServiceTest.php (68%) create mode 100644 tests/PhpPact/Standalone/StubService/StubServerConfigTest.php rename tests/PhpPact/Standalone/{StubServer => StubService}/StubServerTest.php (63%) create mode 100644 tests/_output/.gitignore create mode 100644 tests/_public/index.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 487c246f2..e0ff6d624 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.3', '7.4', '8.0' ] + php: [ '7.4', '8.0' ] steps: - uses: actions/checkout@v2 @@ -20,6 +20,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + tools: php-cs-fixer:3 - uses: ramsey/composer-install@v1 with: @@ -39,7 +40,7 @@ jobs: fail-fast: true matrix: operating-system: [ ubuntu-latest, macos-latest, windows-latest ] - php: [ '7.3', '7.4', '8.0' ] + php: [ '7.4', '8.0' ] dependencies: [ 'lowest', 'locked' ] name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies @@ -52,6 +53,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: ffi - name: Copy the inis if: runner.os == 'Windows' diff --git a/README.md b/README.md index 853b4f1de..00f1b4044 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Table of contents * [Specifications](#specifications) * [Installation](#installation) * [Basic Consumer Usage](#basic-consumer-usage) - * [Start and Stop the Mock Server](#start-and-stop-the-mock-server) + * [Configure PHPUnit](#configure-phpunit) + * [Create Mock Server Config](#create-mock-server-config) * [Create Consumer Unit Test](#create-consumer-unit-test) * [Create Mock Request](#create-mock-request) * [Create Mock Response](#create-mock-response) @@ -35,6 +36,7 @@ Table of contents * [Examples](#additional-examples) ## Versions +8.x adds support for pact specification 3.X via Pact FFI. This results in dropping support for PHP 7.3. 7.x updates internal dependencies and libraries, mostly to Guzzle 7.X. This results in dropping support for PHP 7.2. 6.x updates internal dependencies, mostly surrounding the Amp library. This results in dropping support for PHP 7.1. @@ -48,7 +50,10 @@ 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 supports [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2). +Pact-Php 8.X supports [Pact-Specification 3.X](https://github.com/pact-foundation/pact-specification/tree/version-3). ## Installation @@ -64,37 +69,59 @@ 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 +### Configure PHPUnit + +```xml + + + + + ./tests/Consumer + + + + + + + + PhpPact Example Tests + + + + + + + + + + + + + + +``` + +### Create Mock Server Config + +Create mock server config from environment variables: -This library contains a wrapper for the [Ruby Standalone Mock Service](https://github.com/pact-foundation/pact-mock_service). +```php +use PhpPact\Standalone\MockService\MockServerEnvConfig; -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. +$config = new MockServerEnvConfig(); +$config->setProvider('someProvider'); +``` -Alternatively, you can start and stop as in whatever means you would like by following this example: +Or create it manually: ```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(); +use PhpPact\Standalone\MockService\MockServerConfig; + +$config = new MockServerConfig(); +$config->setConsumer('someConsumer'); +$config->setProvider('someProvider'); +$config->setPactDir('.\example\output'); +$config->setPactSpecificationVersion('3.0.0'); ``` ### Create Consumer Unit Test @@ -108,6 +135,8 @@ Create a standard PHPUnit test case class and function. This will define what the expected request coming from your http service will look like. ```php +use PhpPact\Consumer\Model\ConsumerRequest; + $request = new ConsumerRequest(); $request ->setMethod('GET') @@ -122,6 +151,9 @@ You can also create a body just like you will see in the provider example. This will define what the response from the provider should look like. ```php +use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Model\ProviderResponse; + $matcher = new Matcher(); $response = new ProviderResponse(); @@ -136,6 +168,9 @@ $response In this example, we are using matchers. This allows us to add flexible rules when matching the expectation with the actual value. In the example, you will see regex is used to validate that the response is valid. ```php +use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Model\ProviderResponse; + $matcher = new Matcher(); $response = new ProviderResponse(); @@ -175,22 +210,27 @@ ipv6Address | Regex to match a ipv6 address. | Value (Defaults to ::ffff:192.0.2 Now that we have the request and response, we need to build the interaction and ship it over to the mock server. ```php +use PhpPact\Consumer\InteractionBuilder; +use PhpPact\Standalone\MockService\MockServerEnvConfig; + // Create a configuration that reflects the server that was started. You can // create a custom MockServerConfigInterface if needed. This configuration // is the same that is used via the PactTestListener and uses environment variables. $config = new MockServerEnvConfig(); $builder = new InteractionBuilder($config); $builder + ->newInteraction() // Support multiple interactions per mock server. Call to create new interaction. ->given('a person exists') ->uponReceiving('a get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. + ->willRespondWith($response) + ->createMockServer(); // This has to be last. ``` ### Make the Request ```php -$service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. +$service = new HttpClientService($builder->getBaseUri()); // Pass in the URL to the Mock Server. $result = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. ``` @@ -200,7 +240,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 @@ -234,28 +274,45 @@ There are three ways to verify Pact files. See the examples below. This will grab the Pact file from a Pact Broker and run the data against the stood up API. ```php +use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; +use PhpPact\Standalone\ProviderVerifier\Model\ConsumerVersionSelectors; + +$selectors = new ConsumerVersionSelectors(); +$selectors->add('master', true, 'someConsumer'); + $config = new VerifierConfig(); $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. - ->setProcessTimeout(60) // Set process timeout (optional) - default 60 - ->setProcessIdleTimeout(10) // Set process idle timeout (optional) - default 10 - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ); -// Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. + ->setProviderTags('some,branch,name') + ->setScheme('http') + ->setHost('localhost') + ->setPort(58000) + ->setBasePath('/') + ->setStateChangeUrl(new Uri('http://localhost:58000/change-state')) + ->setConsumerVersionTags('master,test,prod') + ->setConsumerVersionSelectors($selectors) + ->setBrokerUrl(new Uri('http://localhost')) // URL of the Pact Broker to publish results. + ->setBrokerToken('token') + ->setBrokerUsername('user') + ->setBrokerPassword('pass') + ->setIncludeWipPactsSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) + ->setBuildUrl(new Uri('http://build.domain.com')) + ->setFilterConsumerNames('someConsumer', 'otherConsumer') + ->setFilterDescription('Send POST to create') + ->setFilterNoState('state1') + ->setFilterState('state2') + ->setPublish(true) + ->setDisableSslVerification(true) + ->setEnablePending(true) + ->setStateChangeAsQuery(true) + ->setStateChangeTeardown(true) + ->setLogLevel('info') + ->setRequestTimeout(500); + $verifier = new Verifier($config); -$verifier->verify('someConsumer', 'master'); // The tag is option. If no tag is set it will just grab the latest. -// This will not be reached if the PACT verifier throws an error, otherwise it was successful. -$this->assertTrue(true, 'Pact Verification has failed.'); +$this->assertTrue($verifier->verify()); ``` ##### Verify All from Pact Broker @@ -263,24 +320,30 @@ $this->assertTrue(true, 'Pact Verification has failed.'); This will grab every Pact file associated with the given provider. ```php +use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; + public function testPactVerifyAll() { $config = new VerifierConfig(); $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. + ->setProviderTags('some,branch,name') + ->setScheme('http') + ->setHost('localhost') + ->setPort(58000) + ->setBasePath('/') + ->setStateChangeUrl(new Uri('http://localhost:58000/change-state')) + ->setBrokerUrl(new Uri('http://localhost')) // URL of the Pact Broker to publish results. + ->setBrokerToken('token') ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) + ->setIncludeWipPactsSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) + ; - // Verify that all consumers of 'someProvider' are valid. - $verifier = new Verifier($config); - $verifier->verifyAll(); +$verifier = new Verifier($config); - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); +$this->assertTrue($verifier->verify()); } ``` @@ -289,24 +352,28 @@ public function testPactVerifyAll() This allows local Pact file testing. ```php +use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; + public function testPactVerifyAll() { $config = new VerifierConfig(); $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true); // Flag the verifier service to publish the results to the Pact Broker. + ->setScheme('http') + ->setHost('localhost') + ->setPort(58000) + ->setBasePath('/') + ->setDirs('/path/to/pacts', '/another/path/to/pacts') + ->setFiles('consumer1-provider2.json', 'consumer2-provider2.json') + ->setPublish(true) // Flag the verifier service to publish the results to the Pact Broker. ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) + ->setIncludeWipPactsSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) + ; - // Verify that the files in the array are valid. $verifier = new Verifier($config); - $verifier->verifyFiles(['C:\SomePath\consumer-provider.json']); - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $this->assertTrue($verifier->verify()); } ``` @@ -338,7 +405,6 @@ There is a separate repository with an end to end example for both the 2.X and 3 - [2.2.1 tag](https://github.com/mattermack/pact-php-example/tree/2.2.1) for 2.X examples ## Message support -This feature is preliminary as the Pact community as a whole is flushing this out. The goal is not to test the transmission of an object over a bus but instead vet the contents of the message. While examples included focus on a Rabbit MQ, the exact message queue is irrelevant. Initial comparisons require a certain object type to be created by the Publisher/Producer and the Consumer of the message. This includes a metadata set where you @@ -360,7 +426,11 @@ The examples provided are pretty basic. See examples\tests\MessageConsumer. 1. Run Verify. If nothing blows up, #winning. ```php -$builder = new MessageBuilder(self::$config); +use PhpPact\Consumer\MessageBuilder; +use MessageConsumer\ExampleMessageConsumer; + +$config = new MockServerEnvConfig(); +$builder = new MessageBuilder($config); $contents = new \stdClass(); $contents->song = 'And the wind whispers Mary'; @@ -378,63 +448,39 @@ $consumerMessage = new ExampleMessageConsumer(); $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); -$builder->verify(); +$this->assertTrue($builder->verify()); ``` ### Provider Side Message Validation -This may evolve as we work through this implementation. The provider relies heavily on callbacks. -Some of the complexity lies in a consumer and provider having many messages and states between the each other in a single pact. - -For each message, one needs to provide a single provider state. The name of this provider state must be the key to run -a particular message callback on the provider side. See example\tests\MessageProvider -1. Create your callbacks and states wrapped in a callable object - 1. The array key is a provider state / given() on the consumer side - 1. It is helpful to wrap the whole thing in a lambda if you need to customize paramaters to be passed in -1. Choose your verification method -1. If nothing explodes, #winning - -```php +Create a proxy to handle these requests: - $callbacks = array(); +1. POST /change-state + 1. Set up your database to meet the expectations of the request + 1. Reset the database to its original state. +2. POST / + 1. Return message's content in body + 2. Return message's metadata in header `PACT-MESSAGE-METADATA` +3. If nothing matches, proxy the request to provider - // a hello message is a provider state / given() on the consumer side - $callbacks["a hello message"] = function() { - $content = new \stdClass(); - $content->text ="Hello Mary"; - - $metadata = array(); - $metadata['queue'] = "myKey"; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../output/test_consumer-test_provider.json']); - -``` +[Click here](/example/tests/Provider/public/proxy.php) to see the full sample file. ## Usage for the optional `pact-stub-service` If you would like to test with fixtures, you can use the `pact-stub-service` like this: ```php +use PhpPact\Standalone\StubService\StubServerConfig; +use PhpPact\Standalone\StubService\StubServer; +use PhpPact\Standalone\StubService\Service\StubServerHttpService; + $pactLocation = __DIR__ . '/someconsumer-someprovider.json'; -$host = 'localhost'; $port = 7201; -$endpoint = 'test'; $config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($pactLocation) + ->setPort($port); $stubServer = new StubServer($config); $stubServer->start(); diff --git a/UPGRADE-8.0.md b/UPGRADE-8.0.md new file mode 100644 index 000000000..7a95d4dfa --- /dev/null +++ b/UPGRADE-8.0.md @@ -0,0 +1,103 @@ +UPGRADE FROM 7.x to 8.0 +======================= + + * Removed `InteractionBuilder::finalize`, use `InteractionBuilder::verify` instead + * Removed `BuilderInterface::writePact`, use `BuilderInterface::verify` instead + * Removed `PactConfigInterface`, use `MockServerConfigInterface` instead + * [BC BREAK] Updated `MockServerConfigInterface`, removed methods related to http mock server + * Removed environment variables: + - PACT_LOG + - PACT_MOCK_SERVER_HOST + - PACT_MOCK_SERVER_PORT + - PACT_CORS + - PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT + - PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC + - PACT_BROKER_SSL_VERIFY + - PACT_PROVIDER_NAME + * [BC BREAK] Updated `VerifierConfigInterface`, see [Pact Verifier CLI](https://github.com/pact-foundation/pact-reference/tree/master/rust/pact_verifier_cli) + * [BC BREAK] Removed `$config->getBaseUri()`, use `$builder->getBaseUri()` instead + * [BC BREAK] Need to call `MockServerConfigInterface::setProvider` in each test case + * Allowed multiple providers per consumer's test suite + + In a test case: + ```php + $config = new MockServerEnvConfig(); + $config->setProvider('someProvider'); + $builder = new InteractionBuilder($config); + $builder + ->newInteraction() + ->given('a person exists') + ->uponReceiving('a get request to /hello/{name}') + ->with($request) + ->willRespondWith($response) + ->createMockServer(); + ``` + + In another test case: + ```php + $config = new MockServerEnvConfig(); + $config->setProvider('otherProvider'); + $builder = new InteractionBuilder($config); + $builder + ->newInteraction() + ->given('a book exists') + ->uponReceiving('a get request to /borrow/{title}') + ->with($request) + ->willRespondWith($response) + ->createMockServer(); + ``` + + * [BC BREAK] Removed `MockServer`, use `InteractionBuilder::createMockServer` instead + * [BC BREAK] Added `InteractionBuilder::newInteraction`, required to be called before each interaction. + * Allowed multiple interactions per mock server + + Example: + ```php + $builder = new InteractionBuilder($config); + $builder + ->newInteraction() + ->given('a person exists') + ->uponReceiving('a get request to /hello/{name}') + ->with($request) + ->willRespondWith($response); + $builder + ->newInteraction() + ->given('a person exists') + ->uponReceiving('a get request to /goodbye/{name}') + ->with($request) + ->willRespondWith($response) + ->createMockServer(); + ``` + + * Removed `VerifierProcess` + * Removed `ProcessRunnerFactory` + * Added `ConsumerVersionSelectors`, supports [consumer version selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/) + + Example: + ```php + $selectors = new ConsumerVersionSelectors(); + $selectors + ->add('new-feature', true, null, 'master') + ->add('test') + ->add('production') + ->add('production', false, 'my-mobile-consumer'); + $config = new VerifierConfig(); + $config + ->setConsumerVersionSelectors($selectors); + ``` + + * Removed `PactMessage` + * Removed `PactMessageConfig`, use `MockServerEnvConfig` or `MockServerConfig` instead + * Removed `MockServerHttpService` + * Removed `MockServerHttpServiceInterface` + * Removed `HealthCheckFailedException` + * Removed `MessageVerifier`, use `Verifier` instead + * Removed `BrokerHttpClient` + * Removed `BrokerHttpClientInterface` + * Added `MockServerNotStartedException` + * Removed `ContractDownloader` + * Removed `Model\Interaction` + * Removed `Model\Message` + * [BC BREAK] Removed `StubServerConfigInterface::setEndpoint`, use `StubServerHttpServiceInterface::getJson(string $endpoint)` instead + * [BC BREAK] Updated `StubServerConfigInterface`, see [pact-stub-server](https://github.com/pact-foundation/pact-stub-server) + * Updated `php-cs-fixer` to `^3.0` diff --git a/composer.json b/composer.json index b291b61bf..924f41ab2 100644 --- a/composer.json +++ b/composer.json @@ -18,33 +18,28 @@ } ], "require": { - "php": "^7.3 |^8.0", + "php": "^7.4 |^8.0", + "ext-ffi": "*", "ext-openssl": "*", "ext-json": "*", - "composer/semver": "^3.2.4", + "ext-zip": "*", + "ext-zlib": "*", "amphp/amp": "^2.5.1", "amphp/byte-stream": "^1.8", - "amphp/dns": "^1.2.3", - "amphp/hpack": "^3.1.0", - "amphp/http-server": "^2.1", "amphp/log": "^1.1", "amphp/process": "^1.1.1", - "amphp/serialization": "^1.0", - "amphp/socket": "^1.1.3", - "amphp/sync": "^1.4.0", - "amphp/cache": "v1.4.0", - "amphp/windows-registry": "v0.3.3", "guzzlehttp/guzzle": "^7.2.0", - "phpunit/phpunit": ">=8.2.3" + "phpunit/phpunit": ">=8.2.3", + "symfony/filesystem": "^5.3" }, "require-dev": { "roave/security-advisories": "dev-latest", - "mockery/mockery": "^1.4.2", "slim/slim": "^4.6", "slim/psr7": "^1.2.0", - "friendsofphp/php-cs-fixer": "^2.19", + "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", - "phpstan/phpstan": "^0.12.90" + "phpstan/phpstan": "^0.12.90", + "symfony/process": "^4.2 | ^5.0.9" }, "autoload": { "psr-4": { @@ -72,8 +67,8 @@ "post-update-cmd": [ "\\PhpPact\\Standalone\\Installer\\InstallManager::uninstall" ], - "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", - "static-code-analysis": "phpstan analyse src/ --level=5", + "start-provider": "php -S localhost:58000 -t example/src/Provider/public/ example/src/Provider/public/proxy.php", + "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" diff --git a/example/pacts/README.md b/example/pacts/README.md deleted file mode 100644 index 0d5d2a0b2..000000000 --- a/example/pacts/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Example Pacts - -The json files in this folder are explicitly here for an easy-to-read output of the test examples. These are *not* the actual test results from running all these tests of this project. By default, the pact files of this project's examples are written to example/output. The tests themselves need to generate the appropropriate files as part of the tests. - -To run the tests locally, try `composer test` - - - - - - diff --git a/example/pacts/someconsumer-someprovider.json b/example/pacts/someconsumer-someprovider.json index c502e5ba4..b06337fe4 100644 --- a/example/pacts/someconsumer-someprovider.json +++ b/example/pacts/someconsumer-someprovider.json @@ -47,7 +47,7 @@ "matchingRules": { "$.body.message": { "match": "regex", - "regex": "(Hello, )[A-Za-z]" + "regex": "(Hello, )[A-Za-z]*" } } }, diff --git a/example/pacts/test_consumer-test_provider.json b/example/pacts/test_consumer-test_provider.json index a015e9ae1..ae5e2dab4 100644 --- a/example/pacts/test_consumer-test_provider.json +++ b/example/pacts/test_consumer-test_provider.json @@ -47,7 +47,7 @@ ], "metadata": { "pactSpecification": { - "version": "2.0.0" + "version": "3.0.0" } } } diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 554c8a463..5fa9b1649 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -13,9 +13,6 @@ ./tests/MessageConsumer - - ./tests/MessageProvider - @@ -29,14 +26,9 @@ - - - - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml index 6e88b2511..909dfc2ae 100644 --- a/example/phpunit.consumer.xml +++ b/example/phpunit.consumer.xml @@ -17,14 +17,10 @@ - - - - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml index aefb2d264..aaf57cfd5 100644 --- a/example/phpunit.core.xml +++ b/example/phpunit.core.xml @@ -17,14 +17,9 @@ - - - - - diff --git a/example/phpunit.message.provider.xml b/example/phpunit.message.provider.xml deleted file mode 100644 index 2844c97d4..000000000 --- a/example/phpunit.message.provider.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ./tests/MessageProvider - - - diff --git a/example/src/Consumer/Service/HttpClientService.php b/example/src/Consumer/Service/HttpClientService.php index 668ba75b8..cc7280287 100644 --- a/example/src/Consumer/Service/HttpClientService.php +++ b/example/src/Consumer/Service/HttpClientService.php @@ -11,11 +11,8 @@ */ class HttpClientService { - /** @var Client */ - private $httpClient; - - /** @var string */ - private $baseUri; + private Client $httpClient; + private string $baseUri; public function __construct(string $baseUri) { @@ -38,7 +35,7 @@ public function getHelloString(string $name): string $body = $response->getBody(); $object = \json_decode($body); - return $object->message; + return $object->message->data->generate; } /** diff --git a/example/src/Consumer/publish_json_example.php b/example/src/Consumer/publish_json_example.php deleted file mode 100644 index 4bf6b9af1..000000000 --- a/example/src/Consumer/publish_json_example.php +++ /dev/null @@ -1,16 +0,0 @@ - 'someConsumer', - 'provider' => 'someProvider' -]); - -$httpService->publishJson($json, '1.0.0'); diff --git a/example/src/MessageConsumer/ExampleMessageConsumer.php b/example/src/MessageConsumer/ExampleMessageConsumer.php index 0a8219440..77851aa69 100644 --- a/example/src/MessageConsumer/ExampleMessageConsumer.php +++ b/example/src/MessageConsumer/ExampleMessageConsumer.php @@ -4,7 +4,7 @@ class ExampleMessageConsumer { - public function ProcessText($message) + public function ProcessText(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->text, true) . "\n"; @@ -12,7 +12,7 @@ public function ProcessText($message) return $obj; } - public function ProcessSong($message) + public function ProcessSong(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->song, true) . "\n"; diff --git a/example/src/Provider/public/proxy.php b/example/src/Provider/public/proxy.php new file mode 100644 index 000000000..d4aa1a7ad --- /dev/null +++ b/example/src/Provider/public/proxy.php @@ -0,0 +1,70 @@ +addBodyParsingMiddleware(); + +$app->post('/', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + switch ($body['description']) { + case 'an alligator named Mary exists': + $response->getBody()->write(\json_encode([ + 'text' => 'Hello Mary', + ])); + + return $response->withHeader('Content-Type', 'application/json') + ->withHeader('pact_message_metadata', \base64_encode(\json_encode([ + 'queue' => 'wind cries', + 'routing_key' => 'wind cries', + ]))); + case 'footprints dressed in red': + $response->getBody()->write(\json_encode([ + 'song' => 'And the wind whispers Mary', + ])); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('pact_message_metadata', \base64_encode(\json_encode([ + 'queue' => 'And the clowns have all gone to bed', + 'routing_key' => 'And the clowns have all gone to bed', + ]))); + + default: + break; + } + // What to do with $body['providerStates'] ? + + return $response; +}); + +$app->post('/change-state', function (Request $request, Response $response) { + $body = $request->getParsedBody(); + switch ($body['state']) { + case 'a message': + case 'You can hear happiness staggering on down the street': + if (($body['action'] ?? null) === 'teardown') { + \error_log('Removing fixtures...'); + } else { + \error_log('Creating fixtures...'); + } + + break; + + default: + break; + } + + return $response; +}); + +try { + $app->run(); +} catch (HttpNotFoundException $exception) { + return false; +} diff --git a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php index e5106f558..7a73e5c7a 100644 --- a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php @@ -2,17 +2,21 @@ namespace Consumer\Service; +use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\InteractionBuilder; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Standalone\Exception\MissingEnvVariableException; +use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; +use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; use PhpPact\Standalone\MockService\MockServerEnvConfig; use PHPUnit\Framework\TestCase; class ConsumerServiceGoodbyeTest extends TestCase { /** - * @throws MissingEnvVariableException + * @throws NoDownloaderFoundException + * @throws FileDownloadFailureException + * @throws MockServerNotStartedException */ public function testGetGoodbyeString() { @@ -31,18 +35,20 @@ public function testGetGoodbyeString() ]); $config = new MockServerEnvConfig(); + $config->setProvider('someProvider'); $builder = new InteractionBuilder($config); $builder + ->newInteraction() ->given('Get Goodbye') ->uponReceiving('A get request to /goodbye/{name}') ->with($request) - ->willRespondWith($response); + ->willRespondWith($response) + ->createMockServer(); - $service = new HttpClientService($config->getBaseUri()); + $service = new HttpClientService($builder->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 2a80f394a..74911f4a1 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php @@ -2,10 +2,12 @@ namespace Consumer\Service; +use Exception; use PhpPact\Consumer\InteractionBuilder; use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; +use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; use PhpPact\Standalone\MockService\MockServerEnvConfig; use PHPUnit\Framework\TestCase; @@ -14,7 +16,8 @@ class ConsumerServiceHelloTest extends TestCase /** * Example PACT test. * - * @throws \Exception + * @throws NoDownloaderFoundException + * @throws Exception */ public function testGetHelloString() { @@ -38,17 +41,19 @@ public function testGetHelloString() // Create a configuration that reflects the server that was started. You can create a custom MockServerConfigInterface if needed. $config = new MockServerEnvConfig(); + $config->setProvider('someProvider'); $builder = new InteractionBuilder($config); $builder + ->newInteraction() ->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) + ->createMockServer(); - $service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. + $service = new HttpClientService($builder->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->assertEquals('Hello, Bob', $result); // Make your assertions. } } diff --git a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php index c02006c09..1400213ec 100644 --- a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php +++ b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php @@ -2,12 +2,9 @@ namespace MessageConsumer; -require_once __DIR__ . '/../../src/MessageConsumer/ExampleMessageConsumer.php'; - -use PhpPact\Broker\Service\BrokerHttpClient; use PhpPact\Consumer\MessageBuilder; -use PhpPact\Http\GuzzleClient; -use PhpPact\Standalone\PactMessage\PactMessageConfig; +use PhpPact\Standalone\MockService\MockServerConfigInterface; +use PhpPact\Standalone\MockService\MockServerEnvConfig; use PHPUnit\Framework\TestCase; /** @@ -15,30 +12,18 @@ */ class ExampleMessageConsumerTest extends TestCase { - private static $config; + private static MockServerConfigInterface $config; public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$config = (new PactMessageConfig()) + self::$config = (new MockServerEnvConfig()) ->setConsumer('test_consumer') ->setProvider('test_provider') ->setPactDir(__DIR__ . '/../../output/'); } - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - - // build out brokerHttpService as your example - /* - $brokerHttpService = new BrokerHttpClient(new GuzzleClient(), new Uri($pactBrokerUri)); - $brokerHttpService->publishJson($json, $consumerVersion); - $brokerHttpService->tag($this->mockServerConfig->getConsumer(), $consumerVersion, $tag); - */ - } - /** * @throws \Exception */ @@ -62,11 +47,7 @@ public function testProcessText() $callback = [$consumerMessage, 'ProcessText']; $builder->setCallback($callback); - $hasException = false; - - $builder->verify(); - - $this->assertTrue(true, 'Expects to reach this true statement by running verify()'); + $this->assertTrue($builder->verify()); } /** @@ -92,14 +73,6 @@ public function testProcessSong() $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); - $hasException = false; - - try { - $builder->verify(); - } catch (\Exception $e) { - $hasException = true; - } - - $this->assertFalse($hasException, 'Expects verification to pass without exceptions being thrown'); + $this->assertTrue($builder->verify()); } } diff --git a/example/tests/MessageProvider/ExampleMessageProviderTest.php b/example/tests/MessageProvider/ExampleMessageProviderTest.php deleted file mode 100644 index 21906049f..000000000 --- a/example/tests/MessageProvider/ExampleMessageProviderTest.php +++ /dev/null @@ -1,61 +0,0 @@ -text ='Hello Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $callbacks['footprints dressed in red'] = function () { - $content = new \stdClass(); - $content->song ='And the wind whispers Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setPublishResults(false); // Flag the verifier service to publish the results to the Pact Broker. - - // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../pacts/test_consumer-test_provider.json']); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Expects to reach true by running verification'); - } -} diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index f0b381a73..f9d54289c 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -7,8 +7,8 @@ use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\Runner\ProcessRunner; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; /** * This is an example on how you could use the included amphp/process wrapper to start your API to run PACT verification against a Provider. @@ -16,19 +16,20 @@ */ class PactVerifyTest extends TestCase { - /** @var ProcessRunner */ - private $processRunner; + private Process $process; /** * Run the PHP build-in web server. */ protected function setUp(): void { - $publicPath = __DIR__ . '/../../src/Provider/public/'; + $publicPath = __DIR__ . '/../../src/Provider/public'; - $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); - - $this->processRunner->run(); + $this->process = new Process(['php', '-S', 'localhost:7202', '-t', $publicPath, $publicPath . '/proxy.php']); + $this->process->start(); + $this->process->waitUntil(function ($type, $output) { + return false !== \strpos($output, 'Development Server (http://localhost:7202) started'); + }); } /** @@ -36,14 +37,14 @@ protected function setUp(): void */ protected function tearDown(): void { - $this->processRunner->stop(); + $this->process->stop(); } /** * This test will run after the web server is started. * - * @throws FileDownloadFailureException * @throws NoDownloaderFoundException + * @throws FileDownloadFailureException */ public function testPactVerifyConsumer() { @@ -51,14 +52,15 @@ public function testPactVerifyConsumer() $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBaseUrl(new Uri('http://localhost:7202')) // URL of the Provider. + ->setHost('localhost') + ->setPort(7202) + ->setStateChangeUrl(new Uri('http://localhost:7202/change-state')) + ->setDirs(__DIR__ . '/../../pacts/') ; // Flag the verifier service to publish the results to the Pact Broker. // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. $verifier = new Verifier($config); - $verifier->verifyFiles([__DIR__ . '/../../pacts/someconsumer-someprovider.json']); - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $this->assertTrue($verifier->verify()); } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..1ad856668 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + excludePaths: + - src/PhpPact/Consumer/InteractionBuilder.php + - src/PhpPact/Consumer/MessageBuilder.php + - src/PhpPact/Consumer/AbstractBuilder.php + - src/PhpPact/Standalone/ProviderVerifier/Verifier.php diff --git a/phpunit.xml b/phpunit.xml index 35d425c24..f6e0c073b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,14 +18,10 @@ - - - - diff --git a/src/PhpPact/Broker/Service/BrokerHttpClient.php b/src/PhpPact/Broker/Service/BrokerHttpClient.php deleted file mode 100644 index c26fe3e59..000000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClient.php +++ /dev/null @@ -1,112 +0,0 @@ -httpClient = $httpClient; - $this->baseUri = $baseUri; - $this->headers = $headers; - - if (!\array_key_exists('Content-Type', $headers)) { - $this->headers['Content-Type'] = 'application/json'; - } - } - - /** - * {@inheritdoc} - */ - public function publishJson(string $json, string $version) - { - $array = \json_decode($json, true); - $consumer = $array['consumer']['name']; - $provider = $array['provider']['name']; - - /** @var UriInterface $uri */ - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/consumer/{$consumer}/version/{$version}"); - - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - 'body' => $json, - ]); - } - - /** - * {@inheritdoc} - */ - public function tag(string $consumer, string $version, string $tag) - { - /** @var UriInterface $uri */ - $uri = $this->baseUri->withPath("/pacticipants/{$consumer}/versions/{$version}/tags/{$tag}"); - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - ]); - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrls(string $provider, string $version = 'latest'): array - { - if ($version !== 'latest') { - @\trigger_error(\sprintf('The second argument "version" in "%s()" method makes no sense and will be removed in any upcoming major version', __METHOD__), E_USER_DEPRECATED); - } - - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrlsForTag(string $provider, string $tag): array - { - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest/{$tag}"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } -} diff --git a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php b/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php deleted file mode 100644 index 05fb303ed..000000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php +++ /dev/null @@ -1,61 +0,0 @@ - 1, + '1.1.0' => 2, + '2.0.0' => 3, + '3.0.0' => 4, + '4.0.0' => 5, + ]; + const UNKNOWN_PACT_SPECIFICATION_VERSION = 0; + + protected FFI $ffi; + protected MockServerConfigInterface $config; + protected Scripts $scripts; + + /** + * @param MockServerConfigInterface $config + * + * @throws FileDownloadFailureException + * @throws NoDownloaderFoundException + */ + public function __construct(MockServerConfigInterface $config) + { + $this->config = $config; + $this->scripts = (new InstallManager())->install(); + $this->ffi = FFI::cdef(\file_get_contents($this->scripts->getCode()), $this->scripts->getLibrary()); + $this->ffi->pactffi_init('PACT_LOGLEVEL'); + } + + /** + * @return int + */ + protected function getPactSpecificationVersion(): int + { + return static::SUPPORTED_PACT_SPECIFICATION_VERSIONS[$this->config->getPactSpecificationVersion()] ?? static::UNKNOWN_PACT_SPECIFICATION_VERSION; + } +} diff --git a/src/PhpPact/Consumer/BuilderInterface.php b/src/PhpPact/Consumer/BuilderInterface.php index 0b58c219e..406acd261 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/Exception/MockServerNotStartedException.php b/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php new file mode 100644 index 000000000..d107876d2 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MockServerNotStartedException.php @@ -0,0 +1,17 @@ +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 93ec41d68..dc4acebad 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -2,37 +2,44 @@ namespace PhpPact\Consumer; +use FFI\CData; +use PhpPact\Consumer\Exception\MockServerNotStartedException; use PhpPact\Consumer\Model\ConsumerRequest; -use PhpPact\Consumer\Model\Interaction; 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 + * Build an interaction and send it to the Pact Rust FFI * Class InteractionBuilder. */ -class InteractionBuilder implements BuilderInterface +class InteractionBuilder extends AbstractBuilder { - /** @var MockServerHttpService */ - protected $mockServerHttpService; - - /** @var MockServerConfigInterface */ - protected $config; - /** @var Interaction */ - private $interaction; + protected CData $pact; + protected CData $interaction; + protected ?int $port = null; /** * InteractionBuilder constructor. * - * @param MockServerConfigInterface $config + * {@inheritdoc} */ public function __construct(MockServerConfigInterface $config) { - $this->config = $config; - $this->mockServerHttpService = new MockServerHttpService(new GuzzleClient(), $config); - $this->interaction = new Interaction(); + parent::__construct($config); + $this->pact = $this->ffi->pactffi_new_pact($config->getConsumer(), $config->getProvider()); + $this->ffi->pactffi_with_specification($this->pact, $this->getPactSpecificationVersion()); + } + + /** + * @param string $description what is received when the request is made + * + * @return InteractionBuilder + */ + public function newInteraction(string $description = ''): self + { + $this->interaction = $this->ffi->pactffi_new_interaction($this->pact, $description); + + return $this; } /** @@ -42,7 +49,7 @@ public function __construct(MockServerConfigInterface $config) */ public function given(string $providerState): self { - $this->interaction->setProviderState($providerState); + $this->ffi->pactffi_given($this->interaction, $providerState); return $this; } @@ -54,7 +61,7 @@ public function given(string $providerState): self */ public function uponReceiving(string $description): self { - $this->interaction->setDescription($description); + $this->ffi->pactffi_upon_receiving($this->interaction, $description); return $this; } @@ -66,55 +73,84 @@ public function uponReceiving(string $description): self */ public function with(ConsumerRequest $request): self { - $this->interaction->setRequest($request); + $this->ffi->pactffi_with_request($this->interaction, $request->getMethod(), $request->getPath()); + foreach ($request->getHeaders() as $header => $values) { + foreach ($values as $index => $value) { + $this->ffi->pactffi_with_header($this->interaction, $this->ffi->InteractionPart_Request, $header, $index, $value); + } + } + foreach ($request->getQuery() as $key => $values) { + $values = \is_array($values) ? $values : [$values]; + foreach ($values as $index => $value) { + $this->ffi->pactffi_with_query_parameter($this->interaction, $key, $index, $value); + } + } + if (!\is_null($request->getBody())) { + $this->ffi->pactffi_with_body($this->interaction, $this->ffi->InteractionPart_Request, null, $request->getBody()); + } return $this; } /** - * 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 + * @return InteractionBuilder */ - public function willRespondWith(ProviderResponse $response): bool + public function willRespondWith(ProviderResponse $response): self { - $this->interaction->setResponse($response); + $this->ffi->pactffi_response_status($this->interaction, $response->getStatus()); + foreach ($response->getHeaders() as $header => $values) { + foreach ($values as $index => $value) { + $this->ffi->pactffi_with_header($this->interaction, $this->ffi->InteractionPart_Response, $header, $index, $value); + } + } + if (!\is_null($response->getBody())) { + $this->ffi->pactffi_with_body($this->interaction, $this->ffi->InteractionPart_Response, null, $response->getBody()); + } - return $this->mockServerHttpService->registerInteraction($this->interaction); + return $this; } - /** - * {@inheritdoc} - */ - public function verify(): bool + public function createMockServer(): void { - return $this->mockServerHttpService->verifyInteractions(); + $this->port = $this->ffi->pactffi_create_mock_server_for_pact($this->pact, '127.0.0.1:0', false); } /** - * Writes the file to disk and deletes interactions from mock server. + * {@inheritdoc} + * + * @throws MockServerNotStartedException */ - public function finalize(): bool + public function verify(): bool { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); + if ($this->port === null) { + throw new MockServerNotStartedException('Mock server is not started.'); + } + + $result = $this->ffi->pactffi_mock_server_matched($this->port); - // Delete the interactions. - $this->mockServerHttpService->deleteAllInteractions(); + if ($result) { + $this->ffi->pactffi_write_pact_file($this->port, $this->config->getPactDir(), true); + } - return true; + $this->ffi->pactffi_cleanup_mock_server($this->port); + $this->port = null; + + return $result; } /** - * {@inheritdoc} + * @throws MockServerNotStartedException + * + * @return string */ - public function writePact(): bool + public function getBaseUri(): string { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); + if ($this->port === null) { + throw new MockServerNotStartedException('Mock server is not started.'); + } - return true; + return \sprintf('http://localhost:%d', $this->port); } } diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php index 27f75dd37..94d77e80b 100644 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ b/src/PhpPact/Consumer/Listener/PactTestListener.php @@ -2,13 +2,11 @@ namespace PhpPact\Consumer\Listener; +use Exception; 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,46 +21,25 @@ class PactTestListener implements TestListener { use TestListenerDefaultImplementation; - /** @var MockServer */ - private $server; - /** * Name of the test suite configured in your phpunit config. * * @var string[] */ - private $testSuiteNames; - - /** @var MockServerEnvConfig */ - private $mockServerConfig; + private array $testSuiteNames; - /** @var bool */ - private $failed; + private MockServerEnvConfig $builderConfig; + private bool $failed = false; /** * PactTestListener constructor. * * @param string[] $testSuiteNames test suite names that need evaluated with the listener - * - * @throws MissingEnvVariableException */ public function __construct(array $testSuiteNames) { - $this->testSuiteNames = $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(); - } + $this->testSuiteNames = $testSuiteNames; + $this->builderConfig = new MockServerEnvConfig(); } public function addError(Test $test, \Throwable $t, float $time): void @@ -79,19 +56,12 @@ public function addFailure(Test $test, AssertionFailedError $e, float $time): vo * Publish JSON results to PACT Broker and stop the Mock Server. * * @param TestSuite $suite + * + * @throws Exception */ 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 +71,26 @@ 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->builderConfig->getConsumer()); + $brokerConfig->setPactLocations($this->builderConfig->getPactDir()); + $brokerConfig->setBrokerUri(new Uri($pactBrokerUri)); + $brokerConfig->setConsumerVersion($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($json, $consumerVersion); + $broker = new Broker($brokerConfig); + $broker->createVersionTag(); + $broker->publish(); print 'Pact file has been uploaded to the Broker successfully.'; } } diff --git a/src/PhpPact/Consumer/MessageBuilder.php b/src/PhpPact/Consumer/MessageBuilder.php index dc195f3f8..159460a0d 100644 --- a/src/PhpPact/Consumer/MessageBuilder.php +++ b/src/PhpPact/Consumer/MessageBuilder.php @@ -2,36 +2,31 @@ namespace PhpPact\Consumer; -use PhpPact\Consumer\Model\Message; -use PhpPact\Standalone\PactConfigInterface; -use PhpPact\Standalone\PactMessage\PactMessage; +use Exception; +use FFI; +use FFI\CData; +use PhpPact\Standalone\MockService\MockServerConfigInterface; /** - * Build a message and send it to the Ruby Standalone Mock Service + * Build a message and send it to the Pact Rust FFI * Class MessageBuilder. */ -class MessageBuilder implements BuilderInterface +class MessageBuilder extends AbstractBuilder { - /** @var PactMessage */ - protected $pactMessage; - - /** @var PactConfigInterface */ - protected $config; - - /** @var array callable */ - protected $callback; - - /** @var Message */ - private $message; + protected array $callback; + protected CData $messagePact; + protected CData $message; /** - * @param PactConfigInterface $config + * MessageBuilder constructor. + * + * {@inheritdoc} */ - public function __construct(PactConfigInterface $config) + public function __construct(MockServerConfigInterface $config) { - $this->config = $config; - $this->message = new Message(); - $this->pactMessage = new PactMessage(); + parent::__construct($config); + $this->messagePact = $this->ffi->pactffi_new_message_pact($config->getConsumer(), $config->getProvider()); + $this->message = $this->ffi->pactffi_new_message($this->messagePact, ''); } /** @@ -54,15 +49,20 @@ public function setCallback(callable $callback, $description = false): self } /** - * @param string $name what is given to the request - * @param array $params for that request - * @param bool $overwrite clear pass states completely and start this array + * @param string $description what is given to the request + * @param array $params for that request * * @return MessageBuilder */ - public function given(string $name, array $params = [], $overwrite = false): self + public function given(string $description, array $params = []): self { - $this->message->setProviderState($name, $params, $overwrite); + if (\count($params) > 0) { + foreach ($params as $name => $value) { + $this->ffi->pactffi_message_given_with_param($this->message, $description, (string) $name, $value); + } + } else { + $this->ffi->pactffi_message_given($this->message, $description); + } return $this; } @@ -74,19 +74,21 @@ public function given(string $name, array $params = [], $overwrite = false): sel */ public function expectsToReceive(string $description): self { - $this->message->setDescription($description); + $this->ffi->pactffi_message_expects_to_receive($this->message, $description); return $this; } /** - * @param mixed $metadata what is the additional metadata of the message + * @param array $metadata what is the additional metadata of the message * * @return MessageBuilder */ - public function withMetadata($metadata): self + public function withMetadata(array $metadata): self { - $this->message->setMetadata($metadata); + foreach ($metadata as $key => $value) { + $this->ffi->pactffi_message_with_metadata($this->message, $key, $value); + } return $this; } @@ -100,7 +102,17 @@ public function withMetadata($metadata): self */ public function withContent($contents): self { - $this->message->setContents($contents); + if (\is_string($contents)) { + $contentType = 'text/plain'; + } else { + $contents = \json_encode($contents); + $contentType = 'application/json'; + } + $length = \strlen($contents); + $size = $length + 1; + $cData = $this->ffi->new("uint8_t[{$size}]"); + FFI::memcpy($cData, $contents, $length); + $this->ffi->pactffi_message_with_contents($this->message, $contentType, $cData, $size); return $this; } @@ -108,14 +120,11 @@ public function withContent($contents): self /** * Run reify to create an example pact from the message (i.e. create messages from matchers) * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - * * @return string */ public function reify(): string { - return $this->pactMessage->reify($this->message); + return $this->ffi->pactffi_message_reify($this->message); } /** @@ -124,7 +133,7 @@ public function reify(): string * @param callable $callback * @param false|string $description description of the pact and thus callback * - * @throws \Exception + * @throws Exception * * @return bool */ @@ -132,53 +141,35 @@ public function verifyMessage(callable $callback, $description = false): bool { $this->setCallback($callback, $description); - return $this->verify($description); + return $this->verify(); } /** * Verify the use of the pact by calling the callback * It also calls finalize to write the pact * - * @param false|string $description description of the pact and thus callback - * - * @throws \Exception if callback is not set + * @throws Exception if callback is not set * * @return bool */ - public function verify($description = false): bool + public function verify(): bool { if (\count($this->callback) < 1) { throw new \Exception('Callbacks need to exist to run verify.'); } - $pactJson = $this->reify(); + $contents = $this->reify(); // call the function to actually run the logic try { foreach ($this->callback as $callback) { //@todo .. what do with the providerState - \call_user_func($callback, $pactJson); + \call_user_func($callback, $contents); } - return $this->writePact(); + return !$this->ffi->pactffi_write_message_pact_file($this->messagePact, $this->config->getPactDir(), true); } catch (\Exception $e) { return false; } } - - /** - * Write the Pact without deleting the interactions. - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - * - * @return bool - */ - public function writePact(): bool - { - // you do not want to save the reified json - $pactJson = \json_encode($this->message); - - return $this->pactMessage->update($pactJson, $this->config->getConsumer(), $this->config->getProvider(), $this->config->getPactDir()); - } } diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 9fe9d2260..cde4af1a3 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -6,32 +6,13 @@ * Request initiated by the consumer. * Class ConsumerRequest. */ -class ConsumerRequest implements \JsonSerializable +class ConsumerRequest { - /** - * @var string - */ - private $method; - - /** - * @var string - */ - private $path; - - /** - * @var string[] - */ - private $headers; - - /** - * @var mixed - */ - private $body; - - /** - * @var string - */ - private $query; + private string $method; + private string $path; + private ?string $body = null; + private array $headers = []; + private array $query = []; /** * @return string @@ -56,7 +37,7 @@ public function setMethod(string $method): self /** * @return string */ - public function getPath() + public function getPath(): string { return $this->path; } @@ -74,42 +55,52 @@ public function setPath(string $path): self } /** - * @return string[] + * @return array */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } /** - * @param string[] $headers + * @param array $headers * * @return ConsumerRequest */ public function setHeaders(array $headers): self { - $this->headers = $headers; + $this->headers = []; + foreach ($headers as $header => $values) { + $this->addHeader($header, $values); + } return $this; } /** - * @param string $header - * @param array | string $value + * @param string $header + * @param array|string $values * * @return ConsumerRequest */ - public function addHeader(string $header, $value): self + public function addHeader(string $header, $values): self { - $this->headers[$header] = $value; + $this->headers[$header] = []; + if (\is_array($values)) { + foreach ($values as $value) { + $this->addHeaderValue($header, $value); + } + } else { + $this->addHeaderValue($header, $values); + } return $this; } /** - * @return mixed + * @return null|string */ - public function getBody() + public function getBody(): ?string { return $this->body; } @@ -119,17 +110,24 @@ public function getBody() * * @return ConsumerRequest */ - public function setBody($body) + public function setBody($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; } @@ -141,7 +139,9 @@ public function getQuery() */ public function setQuery(string $query): self { - $this->query = $query; + foreach (\explode('&', $query) as $parameter) { + $this->addQueryParameter(...\explode('=', $parameter)); + } return $this; } @@ -154,40 +154,17 @@ public function setQuery(string $query): self */ public function addQueryParameter(string $key, string $value): self { - if ($this->query === null) { - $this->query = "{$key}={$value}"; - } else { - $this->query = "{$this->query}&{$key}={$value}"; - } + $this->query[$key][] = $value; return $this; } /** - * {@inheritdoc} + * @param string $header + * @param string $value */ - public function jsonSerialize() + private function addHeaderValue(string $header, string $value): void { - $results = []; - - $results['method'] = $this->getMethod(); - - if ($this->getHeaders() !== null) { - $results['headers'] = $this->getHeaders(); - } - - if ($this->getPath() !== null) { - $results['path'] = $this->getPath(); - } - - if ($this->getBody() !== null) { - $results['body'] = $this->getBody(); - } - - if ($this->getQuery() !== null) { - $results['query'] = $this->getQuery(); - } - - return $results; + $this->headers[$header][] = $value; } } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php deleted file mode 100644 index 438e1057a..000000000 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ /dev/null @@ -1,131 +0,0 @@ -description; - } - - /** - * @param string $description - * - * @return Interaction - */ - public function setDescription(string $description): self - { - $this->description = $description; - - return $this; - } - - /** - * @return null|string - */ - public function getProviderState(): ?string - { - return $this->providerState; - } - - /** - * @param string $providerState - * - * @return Interaction - */ - public function setProviderState(string $providerState): self - { - $this->providerState = $providerState; - - return $this; - } - - /** - * @return ConsumerRequest - */ - public function getRequest(): ConsumerRequest - { - return $this->request; - } - - /** - * @param ConsumerRequest $request - * - * @return Interaction - */ - public function setRequest(ConsumerRequest $request): self - { - $this->request = $request; - - return $this; - } - - /** - * @return ProviderResponse - */ - public function getResponse(): ProviderResponse - { - return $this->response; - } - - /** - * @param ProviderResponse $response - * - * @return Interaction - */ - public function setResponse(ProviderResponse $response): self - { - $this->response = $response; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() - { - if ($this->getProviderState()) { - return [ - 'description' => $this->getDescription(), - 'providerState' => $this->getProviderState(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } - - return [ - 'description' => $this->getDescription(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } -} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php deleted file mode 100644 index 38ea15487..000000000 --- a/src/PhpPact/Consumer/Model/Message.php +++ /dev/null @@ -1,155 +0,0 @@ -description; - } - - /** - * @param string $description - * - * @return Message - */ - public function setDescription(string $description): self - { - $this->description = $description; - - return $this; - } - - /** - * @return array - */ - public function getProviderStates(): array - { - return $this->providerStates; - } - - /** - * @param mixed $overwrite - * - * @return array - */ - public function setProviderState(string $name, array $params = [], $overwrite = true): array - { - $this->addProviderState($name, $params, $overwrite); - - return $this->providerStates; - } - - /** - * @param string $name - * @param array $params - * @param bool $overwrite - if true reset the entire state - * - * @return Message - */ - public function addProviderState(string $name, array $params, $overwrite = false): self - { - $providerState = new \stdClass(); - $providerState->name = $name; - $providerState->params = $params; - - if ($overwrite === true) { - $this->providerStates = []; - } - - $this->providerStates[] = $providerState; - - return $this; - } - - /** - * @return array - */ - public function getMetadata(): array - { - return $this->metadata; - } - - /** - * @param array $metadata - * - * @return Message - */ - public function setMetadata(array $metadata): self - { - $this->metadata = $metadata; - - return $this; - } - - /** - * @return mixed - */ - public function getContents() - { - return $this->contents; - } - - /** - * @param mixed $contents - * - * @return Message - */ - public function setContents($contents) - { - $this->contents = $contents; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() - { - $out = []; - $out['description'] = $this->getDescription(); - - if (\count($this->providerStates) > 0) { - $out['providerStates'] = $this->getProviderStates(); - } - - if ($this->metadata) { - $out['metadata'] = $this->getMetadata(); - } - - if ($this->contents) { - $out['contents'] = $this->getContents(); - } - - return $out; - } -} diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index dd22f8805..dfb977541 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -6,22 +6,11 @@ * Response expectation that would be in response to a Consumer request from the Provider. * Class ProviderResponse. */ -class ProviderResponse implements \JsonSerializable +class ProviderResponse { - /** - * @var int - */ - private $status; - - /** - * @var null|string[] - */ - private $headers; - - /** - * @var null|array - */ - private $body; + private int $status; + private ?string $body = null; + private array $headers = []; /** * @return int @@ -44,75 +33,81 @@ public function setStatus(int $status): self } /** - * @return null|string[] + * @return array */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } /** - * @param string[] $headers + * @param array $headers * * @return ProviderResponse */ public function setHeaders(array $headers): self { - $this->headers = $headers; + $this->headers = []; + foreach ($headers as $header => $values) { + $this->addHeader($header, $values); + } return $this; } /** - * @param string $header - * @param array | string $value + * @param string $header + * @param array|string $values * * @return ProviderResponse */ - public function addHeader(string $header, $value): self + public function addHeader(string $header, $values): self { - $this->headers[$header] = $value; + $this->headers[$header] = []; + if (\is_array($values)) { + foreach ($values as $value) { + $this->addHeaderValue($header, $value); + } + } else { + $this->addHeaderValue($header, $values); + } 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 { - $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; } /** - * {@inheritdoc} + * @param string $header + * @param string $value */ - public function jsonSerialize() + private function addHeaderValue(string $header, string $value): void { - $results = [ - 'status' => $this->getStatus(), - ]; - - if ($this->getHeaders() !== null) { - $results['headers'] = $this->getHeaders(); - } - - if ($this->getBody() !== null) { - $results['body'] = $this->getBody(); - } - - return $results; + $this->headers[$header][] = $value; } } diff --git a/src/PhpPact/Provider/MessageVerifier.php b/src/PhpPact/Provider/MessageVerifier.php deleted file mode 100644 index 751d83e60..000000000 --- a/src/PhpPact/Provider/MessageVerifier.php +++ /dev/null @@ -1,245 +0,0 @@ -callbacks = []; - - $baseUrl = @$this->config->getProviderBaseUrl(); - if (!$baseUrl) { - $config->setProviderBaseUrl(new Uri("http://{$this->defaultProxyHost}:{$this->defaultProxyPort}")); - } - - // default verification delay - $this->setVerificationDelaySec(\floor($config->getProcessIdleTimeout() / $this->defaultDelayFactor)); - } - - /** - * @param array $callbacks - * - * @return self - */ - public function setCallbacks(array $callbacks): self - { - $this->callbacks = $callbacks; - - return $this; - } - - /** - * Add an individual call back - * - * @param string $key - * @param callable $callback - * - * @throws \Exception - * - * @return MessageVerifier - */ - public function addCallback(string $key, callable $callback): self - { - if (!isset($this->callbacks[$key])) { - $this->callbacks[$key] = $callback; - } else { - throw new \Exception("Callback with key ($key) already exists"); - } - - return $this; - } - - /** - * @param float $verificationDelaySec - * - * @return MessageVerifier - */ - public function setVerificationDelaySec(float $verificationDelaySec): self - { - $this->verificationDelaySec = $verificationDelaySec; - - return $this; - } - - /** - * @param Logger $logger - * - * @return MessageVerifier - */ - public function setLogger(Logger $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * @param array $arguments - * - * @throws \Exception - */ - protected function verifyAction(array $arguments) - { - if (\count($this->callbacks) < 1) { - throw new \Exception('Callback needs to bet set when using message pacts'); - } - - $callbacks = $this->callbacks; - $uri = $this->config->getProviderBaseUrl(); - - $scripts = $this->installManager->install(); - $arguments = \array_merge([$scripts->getProviderVerifier()], $arguments); - - /** - * @throws \Amp\Socket\SocketException - * @throws \Error - * @throws \TypeError - * - * @return \Generator - */ - $lambdaLoop = function () use ($callbacks, $arguments, $uri) { - // spin up a server - $url = "{$uri->getHost()}:{$uri->getPort()}"; - $servers = [ - Socket\Server::listen($url) - ]; - - $logger = $this->getLogger(); - - $server = new Server($servers, new CallableRequestHandler(function (Request $request) use ($callbacks) { - if (\count($callbacks) === 1) { - $callback = \array_pop($callbacks); - } else { - $payload = new Payload($request->getBody()); - $requestBody = yield $payload->buffer(); - $requestBody = \json_decode($requestBody); - $description = $requestBody->description; - - $callback = false; - - if (isset($this->callbacks[$description])) { - $callback = $this->callbacks[$description]; - } - - if ($callback === false) { - throw new \Exception("Pacts with multiple states need to have callbacks key'ed by the description"); - } - } - - //@todo pass $providerStates to the call back - $out = \call_user_func($callback); - - // return response should only happen if the \call_user_fun() - return new Response(Status::OK, [ - 'content-type' => 'application/json;', - ], $out); - }), $logger); - - yield $server->start(); - - // delay long enough for the server to be stood up - $delay = (int) ($this->verificationDelaySec * 1000); - - // call the provider-verification cmd - Loop::delay($delay, function () use ($arguments) { - $cmd = \implode(' ', $arguments); - $process = new Process($cmd); - yield $process->start(); - - $payload = new Payload($process->getStdout()); - print yield $payload->buffer(); - - $code = yield $process->join(); - - // if the provider verification cmd returns a non-zero number, the test failed - if ($code !== 0) { - $this->getLogger()->warning(yield $process->getStderr()->read()); - - throw new \Exception("Pact failed to validate. Exit code: {$code}"); - } - - Loop::stop(); - }); - }; - - Loop::run($lambdaLoop); - } - - /** - * @return Logger - */ - private function getLogger() - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('server'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/Broker/Broker.php b/src/PhpPact/Standalone/Broker/Broker.php index d8c7243fc..f06525fa0 100644 --- a/src/PhpPact/Standalone/Broker/Broker.php +++ b/src/PhpPact/Standalone/Broker/Broker.php @@ -6,18 +6,28 @@ use Amp\Log\ConsoleFormatter; use Amp\Log\StreamHandler; use Monolog\Logger; +use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; +use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; use PhpPact\Standalone\Installer\InstallManager; use PhpPact\Standalone\Runner\ProcessRunner; +/** + * Class Broker. + */ class Broker { - /** @var Logger */ - private $logger; - /** @var BrokerConfig */ - private $config; - /** @var string */ - private $command; + private Logger $logger; + private BrokerConfig $config; + private string $command; + /** + * Broker constructor. + * + * @param BrokerConfig $config + * + * @throws FileDownloadFailureException + * @throws NoDownloaderFoundException + */ public function __construct(BrokerConfig $config) { $this->config = $config; diff --git a/src/PhpPact/Standalone/Broker/BrokerConfig.php b/src/PhpPact/Standalone/Broker/BrokerConfig.php index 3b7e29da9..228348b0d 100644 --- a/src/PhpPact/Standalone/Broker/BrokerConfig.php +++ b/src/PhpPact/Standalone/Broker/BrokerConfig.php @@ -4,56 +4,32 @@ use Psr\Http\Message\UriInterface; +/** + * Class BrokerConfig. + */ class BrokerConfig { - /** @var null|UriInterface */ - private $brokerUri; - - /** @var null|string */ - private $brokerToken; - - /** @var null|string */ - private $brokerUsername; - - /** @var null|string */ - private $brokerPassword; - - /** @var bool */ - private $verbose = false; - - /** @var null|string */ - private $pacticipant; - - /** @var null|string */ - private $request; - /** @var null|string */ - private $header; - /** @var null|string */ - private $data; - /** @var null|string */ - private $user; - /** @var null|string */ - private $consumer; - /** @var null|string */ - private $provider; - /** @var null|string */ - private $description; - /** @var null|string */ - private $uuid; - /** @var null|string */ - private $version; - /** @var null|string */ - private $tag; - /** @var null|string */ - private $name; - /** @var null|string */ - private $repositoryUrl; - /** @var null|string */ - private $url; - /** @var null|string */ - private $consumerVersion; - /** @var null|string */ - private $pactLocations; + private ?UriInterface $brokerUri = null; + private ?string $brokerToken = null; + private ?string $brokerUsername = null; + private ?string $brokerPassword = null; + private bool $verbose = false; + private ?string $pacticipant = null; + private ?string $request = null; + private ?string $header = null; + private ?string $data = null; + private ?string $user = null; + private ?string $consumer = null; + private ?string $provider = null; + private ?string $description = null; + private ?string $uuid = null; + private ?string $version = null; + private ?string $tag = null; + private ?string $name = null; + private ?string $repositoryUrl = null; + private ?string $url = null; + private ?string $consumerVersion = null; + private ?string $pactLocations = null; /** * @return null|string @@ -315,7 +291,10 @@ public function setUuid(?string $uuid): self return $this; } - public function isVerbose() + /** + * @return bool + */ + public function isVerbose(): bool { return $this->verbose; } @@ -330,6 +309,8 @@ public function getBrokerUri(): ?UriInterface /** * @param null|UriInterface $brokerUri + * + * @return BrokerConfig */ public function setBrokerUri(?UriInterface $brokerUri): self { @@ -348,6 +329,8 @@ public function getBrokerToken(): ?string /** * @param null|string $brokerToken + * + * @return BrokerConfig */ public function setBrokerToken(?string $brokerToken): self { @@ -366,6 +349,8 @@ public function getBrokerUsername(): ?string /** * @param null|string $brokerUsername + * + * @return BrokerConfig */ public function setBrokerUsername(?string $brokerUsername): self { @@ -384,6 +369,8 @@ public function getBrokerPassword(): ?string /** * @param null|string $brokerPassword + * + * @return BrokerConfig */ public function setBrokerPassword(?string $brokerPassword): self { @@ -392,13 +379,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 BrokerConfig */ public function setPacticipant(?string $pacticipant): self { diff --git a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php b/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php deleted file mode 100644 index 665ad6845..000000000 --- a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php +++ /dev/null @@ -1,17 +0,0 @@ -remove(self::$destinationDir); } } diff --git a/src/PhpPact/Standalone/Installer/Model/Scripts.php b/src/PhpPact/Standalone/Installer/Model/Scripts.php index aae076b36..8724dbc20 100644 --- a/src/PhpPact/Standalone/Installer/Model/Scripts.php +++ b/src/PhpPact/Standalone/Installer/Model/Scripts.php @@ -3,87 +3,64 @@ namespace PhpPact\Standalone\Installer\Model; /** - * Represents locations of Ruby Standalone full path and scripts. - * Class BinaryScripts. + * Class Scripts. */ class Scripts { /** - * Path to PhpPact Mock Service. - * - * @var string + * Path to Pact FFI C Header Code. */ - private $mockService; + private string $code; /** - * Path to the PhpPact Stub Service. - * - * @var string + * Path to Pact FFI Dynamic Library. */ - private $stubService; + private string $library; /** - * Path to the PhpPact Pact Message. + * Path to the PhpPact Stub Service. * * @var string */ - private $pactMessage; + private string $stubService; /** - * Path to the PhpPact Provider Verifier. + * Path to the Ruby Standalone Broker. * * @var string */ - private $providerVerifier; - - /** - * @var string - */ - private $broker; + private string $broker; - public function __construct(string $mockService, string $stubService, string $providerVerifier, string $pactMessage, string $broker) + public function __construct(string $code, string $library, string $stubService, string $broker) { - $this->mockService = $mockService; - $this->stubService = $stubService; - $this->providerVerifier = $providerVerifier; - $this->pactMessage = $pactMessage; - $this->broker = $broker; + $this->code = $code; + $this->library = $library; + $this->stubService = $stubService; + $this->broker = $broker; } /** * @return string */ - public function getMockService(): string + public function getCode(): string { - return $this->mockService; + return $this->code; } /** * @return string */ - public function getStubService(): string + public function getLibrary(): string { - return $this->stubService; + return $this->library; } /** * @return string */ - public function getProviderVerifier(): string - { - return $this->providerVerifier; - } - - /** - * @param string $providerVerifier - * - * @return Scripts - */ - public function setProviderVerifier(string $providerVerifier): self + public function getStubService(): string { - $this->providerVerifier = $providerVerifier; - - return $this; + return $this->stubService; } /** @@ -93,12 +70,4 @@ public function getBroker(): string { return $this->broker; } - - /** - * @return string - */ - public function getPactMessage(): string - { - return $this->pactMessage; - } } diff --git a/src/PhpPact/Standalone/Installer/Service/AbstractInstaller.php b/src/PhpPact/Standalone/Installer/Service/AbstractInstaller.php new file mode 100644 index 000000000..d1ab0ec67 --- /dev/null +++ b/src/PhpPact/Standalone/Installer/Service/AbstractInstaller.php @@ -0,0 +1,176 @@ + 'pact-reference', + 'filename' => 'pact.h', + 'version' => self::PACT_FFI_VERSION, + 'versionPrefix' => 'libpact_ffi-v', + 'extract' => false, + ], + ]; + + /** + * {@inheritdoc} + */ + public function install(string $destinationDir): Scripts + { + if (\file_exists(($destinationDir)) === false) { + \mkdir($destinationDir); + + foreach (static::FILES as $file) { + $uri = \sprintf( + 'https://github.com/pact-foundation/%s/releases/download/%s%s/%s', + $file['repo'], + $file['versionPrefix'], + $file['version'], + $file['filename'] + ); + $tempFilePath = $destinationDir . DIRECTORY_SEPARATOR . $file['filename']; + + $this->download($uri, $tempFilePath); + if ($file['extract']) { + $this + ->extract($tempFilePath, $destinationDir, $file['extractTo'] ?? null, $file['executable'] ?? false) + ->deleteCompressed($tempFilePath); + } + } + } + + return $this->getScripts($destinationDir); + } + + /** + * @param string $destinationDir + * + * @return Scripts + */ + abstract protected function getScripts(string $destinationDir): Scripts; + + /** + * Download the binaries. + * + * @param string $uri uri of the file to be downloaded + * @param string $tempFilePath location to download the file + * + * @throws FileDownloadFailureException + * + * @return AbstractInstaller + */ + private function download(string $uri, string $tempFilePath): self + { + $data = \file_get_contents($uri); + + if ($data === false) { + throw new FileDownloadFailureException('Failed to download binary from Github for Ruby Standalone!'); + } + + $result = \file_put_contents($tempFilePath, $data); + + if ($result === false) { + throw new FileDownloadFailureException('Failed to save binaries for Ruby Standalone!'); + } + + return $this; + } + + /** + * Uncompress the temp file and install the binaries in the destination directory. + * + * @param string $sourceFile + * @param string $destinationDir + * @param null|string $fileName Required for gz file + * @param bool $executable Required for gz file + * + * @return AbstractInstaller + */ + private function extract(string $sourceFile, string $destinationDir, ?string $fileName, bool $executable): self + { + if (\substr($sourceFile, -7) === '.tar.gz') { + $this->extractTarGz($sourceFile, $destinationDir); + } elseif (\substr($sourceFile, -4) === '.zip') { + $this->extractZip($sourceFile, $destinationDir); + } else { + $this->extractGz($sourceFile, $fileName, $executable); + } + + return $this; + } + + /** + * @param string $sourceFile + * @param string $destinationDir + */ + private function extractTarGz(string $sourceFile, string $destinationDir): void + { + $p = new \PharData($sourceFile); + $p->extractTo($destinationDir); + } + + /** + * @param string $sourceFile + * @param string $destinationDir + */ + private function extractZip(string $sourceFile, string $destinationDir): void + { + $zip = new ZipArchive(); + + if ($zip->open($sourceFile)) { + $zip->extractTo($destinationDir); + $zip->close(); + } + } + + /** + * https://gist.github.com/bastiankoetsier/99827fa4754207d860ac + * + * @param string $sourceFile + * @param string $fileName + * @param bool $executable + */ + private function extractGz(string $sourceFile, string $fileName, bool $executable): void + { + $bufferSize = 4096; + $destFile = \pathinfo($sourceFile, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . $fileName; + $file = \gzopen($sourceFile, 'rb'); + $outFile = \fopen($destFile, 'wb'); + while (!\gzeof($file)) { + \fwrite($outFile, \gzread($file, $bufferSize)); + } + \fclose($outFile); + \gzclose($file); + if ($executable) { + // TODO How to change permission to executable in Windows + \chmod($destFile, 0744); + } + } + + /** + * Delete the temp file. + * + * @param string $filePath + * + * @return AbstractInstaller + */ + private function deleteCompressed(string $filePath): self + { + \unlink($filePath); + + return $this; + } +} diff --git a/src/PhpPact/Standalone/Installer/Service/InstallerLinux.php b/src/PhpPact/Standalone/Installer/Service/InstallerLinux.php index 61e902fea..0aad2ade9 100644 --- a/src/PhpPact/Standalone/Installer/Service/InstallerLinux.php +++ b/src/PhpPact/Standalone/Installer/Service/InstallerLinux.php @@ -2,12 +2,40 @@ namespace PhpPact\Standalone\Installer\Service; -use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; use PhpPact\Standalone\Installer\Model\Scripts; -class InstallerLinux implements InstallerInterface +/** + * Class InstallerLinux. + */ +class InstallerLinux extends AbstractInstaller { - const VERSION = '1.88.24'; + const FILES = [ + [ + 'repo' => 'pact-ruby-standalone', + 'filename' => 'pact-' . self::PACT_RUBY_STANDALONE_VERSION . '-linux-x86_64.tar.gz', + 'version' => self::PACT_RUBY_STANDALONE_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + ], + [ + 'repo' => 'pact-stub-server', + 'filename' => 'pact-stub-server-linux-x86_64.gz', + 'version' => self::PACT_STUB_SERVER_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + 'extractTo' => 'pact-stub-server', + 'executable' => true, + ], + [ + 'repo' => 'pact-reference', + 'filename' => 'libpact_ffi-linux-x86_64.so.gz', + 'version' => self::PACT_FFI_VERSION, + 'versionPrefix' => 'libpact_ffi-v', + 'extract' => true, + 'extractTo' => 'libpact_ffi.so', + ], + ...parent::FILES, + ]; /** * {@inheritdoc} @@ -20,85 +48,16 @@ public function isEligible(): bool /** * {@inheritdoc} */ - public function install(string $destinationDir): Scripts + protected function getScripts(string $destinationDir): Scripts { - if (\file_exists($destinationDir . DIRECTORY_SEPARATOR . 'pact') === false) { - $fileName = 'pact-' . self::VERSION . '-linux-x86_64.tar.gz'; - $tempFilePath = \sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName; - - $this - ->download($fileName, $tempFilePath) - ->extract($tempFilePath, $destinationDir) - ->deleteCompressed($tempFilePath); - } - - $scripts = new Scripts( - "{$destinationDir}/pact/bin/pact-mock-service", - "{$destinationDir}/pact/bin/pact-stub-service", - "{$destinationDir}/pact/bin/pact-provider-verifier", - "{$destinationDir}/pact/bin/pact-message", - "{$destinationDir}/pact/bin/pact-broker" + $destinationDir = $destinationDir . DIRECTORY_SEPARATOR; + $binDir = $destinationDir . 'pact' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR; + + return new Scripts( + $destinationDir . 'pact.h', + $destinationDir . 'libpact_ffi.so', + $destinationDir . 'pact-stub-server', + $binDir . 'pact-broker' ); - - return $scripts; - } - - /** - * Download the binaries. - * - * @param string $fileName name of the file to be downloaded - * @param string $tempFilePath location to download the file - * - * @throws FileDownloadFailureException - * - * @return InstallerLinux - */ - private function download(string $fileName, string $tempFilePath): self - { - $uri = 'https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v' . self::VERSION . "/{$fileName}"; - - $data = \file_get_contents($uri); - - if ($data === false) { - throw new FileDownloadFailureException('Failed to download binary from Github for Ruby Standalone!'); - } - - $result = \file_put_contents($tempFilePath, $data); - - if ($result === false) { - throw new FileDownloadFailureException('Failed to save binaries for Ruby Standalone!'); - } - - return $this; - } - - /** - * Uncompress the temp file and install the binaries in the destination directory. - * - * @param string $sourceFile - * @param string $destinationDir - * - * @return InstallerLinux - */ - private function extract(string $sourceFile, string $destinationDir): self - { - $p = new \PharData($sourceFile); - $p->extractTo($destinationDir); - - return $this; - } - - /** - * Delete the temp file. - * - * @param string $filePath - * - * @return InstallerLinux - */ - private function deleteCompressed(string $filePath): self - { - \unlink($filePath); - - return $this; } } diff --git a/src/PhpPact/Standalone/Installer/Service/InstallerMac.php b/src/PhpPact/Standalone/Installer/Service/InstallerMac.php index 35312b564..55e17e2ee 100644 --- a/src/PhpPact/Standalone/Installer/Service/InstallerMac.php +++ b/src/PhpPact/Standalone/Installer/Service/InstallerMac.php @@ -2,12 +2,40 @@ namespace PhpPact\Standalone\Installer\Service; -use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; use PhpPact\Standalone\Installer\Model\Scripts; -class InstallerMac implements InstallerInterface +/** + * Class InstallerMac. + */ +class InstallerMac extends AbstractInstaller { - const VERSION = '1.88.24'; + const FILES = [ + [ + 'repo' => 'pact-ruby-standalone', + 'filename' => 'pact-' . self::PACT_RUBY_STANDALONE_VERSION . '-osx.tar.gz', + 'version' => self::PACT_RUBY_STANDALONE_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + ], + [ + 'repo' => 'pact-stub-server', + 'filename' => 'pact-stub-server-osx-x86_64.gz', + 'version' => self::PACT_STUB_SERVER_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + 'extractTo' => 'pact-stub-server', + 'executable' => true, + ], + [ + 'repo' => 'pact-reference', + 'filename' => 'libpact_ffi-osx-x86_64.dylib.gz', + 'version' => self::PACT_FFI_VERSION, + 'versionPrefix' => 'libpact_ffi-v', + 'extract' => true, + 'extractTo' => 'libpact_ffi.dylib', + ], + ...parent::FILES, + ]; /** * {@inheritdoc} @@ -20,85 +48,16 @@ public function isEligible(): bool /** * {@inheritdoc} */ - public function install(string $destinationDir): Scripts + protected function getScripts(string $destinationDir): Scripts { - if (\file_exists($destinationDir . DIRECTORY_SEPARATOR . 'pact') === false) { - $fileName = 'pact-' . self::VERSION . '-osx.tar.gz'; - $tempFilePath = \sys_get_temp_dir() . DIRECTORY_SEPARATOR . $fileName; - - $this - ->download($fileName, $tempFilePath) - ->extract($tempFilePath, $destinationDir) - ->deleteCompressed($tempFilePath); - } - - $scripts = new Scripts( - "{$destinationDir}/pact/bin/pact-mock-service", - "{$destinationDir}/pact/bin/pact-stub-service", - "{$destinationDir}/pact/bin/pact-provider-verifier", - "{$destinationDir}/pact/bin/pact-message", - "{$destinationDir}/pact/bin/pact-broker" + $destinationDir = $destinationDir . DIRECTORY_SEPARATOR; + $binDir = $destinationDir . 'pact' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR; + + return new Scripts( + $destinationDir . 'pact.h', + $destinationDir . 'libpact_ffi.dylib', + $destinationDir . 'pact-stub-server', + $binDir . 'pact-broker' ); - - return $scripts; - } - - /** - * Download the binaries. - * - * @param string $fileName name of the file to be downloaded - * @param string $tempFilePath location to download the file - * - * @throws FileDownloadFailureException - * - * @return InstallerMac - */ - private function download(string $fileName, string $tempFilePath): self - { - $uri = 'https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v' . self::VERSION . "/{$fileName}"; - - $data = \file_get_contents($uri); - - if ($data === false) { - throw new FileDownloadFailureException('Failed to download binary from Github for Ruby Standalone!'); - } - - $result = \file_put_contents($tempFilePath, $data); - - if ($result === false) { - throw new FileDownloadFailureException('Failed to save binaries for Ruby Standalone!'); - } - - return $this; - } - - /** - * Uncompress the temp file and install the binaries in the destination directory. - * - * @param string $sourceFile - * @param string $destinationDir - * - * @return InstallerMac - */ - private function extract(string $sourceFile, string $destinationDir): self - { - $p = new \PharData($sourceFile); - $p->extractTo($destinationDir); - - return $this; - } - - /** - * Delete the temp file. - * - * @param string $filePath - * - * @return InstallerMac - */ - private function deleteCompressed(string $filePath): self - { - \unlink($filePath); - - return $this; } } diff --git a/src/PhpPact/Standalone/Installer/Service/InstallerWindows.php b/src/PhpPact/Standalone/Installer/Service/InstallerWindows.php index e2374c9e3..b467a41a5 100644 --- a/src/PhpPact/Standalone/Installer/Service/InstallerWindows.php +++ b/src/PhpPact/Standalone/Installer/Service/InstallerWindows.php @@ -2,17 +2,40 @@ namespace PhpPact\Standalone\Installer\Service; -use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; use PhpPact\Standalone\Installer\Model\Scripts; -use ZipArchive; /** - * Download the Ruby Standalone binaries for Windows. - * Class BinaryDownloaderWindows. + * Class InstallerWindows. */ -class InstallerWindows implements InstallerInterface +class InstallerWindows extends AbstractInstaller { - const VERSION = '1.88.24'; + const FILES = [ + [ + 'repo' => 'pact-ruby-standalone', + 'filename' => 'pact-' . self::PACT_RUBY_STANDALONE_VERSION . '-win32.zip', + 'version' => self::PACT_RUBY_STANDALONE_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + ], + [ + 'repo' => 'pact-stub-server', + 'filename' => 'pact-stub-server-windows-x86_64.exe.gz', + 'version' => self::PACT_STUB_SERVER_VERSION, + 'versionPrefix' => 'v', + 'extract' => true, + 'extractTo' => 'pact-stub-server.exe', + 'executable' => true, + ], + [ + 'repo' => 'pact-reference', + 'filename' => 'pact_ffi-windows-x86_64.dll.gz', + 'version' => self::PACT_FFI_VERSION, + 'versionPrefix' => 'libpact_ffi-v', + 'extract' => true, + 'extractTo' => 'pact_ffi.dll', + ], + ...parent::FILES, + ]; /** * {@inheritdoc} @@ -25,90 +48,16 @@ public function isEligible(): bool /** * {@inheritdoc} */ - public function install(string $destinationDir): Scripts + protected function getScripts(string $destinationDir): Scripts { - if (\file_exists($destinationDir . DIRECTORY_SEPARATOR . 'pact') === false) { - $fileName = 'pact-' . self::VERSION . '-win32.zip'; - $tempFilePath = __DIR__ . DIRECTORY_SEPARATOR . $fileName; + $destinationDir = $destinationDir . DIRECTORY_SEPARATOR; + $binDir = $destinationDir . 'pact' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR; - $this - ->download($fileName, $tempFilePath) - ->extract($tempFilePath, $destinationDir) - ->deleteCompressed($tempFilePath); - } - - $binDir = $destinationDir . DIRECTORY_SEPARATOR . 'pact' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR; - $scripts = new Scripts( - $binDir . 'pact-mock-service.bat', - $binDir . 'pact-stub-service.bat', - $binDir . 'pact-provider-verifier.bat', - $binDir . 'pact-message.bat', + return new Scripts( + $destinationDir . 'pact.h', + $destinationDir . 'pact_ffi.dll', + $destinationDir . 'pact-stub-server.exe', $binDir . 'pact-broker.bat' ); - - return $scripts; - } - - /** - * Download the binaries. - * - * @param string $fileName name of the file to be downloaded - * @param string $tempFilePath location to download the file - * - * @throws FileDownloadFailureException - * - * @return InstallerWindows - */ - private function download(string $fileName, string $tempFilePath): self - { - $uri = 'https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v' . self::VERSION . "/{$fileName}"; - - $data = \file_get_contents($uri); - - if ($data === false) { - throw new FileDownloadFailureException('Failed to download binary from Github for Ruby Standalone!'); - } - - $result = \file_put_contents($tempFilePath, $data); - - if ($result === false) { - throw new FileDownloadFailureException('Failed to save binaries for Ruby Standalone!'); - } - - return $this; - } - - /** - * Uncompress the temp file and install the binaries in the destination directory. - * - * @param string $sourceFile - * @param string $destinationDir - * - * @return InstallerWindows - */ - private function extract(string $sourceFile, string $destinationDir): self - { - $zip = new ZipArchive(); - - if ($zip->open($sourceFile)) { - $zip->extractTo($destinationDir); - $zip->close(); - } - - return $this; - } - - /** - * Delete the temp file. - * - * @param string $filePath - * - * @return InstallerWindows - */ - private function deleteCompressed(string $filePath): self - { - \unlink($filePath); - - return $this; } } diff --git a/src/PhpPact/Standalone/MockService/MockServer.php b/src/PhpPact/Standalone/MockService/MockServer.php deleted file mode 100644 index 39e0e33db..000000000 --- a/src/PhpPact/Standalone/MockService/MockServer.php +++ /dev/null @@ -1,170 +0,0 @@ -config = $config; - $this->installManager = new InstallManager(); - - 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 - { - $scripts = $this->installManager->install(); - - $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(); - } - - /** - * Wrapper to add a custom installer. - * - * @param InstallerInterface $installer - * - * @return self - */ - public function registerInstaller(InstallerInterface $installer): self - { - $this->installManager->registerInstaller($installer); - - return $this; - } - - /** - * 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 39483665d..b0f49a2c7 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfig.php @@ -2,161 +2,37 @@ namespace PhpPact\Standalone\MockService; -use Composer\Semver\VersionParser; -use GuzzleHttp\Psr7\Uri; -use PhpPact\Standalone\PactConfigInterface; -use Psr\Http\Message\UriInterface; - /** - * Configuration defining the default PhpPact Ruby Standalone server. - * Class MockServerConfig. + * Class ConsumerConfig. */ -class MockServerConfig implements MockServerConfigInterface, PactConfigInterface +class MockServerConfig implements MockServerConfigInterface { - /** - * Host on which to bind the service. - * - * @var string - */ - private $host = 'localhost'; - - /** - * Port on which to run the service. - * - * @var int - */ - private $port = 7200; - - /** - * @var bool - */ - private $secure = false; - /** * Consumer name. - * - * @var string */ - private $consumer; + private string $consumer; /** * Provider name. - * - * @var string */ - private $provider; + private string $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; + private string $pactDir; /** - * The max allowed attempts the mock server has to be available in. Otherwise it is considered as sick. - * - * @var int + * The pact specification version to use when writing the pact. Note that versions 1 is not supported. */ - private $healthCheckTimeout; + private string $pactSpecificationVersion; /** - * The seconds between health checks of mock server - * - * @var int + * BuilderConfig constructor. */ - private $healthCheckRetrySec; - private $logLevel; - - /** - * {@inheritdoc} - */ - public function getHost(): string + public function __construct() { - return $this->host; - } - - /** - * {@inheritdoc} - */ - public function setHost(string $host): MockServerConfigInterface - { - $this->host = $host; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getPort(): int - { - return $this->port; - } - - /** - * {@inheritdoc} - */ - public function setPort(int $port): MockServerConfigInterface - { - $this->port = $port; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function isSecure(): bool - { - return $this->secure; - } - - /** - * {@inheritdoc} - */ - public function setSecure(bool $secure): MockServerConfigInterface - { - $this->secure = $secure; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getBaseUri(): UriInterface - { - $protocol = $this->secure ? 'https' : 'http'; - - return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); + $this->pactDir = \sys_get_temp_dir(); } /** @@ -170,7 +46,7 @@ public function getConsumer(): string /** * {@inheritdoc} */ - public function setConsumer(string $consumer): PactConfigInterface + public function setConsumer(string $consumer): MockServerConfigInterface { $this->consumer = $consumer; @@ -188,7 +64,7 @@ public function getProvider(): string /** * {@inheritdoc} */ - public function setProvider(string $provider): PactConfigInterface + public function setProvider(string $provider): MockServerConfigInterface { $this->provider = $provider; @@ -198,19 +74,15 @@ public function setProvider(string $provider): PactConfigInterface /** * {@inheritdoc} */ - public function getPactDir() + public function getPactDir(): string { - if ($this->pactDir === null) { - return \sys_get_temp_dir(); - } - return $this->pactDir; } /** * {@inheritdoc} */ - public function setPactDir($pactDir): PactConfigInterface + public function setPactDir(string $pactDir): MockServerConfigInterface { if ('\\' !== \DIRECTORY_SEPARATOR) { $pactDir = \str_replace('\\', \DIRECTORY_SEPARATOR, $pactDir); @@ -224,33 +96,7 @@ public function setPactDir($pactDir): PactConfigInterface /** * {@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() + public function getPactSpecificationVersion(): string { return $this->pactSpecificationVersion; } @@ -258,110 +104,10 @@ public function getPactSpecificationVersion() /** * {@inheritdoc} */ - public function setPactSpecificationVersion($pactSpecificationVersion): PactConfigInterface + public function setPactSpecificationVersion(string $pactSpecificationVersion): MockServerConfigInterface { - /* - * 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 a1066ab3d..e0c6f75e0 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php @@ -2,8 +2,6 @@ namespace PhpPact\Standalone\MockService; -use Psr\Http\Message\UriInterface; - /** * Mock Server configuration interface to allow for simple overrides that are reusable. * Interface MockServerConfigInterface. @@ -11,91 +9,50 @@ interface MockServerConfigInterface { /** - * @return string the host of the mock service - */ - public function getHost(): string; - - /** - * @param string $host The host of the mock service - * - * @return MockServerConfigInterface - */ - public function setHost(string $host): self; - - /** - * @return int the port of the mock service - */ - public function getPort(): int; - - /** - * @param int $port the port of the mock service - * - * @return MockServerConfigInterface - */ - public function setPort(int $port): self; - - /** - * @return bool true if https + * @return string */ - public function isSecure(): bool; + public function getConsumer(): string; /** - * @param bool $secure set to true for https + * @param string $consumer consumers name * * @return MockServerConfigInterface */ - public function setSecure(bool $secure): self; + public function setConsumer(string $consumer): self; /** - * @return UriInterface + * @return string */ - public function getBaseUri(): UriInterface; + public function getProvider(): string; /** - * @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 + * @param string $provider providers name * * @return MockServerConfigInterface */ - public function setPactFileWriteMode(string $pactFileWriteMode): self; - - /** - * @return bool - */ - public function hasCors(): bool; + public function setProvider(string $provider): self; /** - * @param bool|string $flag - * - * @return MockServerConfigInterface + * @return string url to place the pact files when written to disk */ - public function setCors($flag): self; + public function getPactDir(): string; /** - * @param int $timeout + * @param string $pactDir url to place the pact files when written to disk * * @return MockServerConfigInterface */ - public function setHealthCheckTimeout($timeout): self; + public function setPactDir(string $pactDir): self; /** - * @return int + * @return string pact version */ - public function getHealthCheckTimeout(): int; + public function getPactSpecificationVersion(): string; /** - * @param int $seconds + * @param string $pactSpecificationVersion pact specification version * * @return MockServerConfigInterface */ - public function setHealthCheckRetrySec($seconds): self; - - /** - * @return int - */ - public function getHealthCheckRetrySec(): int; + public function setPactSpecificationVersion(string $pactSpecificationVersion): self; } diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index 529f7d2fb..583d10d25 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -10,41 +10,16 @@ */ class MockServerEnvConfig extends MockServerConfig { - const DEFAULT_SPECIFICATION_VERSION = '2.0.0'; + const DEFAULT_SPECIFICATION_VERSION = '3.0.0'; /** - * MockServerEnvConfig constructor. - * - * @throws MissingEnvVariableException + * PactEnvConfig constructor. */ public function __construct() { - $this->setHost($this->parseEnv('PACT_MOCK_SERVER_HOST')); - $this->setPort((int) $this->parseEnv('PACT_MOCK_SERVER_PORT')); + parent::__construct(); + $this->setPactDir($this->parseEnv('PACT_OUTPUT_DIR')); $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); - } - - if ($logLevel = $this->parseEnv('PACT_LOGLEVEL', false)) { - $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) { diff --git a/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php b/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php deleted file mode 100644 index 3d2ac348f..000000000 --- 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 e3bfbe62e..000000000 --- a/src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php +++ /dev/null @@ -1,51 +0,0 @@ -installManager = new InstallManager(); - } - - /** - * Build an example from the data structure back into its generated form - * i.e. strip out all of the matchers etc - * - * @param Message $pact - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - * - * @return string - */ - public function reify(Message $pact): string - { - $scripts = $this->installManager->install(); - - $json = \json_encode($pact); - $process = new ProcessRunner($scripts->getPactMessage(), ['reify', "'" . $json . "'"]); - - $process->runBlocking(); - - $output = $process->getOutput(); - \preg_replace("/\r|\n/", '', $output); - - return $output; - } - - /** - * Update a pact with the given message, or create the pact if it does not exist. The MESSAGE_JSON may be in the legacy Ruby JSON format or the v2+ format. - * - * @param string $pactJson - * @param string $consumer - * @param string $provider - * @param string $pactDir - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - * - * @return bool - */ - public function update(string $pactJson, string $consumer, string $provider, string $pactDir): bool - { - $scripts = $this->installManager->install(); - - $arguments = []; - $arguments[] = 'update'; - $arguments[] = "--consumer={$consumer}"; - $arguments[] = "--provider={$provider}"; - $arguments[] = "--pact-dir={$pactDir}"; - $arguments[] = "'" . $pactJson . "'"; - - $process = new ProcessRunner($scripts->getPactMessage(), $arguments); - $process->runBlocking(); - - \sleep(1); - - return true; - } -} diff --git a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php b/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php deleted file mode 100644 index 2874e7722..000000000 --- a/src/PhpPact/Standalone/PactMessage/PactMessageConfig.php +++ /dev/null @@ -1,169 +0,0 @@ -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/src/PhpPact/Standalone/ProviderVerifier/Model/ConsumerVersionSelectors.php b/src/PhpPact/Standalone/ProviderVerifier/Model/ConsumerVersionSelectors.php new file mode 100644 index 000000000..1f5e2ec8f --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/ConsumerVersionSelectors.php @@ -0,0 +1,39 @@ +selectors); + } + + /** + * @param null|string $tag + * @param bool $latest + * @param null|string $consumer + * @param null|string $fallbackTag + * + * @return ConsumerVersionSelectors + */ + public function add(?string $tag = null, bool $latest = true, ?string $consumer = null, ?string $fallbackTag = null): self + { + $selector = \array_filter([ + 'tag' => $tag, + 'latest' => $latest, + 'consumer' => $consumer, + 'fallbackTag' => $fallbackTag, + ]); + if (!empty($selector)) { + $this->selectors[] = $selector; + } + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php index fc7c384ce..1b0d36e7d 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfig.php @@ -5,81 +5,94 @@ use Psr\Http\Message\UriInterface; /** - * {@inheritdoc} + * Class VerifierConfig. */ class VerifierConfig implements VerifierConfigInterface { - /** @var null|UriInterface */ - private $providerBaseUrl; + private ?string $basePath = null; + private ?string $scheme = null; + private ?string $host = null; + private ?int $port = null; + private ?UriInterface $stateChangeUrl = null; + private ?string $providerName = null; + private ?string $providerVersion = null; + private ?string $providerTags = null; + private ?string $consumerVersionTags = null; + private ?UriInterface $brokerUrl = null; + private ?string $brokerToken = null; + private ?string $brokerUsername = null; + private ?string $brokerPassword = null; + private ?string $includeWipPactsSince = null; + private ?UriInterface $buildUrl = null; + private ?string $consumerVersionSelectors = null; + private ?string $logLevel = null; + private ?int $requestTimeout = null; - /** @var null|string */ - private $providerStatesSetupUrl; + private array $urls = []; + private array $dirs = []; + private array $files = []; - /** @var string */ - private $providerName; + private array $filterConsumerNames = []; + private ?string $filterDescription = null; + private ?string $filterNoState = null; + private ?string $filterState = null; - /** @var string */ - private $providerVersion; + private bool $publish = false; + private bool $disableSslVerification = false; + private bool $enablePending = false; + private bool $stateChangeAsQuery = false; + private bool $stateChangeTeardown = false; - /** @var array */ - private $providerVersionTag = []; - - /** @var bool */ - private $publishResults = false; - - /** @var null|UriInterface */ - private $brokerUri; - - /** @var null|string */ - private $brokerToken; - - /** @var null|string */ - private $brokerUsername; - - /** @var null|string */ - private $brokerPassword; - - /** @var string[] */ - private $customProviderHeaders; - - /** @var bool */ - private $verbose = false; - - /** @var string */ - private $format; - - /** @var int */ - private $processTimeout = 60; + /** + * {@inheritdoc} + */ + public function getBasePath(): ?string + { + return $this->basePath; + } - /** @var int */ - private $processIdleTimeout = 10; + /** + * {@inheritdoc} + */ + public function setBasePath(string $basePath): VerifierConfigInterface + { + $this->basePath = $basePath; - /** @var bool */ - private $enablePending = false; + return $this; + } - /** @var null|string */ - private $wipPactSince; + /** + * {@inheritdoc} + */ + public function getScheme(): ?string + { + return $this->scheme; + } - /** @var array */ - private $consumerVersionTag = []; + /** + * {@inheritdoc} + */ + public function setScheme(string $scheme): VerifierConfigInterface + { + $this->scheme = $scheme; - /** @var null|callable */ - private $requestFilter; + return $this; + } /** * {@inheritdoc} */ - public function getProviderBaseUrl() + public function getHost(): ?string { - return $this->providerBaseUrl; + return $this->host; } /** * {@inheritdoc} */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): VerifierConfigInterface + public function setHost(string $host): VerifierConfigInterface { - $this->providerBaseUrl = $providerBaseUrl; + $this->host = $host; return $this; } @@ -87,17 +100,17 @@ public function setProviderBaseUrl(UriInterface $providerBaseUrl): VerifierConfi /** * {@inheritdoc} */ - public function getProviderStatesSetupUrl() + public function getPort(): ?int { - return $this->providerStatesSetupUrl; + return $this->port; } /** * {@inheritdoc} */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): VerifierConfigInterface + public function setPort(int $port): VerifierConfigInterface { - $this->providerStatesSetupUrl = $providerStatesSetupUrl; + $this->port = $port; return $this; } @@ -105,17 +118,17 @@ public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): Verif /** * {@inheritdoc} */ - public function getProviderName(): string + public function getStateChangeUrl(): ?UriInterface { - return $this->providerName; + return $this->stateChangeUrl; } /** * {@inheritdoc} */ - public function setProviderName(string $providerName): VerifierConfigInterface + public function setStateChangeUrl(UriInterface $stateChangeUrl): VerifierConfigInterface { - $this->providerName = $providerName; + $this->stateChangeUrl = $stateChangeUrl; return $this; } @@ -123,17 +136,17 @@ public function setProviderName(string $providerName): VerifierConfigInterface /** * {@inheritdoc} */ - public function getProviderVersion() + public function getProviderName(): ?string { - return $this->providerVersion; + return $this->providerName; } /** * {@inheritdoc} */ - public function setProviderVersion(string $providerVersion): VerifierConfigInterface + public function setProviderName(string $providerName): VerifierConfigInterface { - $this->providerVersion = $providerVersion; + $this->providerName = $providerName; return $this; } @@ -141,33 +154,35 @@ public function setProviderVersion(string $providerVersion): VerifierConfigInter /** * {@inheritdoc} */ - public function getProviderVersionTag() + public function getProviderVersion(): ?string { - return $this->providerVersionTag; + return $this->providerVersion; } /** * {@inheritdoc} */ - public function setProviderVersionTag(string $providerVersionTag): VerifierConfigInterface + public function setProviderVersion(string $providerVersion): VerifierConfigInterface { - return $this->addProviderVersionTag($providerVersionTag); + $this->providerVersion = $providerVersion; + + return $this; } /** * {@inheritdoc} */ - public function getConsumerVersionTag() + public function getProviderTags(): ?string { - return $this->consumerVersionTag; + return $this->providerTags; } /** * {@inheritdoc} */ - public function addConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface + public function setProviderTags(string $providerTags): VerifierConfigInterface { - $this->consumerVersionTag[] = $consumerVersionTag; + $this->providerTags = $providerTags; return $this; } @@ -175,37 +190,37 @@ public function addConsumerVersionTag(string $consumerVersionTag): VerifierConfi /** * {@inheritdoc} */ - public function addProviderVersionTag(string $providerVersionTag): VerifierConfigInterface + public function getConsumerVersionTags(): ?string { - $this->providerVersionTag[] = $providerVersionTag; - - return $this; + return $this->consumerVersionTags; } /** - * @param string $consumerVersionTag + * @param string $consumerVersionTags * * @return VerifierConfigInterface */ - public function setConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface + public function setConsumerVersionTags(string $consumerVersionTags): VerifierConfigInterface { - return $this->addConsumerVersionTag($consumerVersionTag); + $this->consumerVersionTags = $consumerVersionTags; + + return $this; } /** * {@inheritdoc} */ - public function isPublishResults(): bool + public function isPublish(): bool { - return $this->publishResults; + return $this->publish; } /** * {@inheritdoc} */ - public function setPublishResults(bool $publishResults): VerifierConfigInterface + public function setPublish(bool $publish): VerifierConfigInterface { - $this->publishResults = $publishResults; + $this->publish = $publish; return $this; } @@ -213,17 +228,17 @@ public function setPublishResults(bool $publishResults): VerifierConfigInterface /** * {@inheritdoc} */ - public function getBrokerUri() + public function getBrokerUrl(): ?UriInterface { - return $this->brokerUri; + return $this->brokerUrl; } /** * {@inheritdoc} */ - public function setBrokerUri(UriInterface $brokerUri): VerifierConfigInterface + public function setBrokerUrl(UriInterface $brokerUrl): VerifierConfigInterface { - $this->brokerUri = $brokerUri; + $this->brokerUrl = $brokerUrl; return $this; } @@ -249,7 +264,7 @@ public function setBrokerToken(?string $brokerToken): VerifierConfigInterface /** * {@inheritdoc} */ - public function getBrokerUsername() + public function getBrokerUsername(): ?string { return $this->brokerUsername; } @@ -257,7 +272,7 @@ public function getBrokerUsername() /** * {@inheritdoc} */ - public function setBrokerUsername(string $brokerUsername) + public function setBrokerUsername(string $brokerUsername): VerifierConfigInterface { $this->brokerUsername = $brokerUsername; @@ -267,7 +282,7 @@ public function setBrokerUsername(string $brokerUsername) /** * {@inheritdoc} */ - public function getBrokerPassword() + public function getBrokerPassword(): ?string { return $this->brokerPassword; } @@ -275,7 +290,7 @@ public function getBrokerPassword() /** * {@inheritdoc} */ - public function setBrokerPassword(string $brokerPassword) + public function setBrokerPassword(string $brokerPassword): VerifierConfigInterface { $this->brokerPassword = $brokerPassword; @@ -285,27 +300,75 @@ public function setBrokerPassword(string $brokerPassword) /** * {@inheritdoc} */ - public function getCustomProviderHeaders() + public function isDisableSslVerification(): bool { - return $this->customProviderHeaders; + return $this->disableSslVerification; } /** * {@inheritdoc} */ - public function setCustomProviderHeaders(array $customProviderHeaders): VerifierConfigInterface + public function setDisableSslVerification(bool $disableSslVerification): VerifierConfigInterface + { + $this->disableSslVerification = $disableSslVerification; + + return $this; + } + + /** + * @param bool $stateChangeAsQuery + * + * @return VerifierConfigInterface + */ + public function setStateChangeAsQuery(bool $stateChangeAsQuery): VerifierConfigInterface { - $this->customProviderHeaders = $customProviderHeaders; + $this->stateChangeAsQuery = $stateChangeAsQuery; return $this; } + /** + * @return bool + */ + public function isStateChangeAsQuery(): bool + { + return $this->stateChangeAsQuery; + } + + /** + * @param bool $stateChangeTeardown + * + * @return VerifierConfigInterface + */ + public function setStateChangeTeardown(bool $stateChangeTeardown): VerifierConfigInterface + { + $this->stateChangeTeardown = $stateChangeTeardown; + + return $this; + } + + /** + * @return bool + */ + public function isStateChangeTeardown(): bool + { + return $this->stateChangeTeardown; + } + + /** + * {@inheritdoc} + */ + public function isEnablePending(): bool + { + return $this->enablePending; + } + /** * {@inheritdoc} */ - public function addCustomProviderHeader(string $name, string $value): VerifierConfigInterface + public function setEnablePending(bool $enablePending): VerifierConfigInterface { - $this->customProviderHeaders[] = "{$name}: {$value}"; + $this->enablePending = $enablePending; return $this; } @@ -313,17 +376,35 @@ public function addCustomProviderHeader(string $name, string $value): VerifierCo /** * {@inheritdoc} */ - public function isVerbose(): bool + public function setIncludeWipPactsSince(string $includeWipPactsSince): VerifierConfigInterface { - return $this->verbose; + $this->includeWipPactsSince = $includeWipPactsSince; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getIncludeWipPactsSince(): ?string + { + return $this->includeWipPactsSince; + } + + /** + * {@inheritdoc} + */ + public function getBuildUrl(): ?UriInterface + { + return $this->buildUrl; } /** * {@inheritdoc} */ - public function setVerbose(bool $verbose): VerifierConfigInterface + public function setBuildUrl(UriInterface $buildUrl): VerifierConfigInterface { - $this->verbose = $verbose; + $this->buildUrl = $buildUrl; return $this; } @@ -331,75 +412,99 @@ public function setVerbose(bool $verbose): VerifierConfigInterface /** * {@inheritdoc} */ - public function getFormat() + public function setConsumerVersionSelectors(string $consumerVersionSelectors): VerifierConfigInterface { - return $this->format; + $this->consumerVersionSelectors = $consumerVersionSelectors; + + return $this; } /** * {@inheritdoc} */ - public function setFormat(string $format): VerifierConfigInterface + public function getConsumerVersionSelectors(): ?string { - $this->format = $format; + return $this->consumerVersionSelectors; + } + + /** + * {@inheritdoc} + */ + public function setLogLevel(string $logLevel): VerifierConfigInterface + { + $this->logLevel = $logLevel; return $this; } /** - * @param int $timeout - * - * @return VerifierConfigInterface + * {@inheritdoc} */ - public function setProcessTimeout(int $timeout): VerifierConfigInterface + public function getLogLevel(): ?string { - $this->processTimeout = $timeout; + return $this->logLevel; + } + + /** + * {@inheritdoc} + */ + public function setRequestTimeout(int $requestTimeout): VerifierConfigInterface + { + $this->requestTimeout = $requestTimeout; return $this; } /** - * @param int $timeout - * - * @return VerifierConfigInterface + * {@inheritdoc} */ - public function setProcessIdleTimeout(int $timeout): VerifierConfigInterface + public function getRequestTimeout(): ?int { - $this->processIdleTimeout = $timeout; + return $this->requestTimeout; + } + + /** + * {@inheritdoc} + */ + public function setUrls(string ...$urls): VerifierConfigInterface + { + $this->urls = $urls; return $this; } /** - * @return int + * {@inheritdoc} */ - public function getProcessTimeout(): int + public function getUrls(): array { - return $this->processTimeout; + return $this->urls; } /** - * @return int + * {@inheritdoc} */ - public function getProcessIdleTimeout(): int + public function setDirs(string ...$dirs): VerifierConfigInterface { - return $this->processIdleTimeout; + $this->dirs = $dirs; + + return $this; } /** * {@inheritdoc} */ - public function isEnablePending(): bool + public function getDirs(): array { - return $this->enablePending; + return $this->dirs; } /** * {@inheritdoc} */ - public function setEnablePending(bool $pending): VerifierConfigInterface + public function setFiles(string ...$files): VerifierConfigInterface { - $this->enablePending = $pending; + $this->files = $files; return $this; } @@ -407,9 +512,43 @@ public function setEnablePending(bool $pending): VerifierConfigInterface /** * {@inheritdoc} */ - public function setIncludeWipPactSince(string $date): VerifierConfigInterface + public function getFiles(): array + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function setFilterConsumerNames(string ...$filterConsumerNames): VerifierConfigInterface + { + $this->filterConsumerNames = $filterConsumerNames; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilterConsumerNames(): array + { + return $this->filterConsumerNames; + } + + /** + * {@inheritdoc} + */ + public function getFilterDescription(): ?string { - $this->wipPactSince = $date; + return $this->filterDescription; + } + + /** + * {@inheritdoc} + */ + public function setFilterDescription(string $filterDescription): VerifierConfigInterface + { + $this->filterDescription = $filterDescription; return $this; } @@ -417,19 +556,35 @@ public function setIncludeWipPactSince(string $date): VerifierConfigInterface /** * {@inheritdoc} */ - public function getIncludeWipPactSince() + public function getFilterNoState(): ?string { - return $this->wipPactSince; + return $this->filterNoState; } - public function getRequestFilter(): ?callable + /** + * {@inheritdoc} + */ + public function setFilterNoState(string $filterNoState): VerifierConfigInterface { - return $this->requestFilter; + $this->filterNoState = $filterNoState; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilterState(): ?string + { + return $this->filterState; } - public function setRequestFilter(callable $requestFilter): VerifierConfigInterface + /** + * {@inheritdoc} + */ + public function setFilterState(string $filterState): VerifierConfigInterface { - $this->requestFilter = $requestFilter; + $this->filterState = $filterState; return $this; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php index 91a3198af..c12812a6c 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php @@ -11,107 +11,142 @@ interface VerifierConfigInterface { /** - * @return null|UriInterface providers base url + * @return null|string providers base path */ - public function getProviderBaseUrl(); + public function getBasePath(): ?string; /** - * @param UriInterface $providerBaseUrl providers base url + * @param string $basePath providers base path * * @return VerifierConfigInterface */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): self; + public function setBasePath(string $basePath): self; /** - * @return null|string Base URL to setup the provider states at + * @return null|string Provider URI scheme */ - public function getProviderStatesSetupUrl(); + public function getScheme(): ?string; /** - * @param string $providerStatesSetupUrl Base URL to setup the provider states at + * @param string $scheme Provider URI scheme * * @return VerifierConfigInterface */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): self; + public function setScheme(string $scheme): self; /** - * @return null|string name of the provider + * @return null|string providers host */ - public function getProviderName(); + public function getHost(): ?string; /** - * @param string $name Name of the provider + * @param string $host providers host * * @return VerifierConfigInterface */ - public function setProviderName(string $name): self; + public function setHost(string $host): self; /** - * @return null|string providers version + * @return null|int providers port + */ + public function getPort(): ?int; + + /** + * @param int $port providers port + * + * @return VerifierConfigInterface + */ + public function setPort(int $port): self; + + /** + * @return null|UriInterface Base URL to setup the provider states at + */ + public function getStateChangeUrl(): ?UriInterface; + + /** + * @param UriInterface $stateChangeUrl Base URL to setup the provider states at + * + * @return VerifierConfigInterface */ - public function getProviderVersion(); + public function setStateChangeUrl(UriInterface $stateChangeUrl): self; /** - * @param string $providerAppVersion providers version + * @return null|string name of the provider + */ + public function getProviderName(): ?string; + + /** + * @param string $providerName Name of the provider * * @return VerifierConfigInterface */ - public function setProviderVersion(string $providerAppVersion): self; + public function setProviderName(string $providerName): self; /** - * @return array providers version tag + * @return null|string providers version */ - public function getProviderVersionTag(); + public function getProviderVersion(): ?string; /** - * @param string $providerVersionTag providers version tag + * @param string $providerVersion providers version * * @return VerifierConfigInterface */ - public function setProviderVersionTag(string $providerVersionTag): self; + public function setProviderVersion(string $providerVersion): self; /** - * @return array consumers version tag + * @return null|string providers version tag */ - public function getConsumerVersionTag(); + public function getProviderTags(): ?string; /** - * @param string $consumerVersionTag consumers version tag + * @param string $providerTags Provider tags to use when publishing results. Accepts comma-separated values. * * @return VerifierConfigInterface */ - public function addConsumerVersionTag(string $consumerVersionTag): self; + public function setProviderTags(string $providerTags): self; /** - * @param string $providerVersionTag provider version tag + * @return null|string consumers version tags + */ + public function getConsumerVersionTags(): ?string; + + /** + * Consumer tags to use when fetching pacts from the Broker. Accepts comma-separated values. + * + * @param string $consumerVersionTags * * @return VerifierConfigInterface */ - public function addProviderVersionTag(string $providerVersionTag): self; + public function setConsumerVersionTags(string $consumerVersionTags): self; /** * @return bool are results going to be published */ - public function isPublishResults(): bool; + public function isPublish(): bool; /** - * @param bool $publishResults flag to publish results + * Enables publishing of verification results back to the Pact Broker. Requires the broker-url and provider-version parameters. + * + * @param bool $publish * * @return VerifierConfigInterface */ - public function setPublishResults(bool $publishResults): self; + public function setPublish(bool $publish): self; /** * @return null|UriInterface url to the pact broker */ - public function getBrokerUri(); + public function getBrokerUrl(): ?UriInterface; /** - * @param UriInterface $brokerUri uri to the pact broker + * URL of the pact broker to fetch pacts from to verify (requires the provider name parameter) + * + * @param UriInterface $brokerUrl * * @return VerifierConfigInterface */ - public function setBrokerUri(UriInterface $brokerUri): self; + public function setBrokerUrl(UriInterface $brokerUrl): self; /** * @return null|string token for the pact broker @@ -128,129 +163,226 @@ public function setBrokerToken(?string $brokerToken): self; /** * @return null|string username for the pact broker if secured */ - public function getBrokerUsername(); + public function getBrokerUsername(): ?string; /** * @param string $brokerUsername username for the pact broker if secured * * @return VerifierConfigInterface */ - public function setBrokerUsername(string $brokerUsername); + public function setBrokerUsername(string $brokerUsername): self; /** * @return null|string password for the pact broker if secured */ - public function getBrokerPassword(); + public function getBrokerPassword(): ?string; /** * @param string $brokerPassword password for the pact broker if secured * * @return VerifierConfigInterface */ - public function setBrokerPassword(string $brokerPassword); + public function setBrokerPassword(string $brokerPassword): self; /** - * @return null|string[] custom headers for the request to the provider such as authorization + * @return bool is verbosity level increased */ - public function getCustomProviderHeaders(); + public function isDisableSslVerification(): bool; /** - * @param string[] $customProviderHeaders custom headers for the requests to the provider such as authorization + * @param bool $disableSslVerification Disables validation of SSL certificates * * @return VerifierConfigInterface */ - public function setCustomProviderHeaders(array $customProviderHeaders): self; + public function setDisableSslVerification(bool $disableSslVerification): self; /** - * @param string $name - * @param string $value + * @param bool $stateChangeAsQuery * * @return VerifierConfigInterface */ - public function addCustomProviderHeader(string $name, string $value): self; + public function setStateChangeAsQuery(bool $stateChangeAsQuery): self; /** - * @return bool is verbosity level increased + * @return bool */ - public function isVerbose(): bool; + public function isStateChangeAsQuery(): bool; /** - * @param bool $verbose increase verbosity level + * @param bool $stateChangeTeardown * * @return VerifierConfigInterface */ - public function setVerbose(bool $verbose): self; + public function setStateChangeTeardown(bool $stateChangeTeardown): self; /** - * @return null|string RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used + * @return bool */ - public function getFormat(); + public function isStateChangeTeardown(): bool; /** - * @param string $format RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used + * @param bool $enablePending allow pacts which are in pending state to be verified without causing the overall task to fail * * @return VerifierConfigInterface */ - public function setFormat(string $format): self; + public function setEnablePending(bool $enablePending): self; + + /** + * @return bool is enabled pending pacts + */ + public function isEnablePending(): bool; /** - * @param int $timeout + * Allow pacts that don't match given consumer selectors (or tags) to be verified, without causing the overall task to fail. For more information, see https://pact.io/wip + * + * @param string $includeWipPactsSince * * @return VerifierConfigInterface */ - public function setProcessTimeout(int $timeout): self; + public function setIncludeWipPactsSince(string $includeWipPactsSince): self; + + /** + * @return null|string get start date of included WIP Pacts + */ + public function getIncludeWipPactsSince(): ?string; + + /** + * @return null|UriInterface + */ + public function getBuildUrl(): ?UriInterface; /** - * @param int $timeout + * URL of the build to associate with the published verification results. * - * @return VerifierConfigInterface + * @param UriInterface $buildUrl + * + * @return $this */ - public function setProcessIdleTimeout(int $timeout): self; + public function setBuildUrl(UriInterface $buildUrl): self; /** - * @return int + * @param string $consumerVersionSelectors + * + * @return $this */ - public function getProcessTimeout(): int; + public function setConsumerVersionSelectors(string $consumerVersionSelectors): self; /** - * @return int + * @return null|string */ - public function getProcessIdleTimeout(): int; + public function getConsumerVersionSelectors(): ?string; /** - * @param bool $pending allow pacts which are in pending state to be verified without causing the overall task to fail + * @return null|string + */ + public function getLogLevel(): ?string; + + /** + * @param string $logLevel Log level (defaults to warn) [possible values: error, warn, info, debug, trace, none] * - * @return VerifierConfigInterface + * @return $this */ - public function setEnablePending(bool $pending): self; + public function setLogLevel(string $logLevel): self; /** - * @return bool is enabled pending pacts + * Sets the HTTP request timeout in milliseconds for requests to the target API and for state change requests. + * + * @param int $requestTimeout + * + * @return $this */ - public function isEnablePending(): bool; + public function setRequestTimeout(int $requestTimeout): self; + + /** + * @return null|int + */ + public function getRequestTimeout(): ?int; /** - * @param string $date Includes pact marked as WIP since this date. - * Accepted formats: Y-m-d (2020-01-30) or c (ISO 8601 date 2004-02-12T15:19:21+00:00) + * @param string ...$urls URL of pact file to verify * - * @return VerifierConfigInterface + * @return $this */ - public function setIncludeWipPactSince(string $date): self; + public function setUrls(string ...$urls): self; /** - * @return null|string get start date of included WIP Pacts + * @return array + */ + public function getUrls(): array; + + /** + * @param string ...$dirs Directory of pact files to verify + * + * @return $this + */ + public function setDirs(string ...$dirs): self; + + /** + * @return array + */ + public function getDirs(): array; + + /** + * @param string ...$files Pact file to verify + * + * @return $this + */ + public function setFiles(string ...$files): self; + + /** + * @return array */ - public function getIncludeWipPactSince(); + public function getFiles(): array; /** - * @return null|callable + * @param string ...$filterConsumerNames Consumer name to filter the pacts to be verified + * + * @return $this + */ + public function setFilterConsumerNames(string ...$filterConsumerNames): self; + + /** + * @return array + */ + public function getFilterConsumerNames(): array; + + /** + * @param string $filterDescription Only validate interactions whose descriptions match this filter + * + * @return $this + */ + public function setFilterDescription(string $filterDescription): self; + + /** + * @return null|string */ - public function getRequestFilter(): ?callable; + public function getFilterDescription(): ?string; /** - * @param callable $requestFilter + * Only validate interactions that have no defined provider state + * + * @param string $filterNoState * * @return $this */ - public function setRequestFilter(callable $requestFilter): self; + public function setFilterNoState(string $filterNoState): self; + + /** + * @return null|string + */ + public function getFilterNoState(): ?string; + + /** + * Only validate interactions whose provider states match this filter + * + * @param string $filterState + * + * @return $this + */ + public function setFilterState(string $filterState): self; + + /** + * @return null|string + */ + public function getFilterState(): ?string; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php b/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php deleted file mode 100644 index 60f73750b..000000000 --- a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php +++ /dev/null @@ -1,25 +0,0 @@ -setLogger($logger); - } - - return $processRunner; - } -} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index 527a04a99..278594b3c 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -2,272 +2,175 @@ namespace PhpPact\Standalone\ProviderVerifier; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Http\GuzzleClient; +use FFI; use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; use PhpPact\Standalone\Installer\InstallManager; -use PhpPact\Standalone\Installer\Service\InstallerInterface; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; /** - * Wrapper for the Ruby Standalone Verifier service. - * Class VerifierServer. + * Class Verifier. */ class Verifier { - /** @var int */ - protected $processTimeout = 60; - - /** @var int */ - protected $processIdleTimeout = 10; - - /** @var VerifierConfigInterface */ - protected $config; - - /** @var null|BrokerHttpClientInterface */ - protected $brokerHttpClient; - - /** @var InstallManager */ - protected $installManager; - - /** @var null|VerifierProcess */ - protected $verifierProcess; - - public function __construct( - VerifierConfigInterface $config, - InstallManager $installManager = null, - VerifierProcess $verifierProcess = null, - BrokerHttpClient $brokerHttpClient = null - ) { - $this->config = $config; - $this->installManager = $installManager?: new InstallManager(); - $this->verifierProcess = $verifierProcess?: new VerifierProcess($this->installManager); - $this->processTimeout = $config->getProcessTimeout(); - $this->processIdleTimeout = $config->getProcessIdleTimeout(); - - if ($brokerHttpClient) { - $this->brokerHttpClient = $brokerHttpClient; - } - } + protected FFI $ffi; + protected VerifierConfigInterface $config; /** - * @throws \Exception + * Verifier constructor. + * + * @param VerifierConfigInterface $config * - * @return array parameters to be passed into the process + * @throws FileDownloadFailureException + * @throws NoDownloaderFoundException + */ + public function __construct(VerifierConfigInterface $config) + { + $this->config = $config; + $scripts = (new InstallManager())->install(); + $this->ffi = FFI::cdef(\file_get_contents($scripts->getCode()), $scripts->getLibrary()); + $this->ffi->pactffi_init('PACT_LOGLEVEL'); + } + + /** + * @return array */ public function getArguments(): array { $parameters = []; - if ($this->config->getProviderBaseUrl() !== null) { - $parameters[] = "--provider-base-url={$this->config->getProviderBaseUrl()}"; + if ($this->config->getBasePath() !== null) { + $parameters[] = "--base-path={$this->config->getBasePath()}"; } - if ($this->config->getProviderVersion() !== null) { - $parameters[] = "--provider-app-version={$this->config->getProviderVersion()}"; + if ($this->config->getScheme() !== null) { + $parameters[] = "--scheme={$this->config->getScheme()}"; } - if (\count($this->config->getConsumerVersionTag()) > 0) { - foreach ($this->config->getConsumerVersionTag() as $tag) { - $parameters[] = "--consumer-version-tag={$tag}"; - } + if ($this->config->getHost() !== null) { + $parameters[] = "--hostname={$this->config->getHost()}"; } - if (\count($this->config->getProviderVersionTag()) > 0) { - foreach ($this->config->getProviderVersionTag() as $tag) { - $parameters[] = "--provider-version-tag={$tag}"; - } + if ($this->config->getPort() !== null) { + $parameters[] = "--port={$this->config->getPort()}"; } - if ($this->config->getProviderStatesSetupUrl() !== null) { - $parameters[] = "--provider-states-setup-url={$this->config->getProviderStatesSetupUrl()}"; + if ($this->config->getProviderName() !== null) { + $parameters[] = "--provider-name={$this->config->getProviderName()}"; } - if ($this->config->isPublishResults() === true) { - $parameters[] = '--publish-verification-results'; + if ($this->config->getProviderVersion() !== null) { + $parameters[] = "--provider-version={$this->config->getProviderVersion()}"; } - if ($this->config->getBrokerToken() !== null) { - $parameters[] = "--broker-token={$this->config->getBrokerToken()}"; + if ($this->config->getConsumerVersionTags() !== null) { + $parameters[] = "--consumer-version-tags={$this->config->getConsumerVersionTags()}"; } - if ($this->config->getBrokerUsername() !== null) { - $parameters[] = "--broker-username={$this->config->getBrokerUsername()}"; + if ($this->config->getProviderTags() !== null) { + $parameters[] = "--provider-tags={$this->config->getProviderTags()}"; } - if ($this->config->getBrokerPassword() !== null) { - $parameters[] = "--broker-password={$this->config->getBrokerPassword()}"; + if ($this->config->getStateChangeUrl() !== null) { + $parameters[] = "--state-change-url={$this->config->getStateChangeUrl()}"; } - if ($this->config->getCustomProviderHeaders() !== null) { - foreach ($this->config->getCustomProviderHeaders() as $customProviderHeader) { - $parameters[] = "--custom-provider-header=\"{$customProviderHeader}\""; - } + if ($this->config->getBrokerUrl() !== null) { + $parameters[] = "--broker-url={$this->config->getBrokerUrl()}"; } - if ($this->config->isVerbose() === true) { - $parameters[] = '--verbose'; + if ($this->config->getBrokerToken() !== null) { + $parameters[] = "--token={$this->config->getBrokerToken()}"; } - if ($this->config->getFormat() !== null) { - $parameters[] = "--format={$this->config->getFormat()}"; + if ($this->config->getBrokerUsername() !== null) { + $parameters[] = "--user={$this->config->getBrokerUsername()}"; } - if ($this->config->isEnablePending() === true) { - $parameters[] = '--enable-pending'; + if ($this->config->getBrokerPassword() !== null) { + $parameters[] = "--password={$this->config->getBrokerPassword()}"; } - if ($this->config->getIncludeWipPactSince() !== null) { - $parameters[] = "--include-wip-pacts-since={$this->config->getIncludeWipPactSince()}"; + if ($this->config->getIncludeWipPactsSince() !== null) { + $parameters[] = "--include-wip-pacts-since={$this->config->getIncludeWipPactsSince()}"; } - return $parameters; - } - - /** - * Make the request to the PACT Verifier Service to run a Pact file tests from the Pact Broker. - * - * @param string $consumerName name of the consumer to be compared against - * @param null|string $tag optional tag of the consumer such as a branch name - * @param null|string $consumerVersion optional specific version of the consumer; this is overridden by tag - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - * - * @return Verifier - */ - public function verify(string $consumerName, string $tag = null, string $consumerVersion = null): self - { - $path = "pacts/provider/{$this->config->getProviderName()}/consumer/{$consumerName}/"; - - if ($tag) { - $path .= "latest/{$tag}/"; - } elseif ($consumerVersion) { - $path .= "version/{$consumerVersion}/"; - } else { - $path .= 'latest/'; + if ($this->config->getBuildUrl() !== null) { + $parameters[] = "--build-url={$this->config->getBuildUrl()}"; } - $uri = $this->config->getBrokerUri()->withPath($path); + if ($this->config->getConsumerVersionSelectors() !== null) { + $selectors = \addslashes($this->config->getConsumerVersionSelectors()); + $parameters[] = "--consumer-version-selectors=\"{$selectors}\""; + } - $arguments = \array_merge([$uri->__toString()], $this->getArguments()); + if ($this->config->getLogLevel() !== null) { + $parameters[] = "--loglevel={$this->config->getLogLevel()}"; + } - $this->verifyAction($arguments); + if ($this->config->getRequestTimeout() !== null) { + $parameters[] = "--request-timeout={$this->config->getRequestTimeout()}"; + } - return $this; - } + foreach ($this->config->getUrls() as $url) { + $parameters[] = "--url={$url}"; + } - /** - * Provides a way to validate local Pact JSON files. - * - * @param array $files paths to pact json files - * - * @throws FileDownloadFailureException - * @throws NoDownloaderFoundException - * - * @return Verifier - */ - public function verifyFiles(array $files): self - { - $arguments = \array_merge($files, $this->getArguments()); + foreach ($this->config->getDirs() as $dir) { + $parameters[] = "--dir={$dir}"; + } - $this->verifyAction($arguments); + foreach ($this->config->getFiles() as $file) { + $parameters[] = "--file={$file}"; + } - return $this; - } + foreach ($this->config->getFilterConsumerNames() as $consumer) { + $parameters[] = "--filter-consumer={$consumer}"; + } - /** - * Verify all Pacts from the Pact Broker are valid for the Provider. - * - * @throws FileDownloadFailureException - * @throws NoDownloaderFoundException - */ - public function verifyAll() - { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrls($this->config->getProviderName()); + if ($this->config->getFilterDescription() !== null) { + $description = \addslashes($this->config->getFilterDescription()); + $parameters[] = "--filter-description=\"{$description}\""; + } - $arguments = \array_merge($arguments, $this->getArguments()); + if ($this->config->getFilterNoState() !== null) { + $parameters[] = "--filter-no-state={$this->config->getFilterNoState()}"; + } - $this->verifyAction($arguments); - } + if ($this->config->getFilterState() !== null) { + $parameters[] = "--filter-state={$this->config->getFilterState()}"; + } - /** - * Verify all PACTs for a given tag. - * - * @param string $tag - * - * @throws FileDownloadFailureException - * @throws NoDownloaderFoundException - */ - public function verifyAllForTag(string $tag) - { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrlsForTag($this->config->getProviderName(), $tag); + if ($this->config->isDisableSslVerification() === true) { + $parameters[] = '--disable-ssl-verification'; + } - $arguments = \array_merge($arguments, $this->getArguments()); + if ($this->config->isEnablePending() === true) { + $parameters[] = '--enable-pending'; + } - $this->verifyAction($arguments); - } + if ($this->config->isPublish() === true) { + $parameters[] = '--publish'; + } - /** - * Wrapper to add a custom installer. - * - * @param InstallerInterface $installer - * - * @return self - */ - public function registerInstaller(InstallerInterface $installer): self - { - $this->installManager->registerInstaller($installer); + if ($this->config->isStateChangeAsQuery() === true) { + $parameters[] = '--state-change-as-query'; + } - return $this; - } + if ($this->config->isStateChangeTeardown() === true) { + $parameters[] = '--state-change-teardown'; + } - public function getTimeoutValues(): array - { - return ['process_timeout' => $this->processTimeout, 'process_idle_timeout' => $this->processIdleTimeout]; + return $parameters; } /** - * Trigger execution of the Pact Verifier Service. - * - * @param array $arguments + * Verifier a provider. * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException + * @return bool */ - protected function verifyAction(array $arguments) + public function verify(): bool { - $this->verifierProcess->run($arguments, $this->processTimeout, $this->processIdleTimeout); - } - - protected function getBrokerHttpClient(): BrokerHttpClientInterface - { - if (!$this->brokerHttpClient) { - $user = $this->config->getBrokerUsername(); - $password = $this->config->getBrokerPassword(); - $token = $this->config->getBrokerToken(); - $reqFilter = $this->config->getRequestFilter(); - - $config = []; - if (\strlen($token) > 0) { - $config = ['headers' => ['Authorization' => 'Bearer ' . $token]]; - } elseif ($user && $password) { - $config = ['auth' => [$user, $password]]; - } - if (\is_callable($reqFilter)) { - $stack = HandlerStack::create(); - $stack->push(Middleware::mapRequest($reqFilter), 'requestFilter'); - $config['handler'] = $stack; - } - $client = new GuzzleClient($config); - - $this->brokerHttpClient = new BrokerHttpClient($client, $this->config->getBrokerUri()); - } - - return $this->brokerHttpClient; + return !$this->ffi->pactffi_verify(\implode(PHP_EOL, $this->getArguments())); } } diff --git a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php b/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php deleted file mode 100644 index ddc519fa2..000000000 --- a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php +++ /dev/null @@ -1,103 +0,0 @@ -installManager = $installManager; - $this->processRunnerFactory = $processRunnerFactory?: new ProcessRunnerFactory(); - } - - /** - * @param LoggerInterface $logger - * - * @return VerifierProcess - */ - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * Execute the Pact Verifier Service. - * - * @param array $arguments - * @param int $processTimeout - * @param int $processIdleTimeout - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException - */ - public function run(array $arguments, $processTimeout, $processIdleTimeout) - { - $scripts = $this->installManager->install(); - - $logger = $this->getLogger(); - $processRunner = $this->processRunnerFactory->createRunner( - $scripts->getProviderVerifier(), - $arguments, - $logger - ); - - $logger->info("Verifying PACT with script:\n{$processRunner->getCommand()}\n\n"); - - try { - $processRunner->runBlocking(); - - $logger->info('out > ' . $processRunner->getOutput()); - $logger->error('err > ' . $processRunner->getStderr()); - } catch (\Exception $e) { - $logger->info('out > ' . $processRunner->getOutput()); - $logger->error('err > ' . $processRunner->getStderr()); - - throw $e; - } - } - - /** - * @return LoggerInterface - */ - private function getLogger() - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('console'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/Runner/ProcessRunner.php b/src/PhpPact/Standalone/Runner/ProcessRunner.php index bcd8325fb..d922f933c 100644 --- a/src/PhpPact/Standalone/Runner/ProcessRunner.php +++ b/src/PhpPact/Standalone/Runner/ProcessRunner.php @@ -11,28 +11,18 @@ use Amp\Process\ProcessException; use Monolog\Logger; use Psr\Log\LoggerInterface; +use Throwable; /** * Wrapper around Process with Amp */ class ProcessRunner { - /** @var Process */ - private $process; - - /** @var string command output */ - private $output; - - /** @var int command exit code */ - private $exitCode; - - /** @var string */ - private $stderr; - - /** - * @var LoggerInterface - */ - private $logger; + private Process $process; + private ?string $output = null; + private int $exitCode; + private ?string $stderr = null; + private ?LoggerInterface $logger = null; /** * @param string $command @@ -154,10 +144,10 @@ public function runNonBlocking(): int $pid = yield $this->process->start(); - $this->process->getStdout()->read()->onResolve(function (\Throwable $reason = null, $value) { + $this->process->getStdout()->read()->onResolve(function (Throwable $reason = null, $value) { $this->output .= $value; }); - $this->process->getStderr()->read()->onResolve(function (\Throwable $reason = null, $value) { + $this->process->getStderr()->read()->onResolve(function (Throwable $reason = null, $value) { $this->output .= $value; }); @@ -178,7 +168,7 @@ public function runNonBlocking(): int * * @return int Process Id */ - public function run($blocking = false): int + public function run(bool $blocking = false): int { return $blocking ? $this->runBlocking() diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php index b3b98fed6..73d21d4a3 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpService.php @@ -2,9 +2,11 @@ namespace PhpPact\Standalone\StubService\Service; +use GuzzleHttp\Psr7\Uri; use PhpPact\Exception\ConnectionException; use PhpPact\Http\ClientInterface; use PhpPact\Standalone\StubService\StubServerConfigInterface; +use Psr\Http\Message\UriInterface; /** * Http Service that interacts with the Ruby Standalone Stub Server. @@ -14,15 +16,8 @@ */ class StubServerHttpService implements StubServerHttpServiceInterface { - /** - * @var ClientInterface - */ - private $client; - - /** - * @var StubServerConfigInterface - */ - private $config; + private ClientInterface $client; + private StubServerConfigInterface $config; /** * StubServerHttpService constructor. @@ -41,9 +36,7 @@ public function __construct(ClientInterface $client, StubServerConfigInterface $ */ public function healthCheck(): bool { - $uri = $this->config->getBaseUri()->withPath('/'); - - $response = $this->client->get($uri, [ + $response = $this->client->get($this->getBaseUri(), [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Pact-Mock-Service' => true, @@ -63,9 +56,9 @@ public function healthCheck(): bool /** * {@inheritdoc} */ - public function getJson(): string + public function getJson(string $endpoint): string { - $uri = $this->config->getBaseUri()->withPath('/' . $this->config->getEndpoint()); + $uri = $this->getBaseUri()->withPath('/' . $endpoint); $response = $this->client->get($uri, [ 'headers' => [ 'Content-Type' => 'application/json', @@ -74,4 +67,12 @@ public function getJson(): string return \json_encode(\json_decode($response->getBody()->getContents())); } + + /** + * @return UriInterface + */ + private function getBaseUri(): UriInterface + { + return new Uri('http://localhost:' . $this->config->getPort()); + } } diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php index 2b8679afc..e9eed428a 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php @@ -21,7 +21,9 @@ public function healthCheck(): bool; /** * Get the current state of the PACT JSON file and write it to disk. * + * @param string $endpoint + * * @return string */ - public function getJson(): string; + public function getJson(string $endpoint): string; } diff --git a/src/PhpPact/Standalone/StubService/StubServer.php b/src/PhpPact/Standalone/StubService/StubServer.php index 0701fce13..ea0097404 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -2,6 +2,7 @@ namespace PhpPact\Standalone\StubService; +use Amp\Process\ProcessException; use Exception; use PhpPact\Standalone\Installer\InstallManager; use PhpPact\Standalone\Installer\Service\InstallerInterface; @@ -13,14 +14,9 @@ */ class StubServer { - /** @var StubServerConfigInterface */ - private $config; - - /** @var InstallManager */ - private $installManager; - - /** @var ProcessRunner */ - private $processRunner; + private StubServerConfigInterface $config; + private InstallManager $installManager; + private ProcessRunner $processRunner; public function __construct(StubServerConfigInterface $config) { @@ -37,11 +33,9 @@ public function __construct(StubServerConfigInterface $config) * * @return int process ID of the started Stub Server */ - public function start($wait = 1): int + public function start(int $wait = 1): int { - $scripts = $this->installManager->install(); - - $this->processRunner = new ProcessRunner($scripts->getStubService(), $this->getArguments()); + $this->processRunner = new ProcessRunner($this->installManager->install()->getStubService(), $this->getArguments()); $processId = $this->processRunner->run(); \sleep($wait); // wait for server to start @@ -52,6 +46,8 @@ public function start($wait = 1): int /** * Stop the Stub Server process. * + * @throws ProcessException + * * @return bool Was stopping successful? */ public function stop(): bool @@ -82,12 +78,64 @@ private function getArguments(): array { $results = []; - $results[] = $this->config->getPactLocation(); - $results[] = "--host={$this->config->getHost()}"; - $results[] = "--port={$this->config->getPort()}"; + if ($this->config->getBrokerUrl() !== null) { + $results[] = "--broker-url={$this->config->getBrokerUrl()}"; + } + + foreach ($this->config->getDirs() as $dir) { + $results[] = "--dir={$dir}"; + } + + if ($this->config->getExtension() !== null) { + $results[] = "--extension={$this->config->getExtension()}"; + } + + foreach ($this->config->getFiles() as $file) { + $results[] = "--file={$file}"; + } + + if ($this->config->getLogLevel() !== null) { + $results[] = "--loglevel={$this->config->getLogLevel()}"; + } + + if ($this->config->getPort() !== null) { + $results[] = "--port={$this->config->getPort()}"; + } + + if ($this->config->getProviderState() !== null) { + $results[] = "--provider-state={$this->config->getProviderState()}"; + } + + if ($this->config->getProviderStateHeaderName() !== null) { + $results[] = "--provider-state-header-name={$this->config->getProviderStateHeaderName()}"; + } + + if ($this->config->getToken() !== null) { + $results[] = "--token={$this->config->getToken()}"; + } + + foreach ($this->config->getUrls() as $url) { + $results[] = "--url={$url}"; + } + + if ($this->config->getUser() !== null) { + $results[] = "--user={$this->config->getUser()}"; + } + + if ($this->config->isCors()) { + $results[] = '--cors'; + } + + if ($this->config->isCorsReferer()) { + $results[] = '--cors-referer'; + } + + if ($this->config->isEmptyProviderState()) { + $results[] = '--empty-provider-state'; + } - if ($this->config->getLog() !== null) { - $results[] = "--log={$this->config->getLog()}"; + if ($this->config->isInsecureTls()) { + $results[] = '--insecure-tls'; } return $results; diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index 0b179366a..5ca1ba4bb 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -2,7 +2,6 @@ namespace PhpPact\Standalone\StubService; -use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; /** @@ -11,49 +10,101 @@ */ class StubServerConfig implements StubServerConfigInterface { + private ?UriInterface $brokerUrl = null; + private array $dirs = []; + private ?string $extension = null; + private array $files = []; + private ?string $logLevel = null; + private ?int $port = null; + private ?string $providerState = null; + private ?string $providerStateHeaderName = null; + private ?string $token = null; + private array $urls = []; + private ?string $user = null; + + private bool $cors = false; + private bool $corsReferer = false; + private bool $emptyProviderState = false; + private bool $insecureTls = false; + + /** + * {@inheritdoc} + */ + public function getBrokerUrl(): ?UriInterface + { + return $this->brokerUrl; + } + + /** + * {@inheritdoc} + */ + public function setBrokerUrl(UriInterface $brokerUrl): StubServerConfigInterface + { + $this->brokerUrl = $brokerUrl; + + return $this; + } + /** - * Host on which to bind the service. - * - * @var string + * {@inheritdoc} */ - private $host = 'localhost'; + public function setDirs(string ...$dirs): StubServerConfigInterface + { + $this->dirs = $dirs; + + return $this; + } /** - * Port on which to run the service. - * - * @var int + * {@inheritdoc} */ - private $port = 7201; + public function getDirs(): array + { + return $this->dirs; + } + + /** + * {@inheritdoc} + */ + public function getExtension(): ?string + { + return $this->extension; + } /** - * @var bool + * {@inheritdoc} */ - private $secure = false; + public function setExtension(string $extension): StubServerConfigInterface + { + $this->extension = $extension; + + return $this; + } /** - * File to which to log output. - * - * @var string + * {@inheritdoc} */ - private $log; + public function setFiles(string ...$files): StubServerConfigInterface + { + $this->files = $files; - private $pactLocation; - private $endpoint; + return $this; + } /** * {@inheritdoc} */ - public function getHost(): string + public function getFiles(): array { - return $this->host; + return $this->files; } /** * {@inheritdoc} */ - public function setHost(string $host): StubServerConfigInterface + public function setLogLevel(string $logLevel): StubServerConfigInterface { - $this->host = $host; + $this->logLevel = $logLevel; return $this; } @@ -61,7 +112,15 @@ public function setHost(string $host): StubServerConfigInterface /** * {@inheritdoc} */ - public function getPort(): int + public function getLogLevel(): ?string + { + return $this->logLevel; + } + + /** + * {@inheritdoc} + */ + public function getPort(): ?int { return $this->port; } @@ -79,17 +138,89 @@ public function setPort(int $port): StubServerConfigInterface /** * {@inheritdoc} */ - public function isSecure(): bool + public function getProviderState(): ?string + { + return $this->providerState; + } + + /** + * {@inheritdoc} + */ + public function setProviderState(string $providerState): StubServerConfigInterface + { + $this->providerState = $providerState; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getProviderStateHeaderName(): ?string + { + return $this->providerStateHeaderName; + } + + /** + * {@inheritdoc} + */ + public function setProviderStateHeaderName(string $providerStateHeaderName): StubServerConfigInterface + { + $this->providerStateHeaderName = $providerStateHeaderName; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getToken(): ?string + { + return $this->token; + } + + /** + * {@inheritdoc} + */ + public function setToken(?string $token): StubServerConfigInterface + { + $this->token = $token; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setUrls(string ...$urls): StubServerConfigInterface + { + $this->urls = $urls; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getUrls(): array { - return $this->secure; + return $this->urls; } /** * {@inheritdoc} */ - public function setSecure(bool $secure): StubServerConfigInterface + public function getUser(): ?string { - $this->secure = $secure; + return $this->user; + } + + /** + * {@inheritdoc} + */ + public function setUser(string $user): StubServerConfigInterface + { + $this->user = $user; return $this; } @@ -97,51 +228,71 @@ public function setSecure(bool $secure): StubServerConfigInterface /** * {@inheritdoc} */ - public function getBaseUri(): UriInterface + public function isCors(): bool + { + return $this->cors; + } + + /** + * {@inheritdoc} + */ + public function setCors(bool $cors): StubServerConfigInterface { - $protocol = $this->secure ? 'https' : 'http'; + $this->cors = $cors; - return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); + return $this; } /** * {@inheritdoc} */ - public function getLog() + public function isCorsReferer(): bool { - return $this->log; + return $this->corsReferer; } /** * {@inheritdoc} */ - public function setLog(string $log): StubServerConfigInterface + public function setCorsReferer(bool $corsReferer): StubServerConfigInterface { - $this->log = $log; + $this->corsReferer = $corsReferer; return $this; } - public function getPactLocation(): string + /** + * {@inheritdoc} + */ + public function isEmptyProviderState(): bool { - return $this->pactLocation; + return $this->emptyProviderState; } - public function setPactLocation(string $location) + /** + * {@inheritdoc} + */ + public function setEmptyProviderState(bool $emptyProviderState): StubServerConfigInterface { - $this->pactLocation = $location; + $this->emptyProviderState = $emptyProviderState; return $this; } - public function getEndpoint(): string + /** + * {@inheritdoc} + */ + public function isInsecureTls(): bool { - return $this->endpoint; + return $this->insecureTls; } - public function setEndpoint(string $endpoint) + /** + * {@inheritdoc} + */ + public function setInsecureTls(bool $insecureTls): StubServerConfigInterface { - $this->endpoint = $endpoint; + $this->insecureTls = $insecureTls; return $this; } diff --git a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php index 2585fd889..2ccccfdc7 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php @@ -11,63 +11,184 @@ interface StubServerConfigInterface { /** - * @return string the host of the stub service + * @return null|UriInterface url to the pact broker */ - public function getHost(): string; + public function getBrokerUrl(): ?UriInterface; /** - * @param string $host The host of the stub service + * URL of the pact broker to fetch pacts from + * + * @param UriInterface $brokerUrl + * + * @return StubServerConfigInterface + */ + public function setBrokerUrl(UriInterface $brokerUrl): self; + + /** + * @param string ...$dirs Directory of pact files to load * * @return StubServerConfigInterface */ - public function setHost(string $host): self; + public function setDirs(string ...$dirs): self; /** - * @return int the port of the stub service + * @return array */ - public function getPort(): int; + public function getDirs(): array; /** - * @param int $port the port of the stub service + * @return null|string + */ + public function getExtension(): ?string; + + /** + * @param string $extension File extension to use when loading from a directory (default is json) + * + * @return StubServerConfigInterface + */ + public function setExtension(string $extension): self; + + /** + * @param string ...$files Pact file to load + * + * @return StubServerConfigInterface + */ + public function setFiles(string ...$files): self; + + /** + * @return array + */ + public function getFiles(): array; + + /** + * @return null|string + */ + public function getLogLevel(): ?string; + + /** + * @param string $logLevel Log level (defaults to info) [possible values: error, warn, info, debug, trace, none] + * + * @return StubServerConfigInterface + */ + public function setLogLevel(string $logLevel): self; + + /** + * @return null|int the port of the stub service + */ + public function getPort(): ?int; + + /** + * @param int $port Port to run on (defaults to random port assigned by the OS) * * @return StubServerConfigInterface */ public function setPort(int $port): self; /** - * @return bool true if https + * @return null|string state of the provider + */ + public function getProviderState(): ?string; + + /** + * @param string $providerState Provider state regular expression to filter the responses by + * + * @return StubServerConfigInterface + */ + public function setProviderState(string $providerState): self; + + /** + * @return null|string name of the header */ - public function isSecure(): bool; + public function getProviderStateHeaderName(): ?string; /** - * @param bool $secure set to true for https + * @param string $providerStateHeaderName Name of the header parameter containing the provider state to be used in case multiple matching interactions are found * * @return StubServerConfigInterface */ - public function setSecure(bool $secure): self; + public function setProviderStateHeaderName(string $providerStateHeaderName): self; /** - * @return UriInterface + * @return null|string token for the pact broker */ - public function getBaseUri(): UriInterface; + public function getToken(): ?string; /** - * @return string directory for log output + * @param null|string $brokerToken Bearer token to use when fetching pacts from URLS or Pact Broker + * + * @return StubServerConfigInterface */ - public function getLog(); + public function setToken(?string $brokerToken): self; /** - * @param string $log directory for log output + * @param string ...$urls URL of pact file to fetch * * @return StubServerConfigInterface */ - public function setLog(string $log): self; + public function setUrls(string ...$urls): self; - public function getPactLocation(): string; + /** + * @return array + */ + public function getUrls(): array; - public function setPactLocation(string $location); + /** + * @return null|string user and password + */ + public function getUser(): ?string; + + /** + * @param string $user User and password to use when fetching pacts from URLS or Pact Broker in user:password form + * + * @return StubServerConfigInterface + */ + public function setUser(string $user): self; - public function getEndpoint(): string; + /** + * @return bool + */ + public function isCors(): bool; + + /** + * @param bool $cors + * + * @return StubServerConfigInterface + */ + public function setCors(bool $cors): self; + + /** + * @return bool + */ + public function isCorsReferer(): bool; - public function setEndpoint(string $location); + /** + * @param bool $corsReferer + * + * @return StubServerConfigInterface + */ + public function setCorsReferer(bool $corsReferer): self; + + /** + * @return bool + */ + public function isEmptyProviderState(): bool; + + /** + * @param bool $emptyProviderState + * + * @return StubServerConfigInterface + */ + public function setEmptyProviderState(bool $emptyProviderState): self; + + /** + * @return bool + */ + public function isInsecureTls(): bool; + + /** + * @param bool $insecureTls + * + * @return StubServerConfigInterface + */ + public function setInsecureTls(bool $insecureTls): self; } diff --git a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php b/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php deleted file mode 100644 index 7c510e794..000000000 --- a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php +++ /dev/null @@ -1,53 +0,0 @@ - [ - 'pacts' => [ - ['href' => 'pact-url-1'], - ['href' => 'pact-url-2'], - ], - ], - ] - ); - - $streamMock = $this->createMock(StreamInterface::class); - $streamMock->expects($this->once()) - ->method('getContents') - ->will($this->returnValue($expectedContents)); - - $responseMock = $this->createMock(ResponseInterface::class); - $responseMock->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($streamMock)); - - $httpClientMock = $this->createMock(ClientInterface::class); - $httpClientMock->expects($this->once()) - ->method('get') - ->will($this->returnValue($responseMock)); - - $uriMock = $this->createMock(UriInterface::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($this->equalTo($expectedPath)) - ->will($this->returnValue($uriMock)); - - $broker = new BrokerHttpClient($httpClientMock, $uriMock); - $broker->getAllConsumerUrls($provider); - } -} diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index 06567a76e..ffeea5858 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -2,47 +2,44 @@ namespace PhpPact\Consumer; +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Uri; +use PhpPact\Consumer\Exception\MockServerNotStartedException; 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 PhpPact\Standalone\MockService\MockServerConfig; use PHPUnit\Framework\TestCase; class InteractionBuilderTest extends TestCase { - /** @var MockServerHttpServiceInterface */ - private $service; - - /** @var MockServer */ - private $mockServer; + private InteractionBuilder $builder; + private string $consumer = 'test-consumer'; + private string $provider = 'test-provider'; + private string $dir = __DIR__ . '/../../_output'; + private Client $httpClient; /** - * @throws MissingEnvVariableException - * @throws \Exception + * @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(); + $config = new MockServerConfig(); + $config->setProvider($this->provider); + $config->setConsumer($this->consumer); + $config->setPactDir($this->dir); + $config->setPactSpecificationVersion('3.0.0'); + $this->builder = new InteractionBuilder($config); + $this->httpClient = new Client(); } /** - * @throws MissingEnvVariableException - * @throws \Exception + * @throws Exception + * @throws GuzzleException */ - public function testSimpleGet() + public function testMultipleInteractions(): void { $matcher = new Matcher(); @@ -61,21 +58,14 @@ public function testSimpleGet() ]) ->addHeader('Content-Type', 'application/json'); - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') - ->uponReceiving('A test response.') + // Simple get request. + $this->builder + ->newInteraction() + ->given('A single item.') + ->uponReceiving('A simple get request.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); - } - - /** - * @throws MissingEnvVariableException - */ - public function testPostWithBody() - { $request = new ConsumerRequest(); $request ->setPath('/something') @@ -93,26 +83,43 @@ public function testPostWithBody() $response = new ProviderResponse(); $response - ->setStatus(200) + ->setStatus(201) ->addHeader('Content-Type', 'application/json') ->setBody([ 'message' => 'Hello, world!', ]); - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') - ->uponReceiving('A test response.') + // Post request with body. + $this->builder + ->newInteraction() + ->given('It does not matter.') + ->uponReceiving('A post request with body.') ->with($request) - ->willRespondWith($response); + ->willRespondWith($response) + ->createMockServer(); + + $this->httpClient->post(new Uri("{$this->builder->getBaseUri()}/something"), [ + 'headers' => ['Content-Type' => 'application/json'], + 'json' => [ + 'someStuff' => 'someThingElse', + 'someNumber' => 11, + 'anArray' => [ + 'some words', + 'some other words here', + 99.99, + ], + ], + 'http_errors' => false, + ]); - $this->assertTrue($result); + $this->assertFalse($this->builder->verify()); } /** - * @throws MissingEnvVariableException + * @throws Exception + * @throws GuzzleException */ - public function testBuildWithEachLikeMatcher() + public function testSingleInteraction(): void { $matcher = new Matcher(); @@ -120,7 +127,10 @@ public function testBuildWithEachLikeMatcher() $request ->setPath('/something') ->setMethod('GET') - ->addHeader('Content-Type', 'application/json'); + ->addHeader('Content-Type', 'application/json') + ->addQueryParameter('test', 1) + ->addQueryParameter('another[]', 2) + ->addQueryParameter('another[]', 33); $response = new ProviderResponse(); $response @@ -133,13 +143,51 @@ public function testBuildWithEachLikeMatcher() ]), ]); - $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') - ->uponReceiving('A test response.') + $this->builder + ->newInteraction() + ->given('A list of items.') + ->uponReceiving('A get request with list in body.') ->with($request) - ->willRespondWith($response); + ->willRespondWith($response) + ->createMockServer(); + + $this->httpClient->get(new Uri("{$this->builder->getBaseUri()}/something?test=1&another[]=2&another[]=33"), [ + 'headers' => ['Content-Type' => 'application/json'], + 'http_errors' => false, + ]); + + $this->assertTrue($this->builder->verify()); + } + + public function testGetBaseUriWhenMockServerNotStarted(): void + { + $this->expectException(MockServerNotStartedException::class); + $this->expectExceptionMessage('Mock server is not started.'); + $this->builder->getBaseUri(); + } - $this->assertTrue($result); + public function testVerifyWhenMockServerNotStarted(): void + { + $this->expectException(MockServerNotStartedException::class); + $this->expectExceptionMessage('Mock server is not started.'); + $this->builder->verify(); + } + + public function testGetBaseUriWhenMockServerStopped(): void + { + $this->builder->createMockServer(); + $this->builder->verify(); + $this->expectException(MockServerNotStartedException::class); + $this->expectExceptionMessage('Mock server is not started.'); + $this->builder->getBaseUri(); + } + + public function testVerifyWhenMockServerStopped(): void + { + $this->builder->createMockServer(); + $this->builder->verify(); + $this->expectException(MockServerNotStartedException::class); + $this->expectExceptionMessage('Mock server is not started.'); + $this->builder->verify(); } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 63d801902..f534bba3b 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -7,8 +7,7 @@ class MatcherTest extends TestCase { - /** @var Matcher */ - private $matcher; + private Matcher $matcher; protected function setUp(): void { diff --git a/tests/PhpPact/Consumer/MessageBuilderTest.php b/tests/PhpPact/Consumer/MessageBuilderTest.php new file mode 100644 index 000000000..bdd0a59d2 --- /dev/null +++ b/tests/PhpPact/Consumer/MessageBuilderTest.php @@ -0,0 +1,52 @@ +setProvider($this->provider); + $config->setConsumer($this->consumer); + $config->setPactDir($this->dir); + $config->setPactSpecificationVersion('3.0.0'); + $this->builder = new MessageBuilder($config); + } + + /** + * @throws Exception + */ + public function testMessage() + { + $contents = new \stdClass(); + $contents->song = 'And the wind whispers Mary'; + + $metadata = ['queue' => 'And the clowns have all gone to bed', 'routing_key' => 'And the clowns have all gone to bed']; + + $this->builder + ->given('You can hear happiness staggering on down the street') + ->expectsToReceive('footprints dressed in red') + ->withMetadata($metadata) + ->withContent($contents); + + $this->builder->setCallback(function (string $message) use ($contents) { + $obj = \json_decode($message); + $this->assertEquals($contents, $obj->contents); + }); + + $this->assertTrue($this->builder->verify()); + } +} diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 71ce76e31..d866ec918 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -17,11 +17,9 @@ 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()); } } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 17291378e..69918480b 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 deleted file mode 100644 index 18e27893d..000000000 --- a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -setHost($host) - ->setPort($port) - ->setProvider($provider) - ->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()); - 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()); - } -} diff --git a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php index 91efbfaed..daf251d5d 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php @@ -1,6 +1,6 @@ setHost($host) - ->setPort($port) ->setProvider($provider) ->setConsumer($consumer) ->setPactDir($pactDir) - ->setPactFileWriteMode($pactFileWriteMode) - ->setLog($log) - ->setPactSpecificationVersion($pactSpecificationVersion) - ->setCors($cors); + ->setPactSpecificationVersion($pactSpecificationVersion); - static::assertSame($host, $subject->getHost()); - static::assertSame($port, $subject->getPort()); static::assertSame($provider, $subject->getProvider()); 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()); } } diff --git a/tests/PhpPact/Standalone/MockServer/MockServerTest.php b/tests/PhpPact/Standalone/MockServer/MockServerTest.php deleted file mode 100644 index fe63811a6..000000000 --- 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 cfb86df6c..000000000 --- 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/ProviderVerifier/VerifierProcessTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php deleted file mode 100644 index 6531e0988..000000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php +++ /dev/null @@ -1,112 +0,0 @@ - 'bar']; - - $scripts = $this->createMock(Scripts::class); - $scripts->expects($this->once()) - ->method('getProviderVerifier') - ->will($this->returnValue($verifier)); - - $logger = $this->createMock(LoggerInterface::class); - - $processRunner = $this->createMock(ProcessRunner::class); - - $installManager = $this->createMock(InstallManager::class); - $installManager->expects($this->once()) - ->method('install') - ->will($this->returnValue($scripts)); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($verifier), $this->equalTo($arguments), $this->equalTo($logger)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($installManager, $processRunnerFactory); - $process->setLogger($logger); - $process->run($arguments, 42, 23); - } - - public function testRunWithDefaultLogger() - { - $verifier = 'foo'; - $arguments = ['foo' => 'bar']; - - $scripts = $this->createMock(Scripts::class); - $scripts->expects($this->once()) - ->method('getProviderVerifier') - ->will($this->returnValue($verifier)); - - $processRunner = $this->createMock(ProcessRunner::class); - - $installManager = $this->createMock(InstallManager::class); - $installManager->expects($this->once()) - ->method('install') - ->will($this->returnValue($scripts)); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($verifier), $this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($installManager, $processRunnerFactory); - $process->run($arguments, 42, 23); - } - - public function testRunForwardsException() - { - $this->expectExceptionMessage('foo'); - $this->expectException(\RuntimeException::class); - - $verifier = 'foo'; - $arguments = ['foo' => 'bar']; - - $expectedException = new \RuntimeException('foo'); - - $scripts = $this->createMock(Scripts::class); - $scripts->expects($this->once()) - ->method('getProviderVerifier') - ->will($this->returnValue($verifier)); - - $processRunner = $this->createMock(ProcessRunner::class); - $processRunner->expects($this->once()) - ->method('runBlocking') - ->will( - $this->returnCallback( - function () use ($expectedException) { - throw $expectedException; - } - ) - ); - - $installManager = $this->createMock(InstallManager::class); - $installManager->expects($this->once()) - ->method('install') - ->will($this->returnValue($scripts)); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($verifier), $this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($installManager, $processRunnerFactory); - $process->run($arguments, 42, 23); - } -} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 9fad3fbf9..d51154a39 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -3,222 +3,129 @@ namespace PhpPact\Standalone\ProviderVerifier; use GuzzleHttp\Psr7\Uri; -use Monolog\Handler\TestHandler; -use Monolog\Logger; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Standalone\Installer\InstallManager; -use PhpPact\Standalone\Installer\Model\Scripts; +use PhpPact\Standalone\Installer\Exception\FileDownloadFailureException; +use PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException; +use PhpPact\Standalone\ProviderVerifier\Model\ConsumerVersionSelectors; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; +use Symfony\Component\Process\Process; class VerifierTest extends TestCase { - public function testGetArguments() + /** + * @throws NoDownloaderFoundException + * @throws FileDownloadFailureException + */ + public function testGetArguments(): void { + $selectors = new ConsumerVersionSelectors(); + $selectors + ->add('new-feature', true, null, 'master') + ->add('test') + ->add('production') + ->add('production', false, 'my-mobile-consumer'); $config = new VerifierConfig(); $config + ->setScheme('http') + ->setHost('myprovider') + ->setPort(1234) + ->setBasePath('/api') + ->setStateChangeUrl(new Uri('http://someurl:1234')) ->setProviderName('someProvider') ->setProviderVersion('1.0.0') - ->addProviderVersionTag('prod') - ->addProviderVersionTag('dev') - ->addConsumerVersionTag('dev') - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setPublishResults(true) + ->setProviderTags('prod,dev') + ->setConsumerVersionTags('dev') + ->setBrokerUrl(new Uri('https://testdemo.pactflow.io')) ->setBrokerToken('someToken') ->setBrokerUsername('someusername') ->setBrokerPassword('somepassword') - ->addCustomProviderHeader('key1', 'value1') - ->addCustomProviderHeader('key2', 'value2') - ->setVerbose(true) - ->setFormat('someformat') - ->setProcessTimeout(30) - ->setProcessIdleTimeout(5) + ->setIncludeWipPactsSince('2020-01-30') + ->setBuildUrl(new Uri('http://build.domain.com')) + ->setConsumerVersionSelectors($selectors) + ->setLogLevel('info') + ->setRequestTimeout(500) + ->setUrls('http://example.com/consumer1-provider1.json', 'http://example.com/consumer2-provider1.json') + ->setDirs('/path/to/pacts', '/another/path/to/pacts') + ->setFiles('consumer1-provider2.json', 'consumer2-provider2.json') + ->setFilterConsumerNames('consumer1', 'consumer2') + ->setFilterDescription('Send POST to create') + ->setFilterNoState('state1') + ->setFilterState('state2') + ->setPublish(true) + ->setDisableSslVerification(true) ->setEnablePending(true) - ->setIncludeWipPactSince('2020-01-30') - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ); - - /** @var BrokerHttpClientInterface $brokerHttpService */ - $server = new Verifier($config); - $arguments = $server->getArguments(); - - $this->assertContains('--provider-base-url=http://myprovider:1234', $arguments); - $this->assertContains('--provider-states-setup-url=http://someurl:1234', $arguments); - $this->assertContains('--publish-verification-results', $arguments); - $this->assertContains('--broker-token=someToken', $arguments); - $this->assertContains('--broker-username=someusername', $arguments); - $this->assertContains('--broker-password=somepassword', $arguments); - $this->assertContains('--custom-provider-header="key1: value1"', $arguments); - $this->assertContains('--custom-provider-header="key2: value2"', $arguments); - $this->assertContains('--verbose', $arguments); - $this->assertContains('--format=someformat', $arguments); - $this->assertContains('--provider-version-tag=prod', $arguments); - $this->assertContains('--provider-version-tag=dev', $arguments); - $this->assertContains('--consumer-version-tag=dev', $arguments); - $this->assertSame(['process_timeout' => 30, 'process_idle_timeout' => 5], $server->getTimeoutValues()); - $this->assertContains('--enable-pending', $arguments); + ->setStateChangeAsQuery(true) + ->setStateChangeTeardown(true); + + $verifier = new Verifier($config); + $arguments = $verifier->getArguments(); + + $this->assertContains('--scheme=http', $arguments); + $this->assertContains('--hostname=myprovider', $arguments); + $this->assertContains('--port=1234', $arguments); + $this->assertContains('--base-path=/api', $arguments); + $this->assertContains('--state-change-url=http://someurl:1234', $arguments); + $this->assertContains('--provider-name=someProvider', $arguments); + $this->assertContains('--provider-version=1.0.0', $arguments); + $this->assertContains('--provider-tags=prod,dev', $arguments); + $this->assertContains('--consumer-version-tags=dev', $arguments); + $this->assertContains('--broker-url=https://testdemo.pactflow.io', $arguments); + $this->assertContains('--token=someToken', $arguments); + $this->assertContains('--user=someusername', $arguments); + $this->assertContains('--password=somepassword', $arguments); $this->assertContains('--include-wip-pacts-since=2020-01-30', $arguments); - } - - public function testGetArgumentsEmptyConfig() - { - $this->assertEmpty((new Verifier(new VerifierConfig()))->getArguments()); + $this->assertContains('--build-url=http://build.domain.com', $arguments); + $this->assertContains('--consumer-version-selectors="[{\"tag\":\"new-feature\",\"latest\":true,\"fallbackTag\":\"master\"},{\"tag\":\"test\",\"latest\":true},{\"tag\":\"production\",\"latest\":true},{\"tag\":\"production\",\"consumer\":\"my-mobile-consumer\"}]"', $arguments); + $this->assertContains('--loglevel=info', $arguments); + $this->assertContains('--request-timeout=500', $arguments); + $this->assertContains('--url=http://example.com/consumer1-provider1.json', $arguments); + $this->assertContains('--url=http://example.com/consumer2-provider1.json', $arguments); + $this->assertContains('--dir=/path/to/pacts', $arguments); + $this->assertContains('--dir=/another/path/to/pacts', $arguments); + $this->assertContains('--file=consumer1-provider2.json', $arguments); + $this->assertContains('--file=consumer2-provider2.json', $arguments); + $this->assertContains('--filter-consumer=consumer1', $arguments); + $this->assertContains('--filter-consumer=consumer2', $arguments); + $this->assertContains('--filter-description="Send POST to create"', $arguments); + $this->assertContains('--filter-no-state=state1', $arguments); + $this->assertContains('--filter-state=state2', $arguments); + $this->assertContains('--disable-ssl-verification', $arguments); + $this->assertContains('--enable-pending', $arguments); + $this->assertContains('--publish', $arguments); + $this->assertContains('--state-change-as-query', $arguments); + $this->assertContains('--state-change-teardown', $arguments); } /** - * @dataProvider dataProviderForBrokerPathTest - * - * @param string $consumerName - * @param string $providerName - * @param null|string $tag - * @param null|string $version - * @param string $path - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException + * @throws NoDownloaderFoundException + * @throws FileDownloadFailureException */ - public function testBuildValidPathToPactBroker($consumerName, $providerName, $tag, $version, $path) - { - $expectedUrltoBroker = 'http://mock/' . $path; - - /** @var Uri $uriMock */ - $uriMock = $this->createMock(Uri::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($path) - ->willReturn($uriMock); - - $uriMock->expects($this->once()) - ->method('__toString') - ->willReturn($expectedUrltoBroker); - - $installerMock = $this->createMock(InstallManager::class); - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrltoBroker) { - return \in_array($expectedUrltoBroker, $args); - }) - ); - - $config = new VerifierConfig(); - $config->setProviderName($providerName) - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setBrokerUri($uriMock) - ->setVerbose(true); - - $verifier = new Verifier($config, $installerMock, $verifierProcessMock); - - $verifier->verify($consumerName, $tag, $version); - } - - public function dataProviderForBrokerPathTest() + public function testGetArgumentsEmptyConfig(): void { - $consumerName = 'someProviderName'; - $providerName = 'someProviderName'; - $tag = '1.0.0'; - $version = '11111'; - - return [ - [$consumerName, $providerName, null, $version, "pacts/provider/$providerName/consumer/$consumerName/version/$version/"], - [$consumerName, $providerName, $tag, null, "pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, $tag, $version, "pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, null, null, "pacts/provider/$providerName/consumer/$consumerName/latest/"], - ]; + $this->assertEmpty((new Verifier(new VerifierConfig()))->getArguments()); } /** - * @dataProvider provideDataForVerifyAll - * - * @param string $providerName - * @param string $providerVersion - * @param bool $forceLatest - * @param mixed $expectedProviderVersion - * - * @throws \PhpPact\Standalone\Installer\Exception\FileDownloadFailureException - * @throws \PhpPact\Standalone\Installer\Exception\NoDownloaderFoundException + * @throws NoDownloaderFoundException + * @throws FileDownloadFailureException */ - public function testIfDataForVerifyAllIsConvertedCorrectly($providerName, $providerVersion) + public function testVerify(): void { - $expectedUrl1 = 'expectedUrl1'; - $expectedUrl2 = 'expectedUrl2'; - $expectedPactUrls = [$expectedUrl1, $expectedUrl2]; - - $installerMock = $this->createMock(InstallManager::class); - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrl1, $expectedUrl2) { - return \in_array($expectedUrl1, $args) && \in_array($expectedUrl2, $args); - }) - ); - - $brokerHttpClient = $this->createMock(BrokerHttpClient::class); - - $brokerHttpClient->expects($this->once()) - ->method('getAllConsumerUrls') - ->with($this->equalTo($providerName)) - ->willReturn($expectedPactUrls); + $provider = new Process(['php', '-S', 'localhost:8000', '-t', __DIR__ . '/../../../_public']); + $provider->start(); + $provider->waitUntil(function ($type, $output) { + return false !== \strpos($output, 'Development Server (http://localhost:8000) started'); + }); $config = new VerifierConfig(); - $config->setProviderName($providerName); - $config->setProviderVersion($providerVersion); - - $verifier = new Verifier($config, $installerMock, $verifierProcessMock, $brokerHttpClient); - $verifier->verifyAll(); - } - - public function provideDataForVerifyAll() - { - return [ - ['someProvider', '1.0.0'], - ['someProvider', '1.2.3'], - ]; - } - - public function testRunShouldLogOutputIfCmdFails() - { - if ('\\' !== \DIRECTORY_SEPARATOR) { - $cmd = __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.sh'; - } else { - $cmd = 'cmd /c' . __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.bat'; - } - - $scriptsMock = $this->createMock(Scripts::class); - $scriptsMock->method('getProviderVerifier')->willReturn($cmd); - - $installerMock = $this->createMock(InstallManager::class); - $installerMock->method('install')->willReturn($scriptsMock); - - $process = new VerifierProcess($installerMock); - - $logger = new Logger('console', [$handler = new TestHandler()]); - $process->setLogger($logger); - - try { - $exception = null; - $process->run([], 60, 10); - } catch (\Exception $e) { - $exception = $e; - } - - $logMessages = $handler->getRecords(); + $config + ->setDirs(__DIR__ . '/../../../_resources') + ->setPort(8000) + ->setProviderName('someProvider') + ->setProviderVersion('1.0.0'); - $this->assertGreaterThan(2, \count($logMessages)); - $this->assertStringContainsString('first line', $logMessages[\count($logMessages) - 2]['message']); - $this->assertStringContainsString('second line', $logMessages[\count($logMessages) - 1]['message']); + $verifier = new Verifier($config); - $this->assertNotNull($exception); + $this->assertTrue($verifier->verify()); } } diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat b/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat deleted file mode 100755 index 4cdede33b..000000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat +++ /dev/null @@ -1,8 +0,0 @@ -@ECHO OFF - -REM this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -ECHO "first line" -ECHO "second line" 1>&2 - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh b/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh deleted file mode 100755 index c196300aa..000000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -echoerr() { echo "$@" 1>&2; } - -echo "first line" -echoerr "second line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php deleted file mode 100644 index 536138001..000000000 --- a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php +++ /dev/null @@ -1,28 +0,0 @@ -setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setLog($log); - - static::assertSame($pactLocation, $subject->getPactLocation()); - static::assertSame($host, $subject->getHost()); - static::assertSame($port, $subject->getPort()); - static::assertSame($log, $subject->getLog()); - } -} diff --git a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php b/tests/PhpPact/Standalone/StubService/Service/StubServerHttpServiceTest.php similarity index 68% rename from tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php rename to tests/PhpPact/Standalone/StubService/Service/StubServerHttpServiceTest.php index 4e029ecef..e9d3ee325 100644 --- a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php +++ b/tests/PhpPact/Standalone/StubService/Service/StubServerHttpServiceTest.php @@ -11,14 +11,9 @@ class StubServerHttpServiceTest extends TestCase { - /** @var StubServerHttpServiceInterface */ - private $service; - - /** @var StubServer */ - private $stubServer; - - /** @var StubServerConfigInterface */ - private $config; + private StubServerHttpServiceInterface $service; + private StubServer $stubServer; + private StubServerConfigInterface $config; /** * @throws MissingEnvVariableException @@ -27,18 +22,14 @@ class StubServerHttpServiceTest extends TestCase protected function setUp(): void { $pactLocation = __DIR__ . '/../../../../_resources/someconsumer-someprovider.json'; - $host = 'localhost'; $port = 7201; - $endpoint = 'test'; $this->config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($pactLocation) + ->setPort($port); $this->stubServer = new StubServer($this->config); - $this->stubServer->start(10); + $this->stubServer->start(); $this->service = new StubServerHttpService(new GuzzleClient(), $this->config); } @@ -49,7 +40,7 @@ protected function tearDown(): void public function testGetJson() { - $result = $this->service->getJson(); + $result = $this->service->getJson('test'); $this->assertEquals('{"results":[{"name":"Games"}]}', $result); } } diff --git a/tests/PhpPact/Standalone/StubService/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubService/StubServerConfigTest.php new file mode 100644 index 000000000..2becb556c --- /dev/null +++ b/tests/PhpPact/Standalone/StubService/StubServerConfigTest.php @@ -0,0 +1,24 @@ +setDirs($pactLocation) + ->setPort($port) + ->setLogLevel($logLevel); + + static::assertSame([$pactLocation], $subject->getDirs()); + static::assertSame($port, $subject->getPort()); + static::assertSame($logLevel, $subject->getLogLevel()); + } +} diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubService/StubServerTest.php similarity index 63% rename from tests/PhpPact/Standalone/StubServer/StubServerTest.php rename to tests/PhpPact/Standalone/StubService/StubServerTest.php index cf12606d7..21db9c29c 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubService/StubServerTest.php @@ -1,9 +1,7 @@ setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($pactLocation) + ->setPort($port); $stubServer = new StubServer($subject); $pid = $stubServer->start(); diff --git a/tests/PhpPact/php.ini b/tests/PhpPact/php.ini index 243c4ea65..a62e7fec8 100644 --- a/tests/PhpPact/php.ini +++ b/tests/PhpPact/php.ini @@ -51,6 +51,7 @@ extension=php_openssl.dll extension=fileinfo extension=intl extension=php_sockets.dll +extension=php_ffi.dll [CLI Server] cli_server.color = On diff --git a/tests/_output/.gitignore b/tests/_output/.gitignore new file mode 100644 index 000000000..a6c57f5fb --- /dev/null +++ b/tests/_output/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/tests/_public/index.php b/tests/_public/index.php new file mode 100644 index 000000000..cc71ddccc --- /dev/null +++ b/tests/_public/index.php @@ -0,0 +1,10 @@ + [ + [ + 'name' => 'g', + ], + ], +]);