diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 00000000..dde4e49c --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,52 @@ +BUILD_TEST_TASK_TEMPLATE: &BUILD_TEST_TASK_TEMPLATE + arch_check_script: + - uname -am + install_script: + - composer install + cirrus_ci_macos_local_script: | + if [ "$(uname -s)" == 'Darwin' ] && [ "$CIRRUS_CLI" == 'true' ]; then + chmod +x vendor/bin/php-cs-fixer + chmod +x vendor/bin/phpstan + chmod +x vendor/bin/phpunit + chmod +x bin/pact-stub-server/pact-stub-server + fi + lint_script: + - composer run lint + static_analysis_script: + - composer run static-code-analysis + test_script: + - composer test + +linux_arm64_task: + env: + COMPOSER_ALLOW_SUPERUSER: 1 + matrix: + - VERSION: 8.2 + - VERSION: 8.1 + - VERSION: 8.0 + container: + image: php:$VERSION + pre_req_script: + - apt update --yes && apt install --yes zip unzip git libffi-dev + - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php + - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer + - docker-php-ext-install sockets + - docker-php-ext-install ffi + version_check_script: + - php --version + << : *BUILD_TEST_TASK_TEMPLATE + +macos_arm64_task: +# https://www.markhesketh.com/switching-multiple-php-versions-on-macos/ + env: + matrix: + - VERSION: 8.2 + - VERSION: 8.1 + - VERSION: 8.0 + macos_instance: + image: ghcr.io/cirruslabs/macos-ventura-base:latest + pre_req_script: + - brew install php@$VERSION composer + version_check_script: + - php --version + << : *BUILD_TEST_TASK_TEMPLATE diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5d94c326 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Dependabot Config + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + # Maintain dependencies for Composer + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a9e172f..35b5c350 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ name: pact-php on: - push: + push: pull_request: # Once on the first of the month at 06:00 UTC schedule: @@ -27,6 +27,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + coverage: none - uses: ramsey/composer-install@v2 with: @@ -48,6 +49,7 @@ jobs: operating-system: [ ubuntu-latest, macos-latest, windows-latest ] php: [ '8.0', '8.1', '8.2' ] dependencies: [ 'lowest', 'locked' ] + timeout-minutes: 5 name: PHP ${{ matrix.php }} on ${{ matrix.operating-system }} with ${{ matrix.dependencies }} dependencies @@ -58,8 +60,9 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - extensions: openssl, sockets, curl, zip + extensions: sockets, curl, zip, ffi php-version: ${{ matrix.php }} + coverage: none - name: Composer install uses: ramsey/composer-install@v2 diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index cac6eabb..cf318188 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -7,7 +7,8 @@ ->name('*.php'); $config = new PhpCsFixer\Config(); -$config->setRules(['@PSR12' => true, +$config->setRules([ + '@PSR12' => true, 'strict_param' => false, 'array_syntax' => ['syntax' => 'short'], ]) diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 00000000..31b4f294 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,57 @@ +# Pact-PHP + +## Pre Reqs + +- PHP 8.x or greater +- FFI and Sockets extensions enabled in your php.ini + +## Steps + +1. Run `composer install` + 1. This will install php dependencies to `vendor` + 2. This will install pact libraries to `bin` +2. Run `composer test` + 1. This will run our unit tests +3. Run `composer lint` + 1. This will run the phpcs-lint +4. Run `composer fix` + 1. This will correct any auto fixable linter errors +5. Run `composer static-code-analysis` + 1. Run static code analysis + +## CI Locally + +### MacOS ARM + +#### Pre Reqs + +- MacOS ARM +- Tart.run +- Cirrus-CLI + +#### Steps + +Run all versions of PHP + +- `cirrus run --output github-actions macos_arm64 -e CIRRUS_CLI=true` + +Run a specified version of PHP + +- `cirrus run --output github-actions 'macos_arm64 VERSION:8.2' -e CIRRUS_CLI=true` + +### Linux ARM + +#### Pre Reqs + +- Docker +- x86_64 or arm64/aarch64 host + +#### Steps + +Run all versions of PHP + +- `cirrus run --output github-actions linux_arm64` + +Run a specified version of PHP + +- `cirrus run --output github-actions 'macos_arm64 VERSION:8.2'` \ No newline at end of file diff --git a/README.md b/README.md index f7230c5f..34e0513d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ 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) - [Create Consumer Unit Test](#create-consumer-unit-test) - [Create Mock Request](#create-mock-request) - [Create Mock Response](#create-mock-response) @@ -25,12 +24,16 @@ Table of contents - [Make the Request](#make-the-request) - [Verify Interactions](#verify-interactions) - [Make Assertions](#make-assertions) + - [Delete Old Pact](#delete-old-pact) + - [Publish Contracts To Pact Broker](#publish-contracts-to-pact-broker) + - [CLI](#cli) + - [Github Actions](#github-actions) - [Basic Provider Usage](#basic-provider-usage) - [Create Unit Test](#create-unit-test) - [Start API](#start-api) - [Provider Verification](#provider-verification) - [Verify From Pact Broker](#verify-from-pact-broker) - - [Verify All from Pact Broker](#verify-all-from-pact-broker) + - [Verify Files in Directory](#verify-files-in-directory) - [Verify Files by Path](#verify-files-by-path) - [Tips](#tips) - [Starting API Asynchronously](#starting-api-asynchronously) @@ -42,7 +45,10 @@ Table of contents - [Usage for the optional `pact-stub-service`](#usage-for-the-optional-pact-stub-service) ## Versions -9.X updates internal dependencies and libraries. This results in dropping PHP 7.4 + +10.X updates internal dependencies and libraries + adds support for pact specification 3.X & 4.X via Pact FFI. + +9.X updates internal dependencies and libraries including pact-ruby-standalone v2.x which adds support for ARM64 CPU's for Linux/MacOS and providing x86 and x86_64 Windows via pact-ruby-standalone v2.x. This results in dropping PHP 7.4 8.X updates internal dependencies and libraries. This results in dropping PHP 7.3 @@ -59,7 +65,24 @@ 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 -> 9.X supports [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2). +Pact-Php 10.X supports: + * [Pact-Specification 2.X](https://github.com/pact-foundation/pact-specification/tree/version-2) + * [Pact-Specification 3.X](https://github.com/pact-foundation/pact-specification/tree/version-3). + * [Pact-Specification 4.X](https://github.com/pact-foundation/pact-specification/tree/version-4). + +##  Supported Platforms + +| OS | Architecture | Supported | Pact-PHP Version | +| ------- | ------------ | --------- | ---------------- | +| OSX | x86_64 | ✅ | All | +| Linux | x86_64 | ✅ | All | +| OSX | arm64 | ✅ | 9.x + | +| Linux | arm64 | ✅ | 9.x + | +| Windows | x86_64 | ✅ | All | +| Windows | x86 | ✅ | All | ## Installation @@ -75,39 +98,6 @@ 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 - -This library contains a wrapper for the [Ruby Standalone Mock Service](https://github.com/pact-foundation/pact-mock_service). - -The easiest way to configure this is to use a [PHPUnit Listener](https://phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.test-listeners). A default listener is included in this project, see [PactTestListener.php](/src/PhpPact/Consumer/Listener/PactTestListener.php). This utilizes environmental variables for configurations. These env variables can either be added to the system or to the phpunit.xml configuration file. Here is an example [phpunit.xml](/example/phpunit.consumer.xml) file configured to use the default. Keep in mind that both the test suite and the arguments array must be the same value. - -Alternatively, you can start and stop as in whatever means you would like by following this example: - -```php -setHost('localhost'); - $config->setPort(7200); - $config->setConsumer('someConsumer'); - $config->setProvider('someProvider'); - $config->setCors(true); - - // Instantiate the mock server object with the config. This can be any - // instance of MockServerConfigInterface. - $server = new MockServer($config); - - // Create the process. - $server->start(); - - // Stop the process. - $server->stop(); -``` - ### Create Consumer Unit Test Create a standard PHPUnit test case class and function. @@ -193,10 +183,10 @@ Now that we have the request and response, we need to build the interaction and $config = new MockServerEnvConfig(); $builder = new InteractionBuilder($config); $builder - ->given('a person exists') + ->given('a person exists', ['name' => 'Bob']) ->uponReceiving('a get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. + ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. ``` ### Make the Request @@ -212,7 +202,8 @@ Verify that all interactions took place that were registered. This typically should be in each test, that way the test that failed to verify is marked correctly. ```php -$builder->verify(); +$verifyResult = $verifier->verify(); +$this->assertTrue($verifyResult); ``` ### Make Assertions @@ -223,6 +214,32 @@ Verify that the data you would expect given the response configured is correct. $this->assertEquals('Hello, Bob', $result); // Make your assertions. ``` +### Delete Old Pact + +If the value of `PACT_FILE_WRITE_MODE` is `merge`, before running the test, we need to delete the old pact manually: + +```shell +rm /path/to/pacts/consumer-provider.json +``` + +### Publish Contracts To Pact Broker + +When all tests in test suite are passed, you may want to publish generated contract files to pact broker. + +#### CLI + +Run this command using CLI tool: + +```shell +pact-broker publish /path/to/pacts/consumer-provider.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken +``` + +See more at https://docs.pact.io/pact_broker/publishing_and_retrieving_pacts#publish-using-cli-tools + +#### Github Actions + +See how to use at https://github.com/pactflow/actions/tree/main/publish-pact-files + ## Basic Provider Usage All of the following code will be used exclusively for Providers. This will run the Pacts against the real Provider and either verify or fail validation on the Pact Broker. @@ -250,51 +267,62 @@ $config = new VerifierConfig(); $config ->setProviderName('someProvider') // Providers name to fetch. ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. - ->setProcessTimeout(60) // Set process timeout (optional) - default 60 - ->setProcessIdleTimeout(10) // Set process idle timeout (optional) - default 10 - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ); -// Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. + ->setProviderTags('prod' ,'dev') + ->setProviderBranch('main') + ->setScheme('http') + ->setHost('localhost') + ->setPort(58000) + ->setBasePath('/') + ->setStateChangeUrl(new Uri('http://localhost:58000/change-state')) + ->setBuildUrl(new Uri('http://build.domain.com')) + ->setFilterConsumerNames('someConsumer', 'otherConsumer') + ->setFilterDescription('Send POST to create') + ->setFilterNoState(true) + ->setFilterState('state') + ->setPublishResults(true) + ->setDisableSslVerification(true) + ->setStateChangeAsBody(false) + ->setStateChangeTeardown(true) + ->setRequestTimeout(500); + $verifier = new Verifier($config); -$verifier->verify('someConsumer', 'master'); // The tag is option. If no tag is set it will just grab the latest. -// This will not be reached if the PACT verifier throws an error, otherwise it was successful. -$this->assertTrue(true, 'Pact Verification has failed.'); +$selectors = (new ConsumerVersionSelectors()) + ->addSelector('{"tag":"foo","latest":true}') + ->addSelector('{"tag":"bar","latest":true}'); + +$broker = new Broker(); +$broker + ->setUrl(new Uri('http://localhost')) + ->setUsername('user') + ->setPassword('pass') + ->setToken('token') + ->setEnablePending(true) + ->setIncludeWipPactSince('2020-01-30') + ->setProviderTags(['prod']) + ->setProviderBranch('main') + ->setConsumerVersionSelectors($selectors) + ->setConsumerVersionTags(['dev']); + +$verifier->addBroker($broker); + +$verifyResult = $verifier->verify(); + +$this->assertTrue($verifyResult); ``` -##### Verify All from Pact Broker +##### Verify Files in Directory -This will grab every Pact file associated with the given provider. +This allows local Pact file testing. ```php -public function testPactVerifyAll() +public function testPactVerifyFilesInDirectory() { - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true) // Flag the verifier service to publish the results to the Pact Broker. - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - - // Verify that all consumers of 'someProvider' are valid. - $verifier = new Verifier($config); - $verifier->verifyAll(); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifier->addDirectory('C:\SomePath'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } ``` @@ -303,25 +331,13 @@ public function testPactVerifyAll() This allows local Pact file testing. ```php -public function testPactVerifyAll() +public function testPactVerifyFiles() { - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch name. - ->setProviderBaseUrl(new Uri('http://localhost:58000')) // URL of the Provider. - ->setBrokerUri(new Uri('http://localhost')) // URL of the Pact Broker to publish results. - ->setPublishResults(true); // Flag the verifier service to publish the results to the Pact Broker. - ->setEnablePending(true) // Flag to enable pending pacts feature (check pact docs for further info) - ->setIncludeWipPactSince('2020-01-30') //Start date of WIP Pacts (check pact docs for further info) - - // Verify that the files in the array are valid. - $verifier = new Verifier($config); - $verifier->verifyFiles(['C:\SomePath\consumer-provider.json']); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifier->addFile('C:\SomePath\consumer-provider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } ``` @@ -353,7 +369,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 @@ -393,68 +408,41 @@ $consumerMessage = new ExampleMessageConsumer(); $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); -$builder->verify(); +$verifyResult = $builder->verify(); + +$this->assertTrue($verifyResult); ``` ### Provider Side Message Validation -This may evolve as we work through this implementation. The provider relies heavily on callbacks. -Some of the complexity lies in a consumer and provider having many messages and states between the each other in a single pact. - -For each message, one needs to provide a single provider state. The name of this provider state must be the key to run -a particular message callback on the provider side. See example\tests\MessageProvider - -1. Create your callbacks and states wrapped in a callable object - 1. The array key is a provider state / given() on the consumer side - 1. It is helpful to wrap the whole thing in a lambda if you need to customize paramaters to be passed in -1. Choose your verification method -1. If nothing explodes, #winning - -```php - - $callbacks = array(); +Handle these requests on your provider: - // a hello message is a provider state / given() on the consumer side - $callbacks["a hello message"] = function() { - $content = new \stdClass(); - $content->text ="Hello Mary"; +1. POST /pact-change-state + 1. Set up your database to meet the expectations of the request + 2. Reset the database to its original state. +2. POST /pact-messages + 1. Return message's content in body + 2. Return message's metadata in header `PACT-MESSAGE-METADATA` - $metadata = array(); - $metadata['queue'] = "myKey"; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../output/test_consumer-test_provider.json']); - -``` +[Click here](/example/src/Provider/public/index.php) to see the full sample file. ## Usage for the optional `pact-stub-service` If you would like to test with fixtures, you can use the `pact-stub-service` like this: ```php -$pactLocation = __DIR__ . '/someconsumer-someprovider.json'; -$host = 'localhost'; -$port = 7201; -$endpoint = 'test'; +$files = [__DIR__ . '/someconsumer-someprovider.json']; +$port = 7201; +$endpoint = 'test'; $config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($files) + ->setPort($port); $stubServer = new StubServer($config); $stubServer->start(); $service = new StubServerHttpService(new GuzzleClient(), $config); -echo $service->getJson(); // output: {"results":[{"name":"Games"}]} +echo $service->getJson($endpoint); // output: {"results":[{"name":"Games"}]} ``` diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..4ae5d59e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,18 @@ +# Pact-PHP Releasing + +Pact-PHP packages are published to Packagist + +- https://packagist.org/packages/pact-foundation/pact-php + +## Release Process + +The release process is automated via GitHub Release tags, and Packagist Webhooks. + +1. Create a tag from the master branch in GitHub + - New versions of your package are automatically fetched from tags you create in your VCS repository. +2. Release to Packagist + - A GitHub webhook will inform packagist that the package has been updated. +3. Create a release from the tag in GitHub + - Set it to `latest` + - Click `Generate CHANGELOG` to fill out a changelog. + - Click `Publish Release` diff --git a/UPGRADE-10.0.md b/UPGRADE-10.0.md new file mode 100644 index 00000000..d634ea28 --- /dev/null +++ b/UPGRADE-10.0.md @@ -0,0 +1,140 @@ +# UPGRADE FROM 9.x to 10.0 + +We have migrated from the pact-ruby core, to the pact-reference(rust) core. + +This migrates from a CLI driven process for the Pact Framework, to an FFI process based framework. + +- Pre-requisites + + - PHP 8.x + + - PHP FFI Extension installed + +- Environment Variables + + - These environment variables are no longer required can be removed: + - PACT_CORS + - PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT + - PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC + +- Consumer + + - The `PhpPact\Consumer\Listener\PactTestListener` listener should be removed from your phpunit config + - Default Pact file write mode has been changed from 'overwrite' to 'merge'. Make sure old pact files are removed before running tests. + + ```shell + rm /path/to/pacts/*.json + ``` + + - Pact files now can ONLY be uploaded to Pact Broker by downloading and running Pact CLI manually. + + ```shell + pact-broker publish /path/to/pacts/*.json --consumer-app-version 1.0.0 --branch main --broker-base-url https://test.pactflow.io --broker-token SomeToken + ``` + +- Verifier + + - `$config->setProviderName("providerName")` is now available via `$config->getProviderInfo()->setName("backend")` + - This is further chainable with the following options:- + - `->setHost('localhost')` + - `->setPort('8080')` + - `->setScheme('http')` + - `->setPath('/')` + - Different pacts sources can be configured via `addXxx` methods + - NB:- You must add at least one source, otherwise the verifier will pass, but not verify any Pact files. + - Types:- + - `addUrl` - Verify Provider by Pact Url retrieved by Broker (Webhooks) + - `addBroker` Verify Provider by dynamically fetched Pacts (Provider change) + - `addFile` / `addDir` - Verify Provider by local file or directory + + Example Usage: + + ```php + $config = new VerifierConfig(); + $config + ->setLogLevel('DEBUG'); + $config + ->getProviderInfo() + ->setName("personProvider") + ->setHost('localhost') + ->setPort('8080') + ->setScheme('http') + ->setPath('/'); + + if ($isCi = getenv('CI')) { + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderVersion(exec('git rev-parse --short HEAD')) + ->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD')); + $config->setPublishOptions($publishOptions); + } + + $broker = new Broker(); + $broker->setUsername(getenv('PACT_BROKER_USERNAME')); + $broker->setPassword(getenv('PACT_BROKER_PASSWORD')); + $broker->setUsername(getenv('PACT_BROKER_TOKEN')); + $verifier = new Verifier($config); + + // 1. verify with a broker, but using a pact url to verify a specific pact + // PACT_URL=http://localhost:9292/pacts/provider/personProvider/consumer/personConsumer/latest + if ($pact_url = getenv('PACT_URL')) { + $url = new Url(); + $url->setUrl(new Uri($pact_url)); + $verifier->addUrl($url); + } + // 2. verify files from local directory or file + // results will not be published + else if ($pactDir = getenv('PACT_DIR')) { + $verifier->addDirectory($pactDir); + } else if ($pactFile = getenv('PACT_FILE')) { + $verifier->addFile($pactFile); + } else { + // 2. verify with broker by fetching dynamic pacts (with consumer version selectors) + // if you don't setConsumerVersionSelectors then it will fetch the latest pact for the named provider + if ($pactBrokerBaseUrl = getenv('PACT_BROKER_BASE_URL')) { + $broker->setUrl(new Uri($pactBrokerBaseUrl)); + } else { + $broker->setUrl(new Uri('http://localhost:9292')); + } + // we need to set the provider branch here for PactBrokerWithDynamicConfiguration + // as $publishOptions->setProviderBranch value set above isn't used. + $broker->setProviderBranch(exec('git rev-parse --abbrev-ref HEAD')); + // NOTE - this needs to be a boolean, not a string value, otherwise it doesn't pass through the selector. + // Maybe a pact-php or pact-rust thing + $selectors = (new ConsumerVersionSelectors()) + ->addSelector(' { "mainBranch" : true } ') + ->addSelector(' { "deployedOrReleased" : true } '); + $broker->setConsumerVersionSelectors($selectors); + $broker->setEnablePending(true); + $broker->setIncludeWipPactSince('2020-01-30'); + $verifier->addBroker($broker); + } + + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + ``` + +- Stub Server + + - No longer defaults to port 7201, picks free port at random. + - Endpoint now can be set by: + + ```php + $service = new StubServerHttpService(new GuzzleClient(), $this->config); + $service->getJson($endpoint); + ``` + +- Example Migrations to 10.x (Pull Request Diffs) + - PHP Verifier https://github.com/acmachado14/simple-pact/compare/main...YOU54F:simple-pact:ffi-next + - PHP Consumer https://github.com/YOU54F/pact-testing/compare/main...YOU54F:pact-testing:ffi-next + - PHP Consumer & Verifier + - Consumer https://github.com/YOU54F/014-pact-http-consumer-php/compare/main...YOU54F:014-pact-http-consumer-php:ffi-next + - Verifier https://github.com/YOU54F/015-pact-http-producer-php/compare/main...YOU54F:015-pact-http-producer-php:ffi-next + + +Examples of Additional Features now possible + +- Pact Plugins + - CSV https://github.com/tienvx/pact-php-csv + - Protobuf/gRPC https://github.com/tienvx/pact-php-protobuf \ No newline at end of file diff --git a/composer.json b/composer.json index 2404f9fa..0770643b 100644 --- a/composer.json +++ b/composer.json @@ -19,33 +19,20 @@ ], "require": { "php": "^8.0", - "ext-openssl": "*", "ext-json": "*", "composer/semver": "^1.4.0|^3.2.0", - "amphp/amp": "^2.5.1", - "amphp/byte-stream": "^1.8", - "amphp/dns": "^1.2.3", - "amphp/hpack": "^3.1.0", - "amphp/http-server": "^2.1", - "amphp/log": "^1.1", - "amphp/process": "^1.1.1", - "amphp/serialization": "^1.0", - "amphp/socket": "^1.1.3", - "amphp/sync": "^1.4.0", - "amphp/cache": "^1.4.0", - "amphp/windows-registry": "v0.3.3", + "symfony/process": "^4.4|^5.4|^6.0", "guzzlehttp/guzzle": "^6.5.8|^7.4.5", - "phpunit/phpunit": ">=8.5.23 <10", - "tienvx/composer-downloads-plugin": "^1.1.0" + "tienvx/composer-downloads-plugin": "^1.2.0" }, "require-dev": { "roave/security-advisories": "dev-latest", - "mockery/mockery": "^1.4.2", "slim/slim": "^4.6", "slim/psr7": "^1.2.0", "friendsofphp/php-cs-fixer": "^3.0", "php-amqplib/php-amqplib": "^3.0", - "phpstan/phpstan": "^1.9" + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": ">=8.5.23 <10" }, "autoload": { "psr-4": { @@ -66,27 +53,50 @@ "MessageProvider\\": [ "example/src/MessageProvider", "example/tests/MessageProvider" + ], + "Provider\\": [ + "example/src/Provider" ] } }, "scripts": { "start-provider": "php -S localhost:58000 -t example/src/Provider/public/", - "static-code-analysis": "phpstan analyse src/ --level=7", - "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" + "static-code-analysis": "phpstan", + "lint": "php-cs-fixer fix --dry-run", + "fix": "php-cs-fixer fix", + "test": [ + "php -r \"array_map('unlink', glob('./example/output/*.json'));\"", + "phpunit --debug -c example/phpunit.all.xml" + ] }, "extra": { "downloads": { - "pact-ruby-standalone": { - "version": "1.91.0", + "pact-ffi-headers": { + "version": "0.4.4", + "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/pact.h", + "path": "bin/pact-ffi-headers/pact.h" + }, + "pact-ffi-lib": { + "version": "0.4.4", + "variables": { + "{$prefix}": "PHP_OS_FAMILY === 'Windows' ? 'pact_ffi' : 'libpact_ffi'", + "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", + "{$architecture}": "in_array(php_uname('m'), ['arm64', 'aarch64']) ? (PHP_OS === 'Darwin' ? 'aarch64-apple-darwin' : 'aarch64') : 'x86_64'", + "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'dll' : (PHP_OS === 'Darwin' ? 'dylib' : 'so')" + }, + "url": "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{$version}/{$prefix}-{$os}-{$architecture}.{$extension}.gz", + "path": "bin/pact-ffi-lib/pact.{$extension}" + }, + "pact-stub-server": { + "version": "0.5.3", "variables": { - "{$os}": "PHP_OS_FAMILY === 'Windows' ? 'win32' : (PHP_OS === 'Darwin' ? 'osx' : 'linux')", - "{$architecture}": "PHP_OS === 'Linux' ? '-x86_64' : ''", - "{$extension}": "PHP_OS_FAMILY === 'Windows' ? 'zip' : 'tar.gz'" + "{$os}": "PHP_OS === 'Darwin' ? 'osx' : strtolower(PHP_OS_FAMILY)", + "{$architecture}": "in_array(php_uname('m'), ['arm64', 'aarch64']) ? 'aarch64' : 'x86_64'", + "{$extension}": "PHP_OS_FAMILY === 'Windows' ? '.exe' : ''" }, - "url": "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{$version}/pact-{$version}-{$os}{$architecture}.{$extension}", - "path": "bin/pact-ruby-standalone" + "url": "https://github.com/pact-foundation/pact-stub-server/releases/download/v{$version}/pact-stub-server-{$os}-{$architecture}{$extension}.gz", + "path": "bin/pact-stub-server/pact-stub-server{$extension}", + "executable": true } } }, diff --git a/example/README.md b/example/README.md index bb79b88e..ab66a43b 100644 --- a/example/README.md +++ b/example/README.md @@ -16,10 +16,6 @@ All examples could be run within tests. ## Consumer Tests for Message Processing vendor/bin/phpunit -c example/phpunit.message.consumer.xml - -## Provider Verification Tests for Message Processing - - vendor/bin/phpunit -c example/phpunit.message.provider.xml ## All tests together diff --git a/example/pacts/someconsumer-someprovider.json b/example/pacts/someconsumer-someprovider.json index c502e5ba..2d917286 100644 --- a/example/pacts/someconsumer-someprovider.json +++ b/example/pacts/someconsumer-someprovider.json @@ -2,61 +2,76 @@ "consumer": { "name": "someConsumer" }, - "provider": { - "name": "someProvider" - }, "interactions": [ { - "description": "A get request to /goodbye/{name}", - "providerState": "Get Goodbye", + "description": "A get request to /hello/{name}", "request": { - "method": "GET", - "path": "/goodbye/Bob", "headers": { "Content-Type": "application/json" - } + }, + "method": "GET", + "path": "/hello/Bob" }, "response": { - "status": 200, + "body": { + "message": "Hello, Bob" + }, "headers": { "Content-Type": "application/json" }, - "body": { - "message": "Goodbye, Bob" - } - }, - "metadata": null + "matchingRules": { + "body": { + "$.message": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "(Hello, )[A-Za-z]+" + } + ] + } + }, + "header": {} + }, + "status": 200 + } }, { - "description": "A get request to /hello/{name}", + "description": "A get request to /goodbye/{name}", + "providerStates": [ + { + "name": "Get Goodbye" + } + ], "request": { - "method": "GET", - "path": "/hello/Bob", "headers": { "Content-Type": "application/json" - } + }, + "method": "GET", + "path": "/goodbye/Bob" }, "response": { - "status": 200, + "body": { + "message": "Goodbye, Bob" + }, "headers": { "Content-Type": "application/json" }, - "body": { - "message": "Hello, Bob" - }, - "matchingRules": { - "$.body.message": { - "match": "regex", - "regex": "(Hello, )[A-Za-z]" - } - } - }, - "metadata": null + "status": 200 + } } ], "metadata": { + "pactRust": { + "ffi": "0.4.4", + "mockserver": "1.0.3", + "models": "1.0.13" + }, "pactSpecification": { - "version": "2.0.0" + "version": "3.0.0" } + }, + "provider": { + "name": "someProvider" } } \ No newline at end of file diff --git a/example/pacts/test_consumer-test_provider.json b/example/pacts/test_consumer-test_provider.json index a015e9ae..13f684cd 100644 --- a/example/pacts/test_consumer-test_provider.json +++ b/example/pacts/test_consumer-test_provider.json @@ -2,52 +2,53 @@ "consumer": { "name": "test_consumer" }, - "provider": { - "name": "test_provider" - }, "messages": [ { - "description": "an alligator named Mary exists", + "contents": { + "song": "And the wind whispers Mary" + }, + "description": "footprints dressed in red", + "metadata": { + "contentType": "application/json", + "queue": "And the clowns have all gone to bed", + "routing_key": "And the clowns have all gone to bed" + }, "providerStates": [ { - "name": "a message" + "name": "You can hear happiness staggering on down the street" } - ], + ] + }, + { "contents": { "text": "Hello Mary" }, - "matchingRules": { - "body": { - } - }, - "metaData": { + "description": "an alligator named Mary exists", + "metadata": { + "contentType": "application/json", "queue": "wind cries", "routing_key": "wind cries" - } - }, - { - "description": "footprints dressed in red", + }, "providerStates": [ { - "name": "You can hear happiness staggering on down the street" - } - ], - "contents": { - "song": "And the wind whispers Mary" - }, - "matchingRules": { - "body": { + "name": "a message", + "params": { + "foo": "bar" + } } - }, - "metaData": { - "queue": "And the clowns have all gone to bed", - "routing_key": "And the clowns have all gone to bed" - } + ] } ], "metadata": { + "pactRust": { + "ffi": "0.4.4", + "models": "1.0.13" + }, "pactSpecification": { - "version": "2.0.0" + "version": "3.0.0" } + }, + "provider": { + "name": "test_provider" } -} +} \ No newline at end of file diff --git a/example/phpunit.all.xml b/example/phpunit.all.xml index 554c8a46..d3f50c70 100644 --- a/example/phpunit.all.xml +++ b/example/phpunit.all.xml @@ -13,30 +13,10 @@ ./tests/MessageConsumer - - ./tests/MessageProvider - - - - - - - PhpPact Consumer Example Tests - - - - - - - - - - - diff --git a/example/phpunit.consumer.xml b/example/phpunit.consumer.xml index 6e88b251..6236e64d 100644 --- a/example/phpunit.consumer.xml +++ b/example/phpunit.consumer.xml @@ -5,26 +5,9 @@ ./tests/Consumer - - - - - - PhpPact Example Tests - - - - - - - - - - - diff --git a/example/phpunit.core.xml b/example/phpunit.core.xml index aefb2d26..6cc7fc8b 100644 --- a/example/phpunit.core.xml +++ b/example/phpunit.core.xml @@ -5,26 +5,9 @@ ../tests - - - - - - PhpPact Consumer Example Tests - - - - - - - - - - - diff --git a/example/phpunit.message.provider.xml b/example/phpunit.message.provider.xml deleted file mode 100644 index 2844c97d..00000000 --- a/example/phpunit.message.provider.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - ./tests/MessageProvider - - - diff --git a/example/src/Consumer/publish_json_example.php b/example/src/Consumer/publish_json_example.php deleted file mode 100644 index 9e91327b..00000000 --- a/example/src/Consumer/publish_json_example.php +++ /dev/null @@ -1,16 +0,0 @@ - 'someConsumer', - 'provider' => 'someProvider' -]); - -$httpService->publishJson('1.0.0', $json); diff --git a/example/src/MessageConsumer/ExampleMessageConsumer.php b/example/src/MessageConsumer/ExampleMessageConsumer.php index 0a821944..77851aa6 100644 --- a/example/src/MessageConsumer/ExampleMessageConsumer.php +++ b/example/src/MessageConsumer/ExampleMessageConsumer.php @@ -4,7 +4,7 @@ class ExampleMessageConsumer { - public function ProcessText($message) + public function ProcessText(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->text, true) . "\n"; @@ -12,7 +12,7 @@ public function ProcessText($message) return $obj; } - public function ProcessSong($message) + public function ProcessSong(string $message): object { $obj = \json_decode($message); print ' [x] Processed ' . \print_r($obj->contents->song, true) . "\n"; diff --git a/example/src/MessageProvider/ExampleMessageProvider.php b/example/src/MessageProvider/ExampleMessageProvider.php index d038e8f2..a8f8c60e 100644 --- a/example/src/MessageProvider/ExampleMessageProvider.php +++ b/example/src/MessageProvider/ExampleMessageProvider.php @@ -4,32 +4,20 @@ class ExampleMessageProvider { - /** @var array */ - private $metadata; + private array $metadata; - /** - * @var mixed - */ - private $contents; + private mixed $contents; - public function __construct($metadata = []) + public function __construct(array $metadata = []) { $this->metadata = $metadata; } - /** - * @return array - */ public function getMetadata(): array { return $this->metadata; } - /** - * @param array $metadata - * - * @return ExampleMessageProvider - */ public function setMetadata(array $metadata): self { $this->metadata = $metadata; @@ -37,20 +25,12 @@ public function setMetadata(array $metadata): self return $this; } - /** - * @return mixed - */ - public function getContents() + public function getContents(): mixed { return $this->contents; } - /** - * @param mixed $contents - * - * @return ExampleMessageProvider - */ - public function setContents($contents) + public function setContents(mixed $contents): self { $this->contents = $contents; @@ -59,10 +39,8 @@ public function setContents($contents) /** * Build metadata and content for message - * - * @return string */ - public function Build() + public function Build(): string { $obj = new \stdClass(); $obj->metadata = $this->metadata; diff --git a/example/src/Provider/ExampleProvider.php b/example/src/Provider/ExampleProvider.php new file mode 100644 index 00000000..c7fddb72 --- /dev/null +++ b/example/src/Provider/ExampleProvider.php @@ -0,0 +1,66 @@ +messages = [ + 'an alligator named Mary exists' => [ + 'metadata' => [ + 'queue' => 'wind cries', + 'routing_key' => 'wind cries', + ], + 'contents' => [ + 'text' => 'Hello Mary', + ] + ], + 'footprints dressed in red' => [ + 'metadata' => [ + 'queue' => 'And the clowns have all gone to bed', + 'routing_key' => 'And the clowns have all gone to bed', + ], + 'contents' => [ + 'song' => 'And the wind whispers Mary', + ] + ], + ]; + } + + public function sayHello(string $name): string + { + return "Hello, {$name}"; + } + + public function sayGoodbye(string $name): string + { + return "Goodbye, {$name}"; + } + + public function dispatchMessage(string $description, array $providerStates): ?ExampleMessageProvider + { + if (!isset($this->messages[$description])) { + return null; + } + + return (new ExampleMessageProvider()) + ->setMetadata($this->messages[$description]['metadata']) + ->setContents($this->messages[$description]['contents']); + } + + public function changeSate(string $action, string $state, array $params): void + { + $this->currentState = [ + 'action' => $action, + 'state' => $state, + 'params' => $params, + ]; + } +} diff --git a/example/src/Provider/public/index.php b/example/src/Provider/public/index.php index 2a5406ed..48e8a188 100644 --- a/example/src/Provider/public/index.php +++ b/example/src/Provider/public/index.php @@ -1,5 +1,6 @@ addBodyParsingMiddleware(); -$app->get('/hello/{name}', function (Request $request, Response $response) { +$provider = new ExampleProvider(); + +$app->get('/hello/{name}', function (Request $request, Response $response) use ($provider) { $name = $request->getAttribute('name'); - $response->getBody()->write(\json_encode(['message' => "Hello, {$name}"])); + $response->getBody()->write(\json_encode(['message' => $provider->sayHello($name)])); return $response->withHeader('Content-Type', 'application/json'); }); -$app->get('/goodbye/{name}', function (Request $request, Response $response) { +$app->get('/goodbye/{name}', function (Request $request, Response $response) use ($provider) { $name = $request->getAttribute('name'); - $response->getBody()->write(\json_encode(['message' => "Goodbye, {$name}"])); + $response->getBody()->write(\json_encode(['message' => $provider->sayGoodbye($name)])); return $response->withHeader('Content-Type', 'application/json'); }); +$app->post('/pact-messages', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $message = $provider->dispatchMessage($body['description'], $body['providerStates']); + if ($message) { + $response->getBody()->write(\json_encode($message->getContents())); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('Pact-Message-Metadata', \base64_encode(\json_encode($message->getMetadata()))); + } + + return $response; +}); + +$app->post('/pact-change-state', function (Request $request, Response $response) use ($provider) { + $body = $request->getParsedBody(); + $provider->changeSate($body['action'], $body['state'], $body['params']); + + return $response; +}); + $app->run(); diff --git a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php index e5106f55..b3140154 100644 --- a/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceGoodbyeTest.php @@ -39,10 +39,10 @@ public function testGetGoodbyeString() ->willRespondWith($response); $service = new HttpClientService($config->getBaseUri()); - $result = $service->getGoodbyeString('Bob'); + $goodbyeResult = $service->getGoodbyeString('Bob'); + $verifyResult = $builder->verify(); - $builder->verify(); - - $this->assertEquals('Goodbye, Bob', $result); + $this->assertTrue($verifyResult); + $this->assertEquals('Goodbye, Bob', $goodbyeResult); } } diff --git a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php index 2a80f394..d10782ff 100644 --- a/example/tests/Consumer/Service/ConsumerServiceHelloTest.php +++ b/example/tests/Consumer/Service/ConsumerServiceHelloTest.php @@ -33,7 +33,7 @@ public function testGetHelloString() ->setStatus(200) ->addHeader('Content-Type', 'application/json') ->setBody([ - 'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]') + 'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]+') ]); // Create a configuration that reflects the server that was started. You can create a custom MockServerConfigInterface if needed. @@ -42,13 +42,13 @@ public function testGetHelloString() $builder ->uponReceiving('A get request to /hello/{name}') ->with($request) - ->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction. + ->willRespondWith($response); // This has to be last. This is what makes FFI calls to register the interaction and start the mock server. $service = new HttpClientService($config->getBaseUri()); // Pass in the URL to the Mock Server. - $result = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. + $helloResult = $service->getHelloString('Bob'); // Make the real API request against the Mock Server. + $verifyResult = $builder->verify(); // This will verify that the interactions took place. - $builder->verify(); // This will verify that the interactions took place. - - $this->assertEquals('Hello, Bob', $result); // Make your assertions. + $this->assertTrue($verifyResult); // Make your assertions. + $this->assertEquals('Hello, Bob', $helloResult); } } diff --git a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php index e9bda5d0..0056d35d 100644 --- a/example/tests/MessageConsumer/ExampleMessageConsumerTest.php +++ b/example/tests/MessageConsumer/ExampleMessageConsumerTest.php @@ -2,8 +2,6 @@ namespace MessageConsumer; -require_once __DIR__ . '/../../src/MessageConsumer/ExampleMessageConsumer.php'; - use Exception; use PhpPact\Consumer\MessageBuilder; use PhpPact\Config\PactConfigInterface; @@ -28,18 +26,6 @@ public static function setUpBeforeClass(): void ->setPactDir(__DIR__ . '/../../output/'); } - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - - // build out brokerHttpService as your example - /* - $brokerHttpService = new BrokerHttpClient(new GuzzleClient(), new Uri($pactBrokerUri)); - $brokerHttpService->publishJson($json, $consumerVersion); - $brokerHttpService->tag($this->mockServerConfig->getConsumer(), $consumerVersion, $tag); - */ - } - /** * @throws Exception */ @@ -53,7 +39,7 @@ public function testProcessText() $metadata = ['queue'=>'wind cries', 'routing_key'=>'wind cries']; $builder - ->given('a message', ['foo']) + ->given('a message', ['foo' => 'bar']) ->expectsToReceive('an alligator named Mary exists') ->withMetadata($metadata) ->withContent($contents); @@ -63,11 +49,9 @@ public function testProcessText() $callback = [$consumerMessage, 'ProcessText']; $builder->setCallback($callback); - $hasException = false; - - $builder->verify(); + $verifyResult = $builder->verify(); - $this->assertTrue(true, 'Expects to reach this true statement by running verify()'); + $this->assertTrue($verifyResult); } /** @@ -93,14 +77,8 @@ public function testProcessSong() $callback = [$consumerMessage, 'ProcessSong']; $builder->setCallback($callback); - $hasException = false; - - try { - $builder->verify(); - } catch (Exception $e) { - $hasException = true; - } + $verifyResult = $builder->verify(); - $this->assertFalse($hasException, 'Expects verification to pass without exceptions being thrown'); + $this->assertTrue($verifyResult); } } diff --git a/example/tests/MessageProvider/ExampleMessageProviderTest.php b/example/tests/MessageProvider/ExampleMessageProviderTest.php deleted file mode 100644 index 21906049..00000000 --- a/example/tests/MessageProvider/ExampleMessageProviderTest.php +++ /dev/null @@ -1,61 +0,0 @@ -text ='Hello Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $callbacks['footprints dressed in red'] = function () { - $content = new \stdClass(); - $content->song ='And the wind whispers Mary'; - - $metadata = []; - $metadata['queue'] = 'myKey'; - - $provider = (new ExampleMessageProvider()) - ->setContents($content) - ->setMetadata($metadata); - - return $provider->Build(); - }; - - $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setPublishResults(false); // Flag the verifier service to publish the results to the Pact Broker. - - // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. - $verifier = (new MessageVerifier($config)) - ->setCallbacks($callbacks) - ->verifyFiles([__DIR__ . '/../../pacts/test_consumer-test_provider.json']); - - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Expects to reach true by running verification'); - } -} diff --git a/example/tests/Provider/PactVerifyTest.php b/example/tests/Provider/PactVerifyTest.php index f1927caa..4f61e93b 100644 --- a/example/tests/Provider/PactVerifyTest.php +++ b/example/tests/Provider/PactVerifyTest.php @@ -3,10 +3,11 @@ namespace Provider; use GuzzleHttp\Psr7\Uri; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransport; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\Runner\ProcessRunner; 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. @@ -14,8 +15,8 @@ */ class PactVerifyTest extends TestCase { - /** @var ProcessRunner */ - private $processRunner; + /** @var Process */ + private Process $process; /** * Run the PHP build-in web server. @@ -24,9 +25,10 @@ protected function setUp(): void { $publicPath = __DIR__ . '/../../src/Provider/public/'; - $this->processRunner = new ProcessRunner('php', ['-S', 'localhost:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - $this->processRunner->run(); + $this->process->start(); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); } /** @@ -34,7 +36,7 @@ protected function setUp(): void */ protected function tearDown(): void { - $this->processRunner->stop(); + $this->process->stop(); } /** @@ -43,18 +45,31 @@ protected function tearDown(): void public function testPactVerifyConsumer() { $config = new VerifierConfig(); - $config - ->setProviderName('someProvider') // Providers name to fetch. - ->setProviderVersion('1.0.0') // Providers version. - ->setProviderBranch('main') // Providers git branch - ->setProviderBaseUrl(new Uri('http://localhost:7202')) // URL of the Provider. - ; // Flag the verifier service to publish the results to the Pact Broker. + $config->getProviderInfo() + ->setName('someProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + $config->getProviderState() + ->setStateChangeUrl(new Uri('http://localhost:7202/pact-change-state')) + ; + $config->addProviderTransport( + (new ProviderTransport()) + ->setProtocol(ProviderTransport::MESSAGE_PROTOCOL) + ->setPort(7202) + ->setPath('/pact-messages') + ->setScheme('http') + ); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } // Verify that the Consumer 'someConsumer' that is tagged with 'master' is valid. $verifier = new Verifier($config); - $verifier->verifyFiles([__DIR__ . '/../../pacts/someconsumer-someprovider.json']); + $verifier->addFile(__DIR__ . '/../../pacts/someconsumer-someprovider.json'); + $verifier->addFile(__DIR__ . '/../../pacts/test_consumer-test_provider.json'); - // This will not be reached if the PACT verifier throws an error, otherwise it was successful. - $this->assertTrue(true, 'Pact Verification has failed.'); + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..1d5823a9 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 7 + paths: + - src diff --git a/phpunit.xml b/phpunit.xml index 35d425c2..0a7db8ac 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,14 +18,8 @@ - - - - - - diff --git a/src/PhpPact/Broker/Service/BrokerHttpClient.php b/src/PhpPact/Broker/Service/BrokerHttpClient.php deleted file mode 100644 index 53be68e7..00000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClient.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ - private array $headers; - - /** - * {@inheritdoc} - */ - public function __construct(ClientInterface $httpClient, UriInterface $baseUri, array $headers = []) - { - $this->httpClient = $httpClient; - $this->baseUri = $baseUri; - $this->headers = $headers; - - if (!\array_key_exists('Content-Type', $headers)) { - $this->headers['Content-Type'] = 'application/json'; - } - } - - /** - * {@inheritdoc} - */ - public function publishJson(string $version, string $json): void - { - $array = \json_decode($json, true, 512, JSON_THROW_ON_ERROR); - $consumer = $array['consumer']['name']; - $provider = $array['provider']['name']; - - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/consumer/{$consumer}/version/{$version}"); - - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - 'body' => $json, - ]); - } - - /** - * {@inheritdoc} - */ - public function tag(string $consumer, string $version, string $tag): void - { - $uri = $this->baseUri->withPath("/pacticipants/{$consumer}/versions/{$version}/tags/{$tag}"); - $this->httpClient->put($uri, [ - 'headers' => $this->headers, - ]); - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrls(string $provider, string $version = 'latest'): array - { - if ($version !== 'latest') { - @\trigger_error(\sprintf('The second argument "version" in "%s()" method makes no sense and will be removed in any upcoming major version', __METHOD__), E_USER_DEPRECATED); - } - - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } - - /** - * {@inheritdoc} - */ - public function getAllConsumerUrlsForTag(string $provider, string $tag): array - { - $uri = $this->baseUri->withPath("/pacts/provider/{$provider}/latest/{$tag}"); - - $response = $this->httpClient->get($uri, [ - 'headers' => $this->headers, - ]); - - $json = \json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - - $urls = []; - foreach ($json['_links']['pacts'] as $pact) { - $urls[] = $pact['href']; - } - - return $urls; - } -} diff --git a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php b/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php deleted file mode 100644 index e4baa16a..00000000 --- a/src/PhpPact/Broker/Service/BrokerHttpClientInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - $headers additional headers - */ - public function __construct(ClientInterface $client, UriInterface $baseUri, array $headers); - - /** - * Publish JSON. - * - * @param string $version Consumer version - * @param string $json PACT File JSON - */ - public function publishJson(string $version, string $json): void; - - /** - * Tag a consumer version with a tag. - */ - public function tag(string $consumer, string $version, string $tag): void; - - /** - * Get all Pact urls for the consumer. - * - * @param string $provider provider name - * @param string $version version of the provider - * - * @return array - */ - public function getAllConsumerUrls(string $provider, string $version = 'latest'): array; - - /** - * Get all Pact URLs for a specific tag. - * - * @return array - */ - public function getAllConsumerUrlsForTag(string $provider, string $tag): array; -} diff --git a/src/PhpPact/Config/LogLevelTrait.php b/src/PhpPact/Config/LogLevelTrait.php new file mode 100644 index 00000000..de5265ce --- /dev/null +++ b/src/PhpPact/Config/LogLevelTrait.php @@ -0,0 +1,24 @@ +logLevel; + } + + public function setLogLevel(string $logLevel): self + { + $logLevel = \strtoupper($logLevel); + if (!\in_array($logLevel, ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'])) { + throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); + } + $this->logLevel = $logLevel; + + return $this; + } +} diff --git a/src/PhpPact/Config/PactConfig.php b/src/PhpPact/Config/PactConfig.php index a54893f5..2d964dab 100644 --- a/src/PhpPact/Config/PactConfig.php +++ b/src/PhpPact/Config/PactConfig.php @@ -6,6 +6,8 @@ class PactConfig implements PactConfigInterface { + use LogLevelTrait; + /** * Consumer name. */ @@ -22,17 +24,9 @@ class PactConfig implements PactConfigInterface private ?string $pactDir = null; /** - * `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. - */ - private string $pactFileWriteMode = self::MODE_OVERWRITE; - - /** - * The pact specification version to use when writing the pact. Note that only versions 1 and 2 are currently supported. + * The pact specification version to use when writing the pact. Note that only versions 1, 2, 3 and 4 are currently supported. */ - private string $pactSpecificationVersion; + private string $pactSpecificationVersion = self::DEFAULT_SPECIFICATION_VERSION; /** * File to which to log output. @@ -40,6 +34,13 @@ class PactConfig implements PactConfigInterface private ?string $log = null; private ?string $logLevel = null; + /** + * `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. + */ + private string $pactFileWriteMode = self::MODE_MERGE; /** * {@inheritdoc} @@ -107,32 +108,6 @@ public function setPactDir(?string $pactDir): self return $this; } - /** - * {@inheritdoc} - */ - public function getPactFileWriteMode(): string - { - return $this->pactFileWriteMode; - } - - /** - * {@inheritdoc} - */ - public function setPactFileWriteMode(string $pactFileWriteMode): self - { - $options = [self::MODE_OVERWRITE, self::MODE_MERGE]; - - if (!\in_array($pactFileWriteMode, $options)) { - $implodedOptions = \implode(', ', $options); - - throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); - } - - $this->pactFileWriteMode = $pactFileWriteMode; - - return $this; - } - /** * {@inheritdoc} */ @@ -177,18 +152,28 @@ public function setLog(string $log): self return $this; } - public function getLogLevel(): ?string + /** + * {@inheritdoc} + */ + public function getPactFileWriteMode(): string { - return $this->logLevel; + return $this->pactFileWriteMode; } - public function setLogLevel(string $logLevel): PactConfigInterface + /** + * {@inheritdoc} + */ + public function setPactFileWriteMode(string $pactFileWriteMode): self { - $logLevel = \strtoupper($logLevel); - if (!\in_array($logLevel, ['DEBUG', 'INFO', 'WARN', 'ERROR'])) { - throw new \InvalidArgumentException('LogLevel ' . $logLevel . ' not supported.'); + $options = [self::MODE_OVERWRITE, self::MODE_MERGE]; + + if (!\in_array($pactFileWriteMode, $options)) { + $implodedOptions = \implode(', ', $options); + + throw new \InvalidArgumentException("Invalid PhpPact File Write Mode, value must be one of the following: {$implodedOptions}."); } - $this->logLevel = $logLevel; + + $this->pactFileWriteMode = $pactFileWriteMode; return $this; } diff --git a/src/PhpPact/Config/PactConfigInterface.php b/src/PhpPact/Config/PactConfigInterface.php index a1597a8c..71f76b3c 100644 --- a/src/PhpPact/Config/PactConfigInterface.php +++ b/src/PhpPact/Config/PactConfigInterface.php @@ -10,7 +10,7 @@ */ interface PactConfigInterface { - public const DEFAULT_SPECIFICATION_VERSION = '2.0.0'; + public const DEFAULT_SPECIFICATION_VERSION = '3.0.0'; public const MODE_OVERWRITE = 'overwrite'; public const MODE_MERGE = 'merge'; diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php new file mode 100644 index 00000000..d25a743c --- /dev/null +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -0,0 +1,66 @@ +message = new Message(); + } + + /** + * @param string $name what is given to the request + * @param array $params for that request + * @param bool $overwrite clear pass states completely and start this array + */ + public function given(string $name, array $params = [], bool $overwrite = false): self + { + $this->message->setProviderState($name, $params, $overwrite); + + return $this; + } + + /** + * @param string $description what is received when the request is made + */ + public function expectsToReceive(string $description): self + { + $this->message->setDescription($description); + + return $this; + } + + /** + * @param array $metadata what is the additional metadata of the message + */ + public function withMetadata(array $metadata): self + { + $this->message->setMetadata($metadata); + + return $this; + } + + /** + * Make the http request to the Mock Service to register the message. Content is required. + * + * @param mixed $contents required to be in the message + */ + public function withContent(mixed $contents): self + { + $this->message->setContents($contents); + + return $this; + } + + public function withContentType(?string $contentType): self + { + $this->message->setContentType($contentType); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/BuilderInterface.php b/src/PhpPact/Consumer/BuilderInterface.php index 5fc039dc..58ef8eda 100644 --- a/src/PhpPact/Consumer/BuilderInterface.php +++ b/src/PhpPact/Consumer/BuilderInterface.php @@ -11,9 +11,4 @@ interface BuilderInterface * Verify that the interactions are valid. */ public function verify(): bool; - - /** - * Write the Pact without deleting the interactions. - */ - public function writePact(): bool; } diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php new file mode 100644 index 00000000..d9fbde15 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriver.php @@ -0,0 +1,32 @@ +mockServer->verify(); + } + + public function registerInteraction(Interaction $interaction): bool + { + $this->pactDriver->setUp(); + $this->interactionRegistry->registerInteraction($interaction); + $this->mockServer->start(); + + return true; + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php new file mode 100644 index 00000000..fe5a3e07 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/InteractionDriverInterface.php @@ -0,0 +1,12 @@ +client->call('pactffi_message_reify', $this->messageRegistry->getId()); + } + + public function writePactAndCleanUp(): bool + { + $this->pactDriver->writePact(); + $this->pactDriver->cleanUp(); + + return true; + } + + public function registerMessage(Message $message): void + { + $this->pactDriver->setUp(); + $this->messageRegistry->registerMessage($message); + } +} diff --git a/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php new file mode 100644 index 00000000..be9a3089 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Interaction/MessageDriverInterface.php @@ -0,0 +1,14 @@ +pactRegistry->deletePact(); + } + + public function writePact(): void + { + $error = $this->client->call( + 'pactffi_pact_handle_write_file', + $this->pactRegistry->getId(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new PactFileNotWroteException($error); + } + } + + public function setUp(): void + { + $this + ->initWithLogLevel() + ->registerPact(); + } + + protected function getSpecification(): int + { + return match (true) { + $this->versionEqualTo('1.0.0') => $this->client->get('PactSpecification_V1'), + $this->versionEqualTo('1.1.0') => $this->client->get('PactSpecification_V1_1'), + $this->versionEqualTo('2.0.0') => $this->client->get('PactSpecification_V2'), + $this->versionEqualTo('3.0.0') => $this->client->get('PactSpecification_V3'), + $this->versionEqualTo('4.0.0') => $this->client->get('PactSpecification_V4'), + default => function () { + trigger_error(sprintf("Specification version '%s' is unknown", $this->config->getPactSpecificationVersion()), E_USER_WARNING); + + return $this->client->get('PactSpecification_Unknown'); + }, + }; + } + + private function versionEqualTo(string $version): bool + { + return Comparator::equalTo($this->config->getPactSpecificationVersion(), $version); + } + + private function initWithLogLevel(): self + { + $logLevel = $this->config->getLogLevel(); + if ($logLevel) { + $this->client->call('pactffi_init_with_log_level', $logLevel); + } + + return $this; + } + + private function registerPact(): self + { + $this->pactRegistry->registerPact( + $this->config->getConsumer(), + $this->config->getProvider(), + $this->getSpecification() + ); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php new file mode 100644 index 00000000..69ccdc42 --- /dev/null +++ b/src/PhpPact/Consumer/Driver/Pact/PactDriverInterface.php @@ -0,0 +1,12 @@ + 'An invalid handle was received. Handles should be created with `pactffi_new_pact`', + -2 => 'Transport_config is not valid JSON', + -3 => 'The mock server could not be started', + -4 => 'The method panicked', + -5 => 'The address is not valid', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } +} diff --git a/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php b/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php new file mode 100644 index 00000000..01209897 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MockServerNotWrotePactFileException.php @@ -0,0 +1,19 @@ + 'A general panic was caught', + 2 => 'The pact file was not able to be written', + 3 => 'A mock server with the provided port was not found', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } +} diff --git a/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php b/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php new file mode 100644 index 00000000..e55a5e09 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/PactFileNotWroteException.php @@ -0,0 +1,19 @@ + 'The function panicked.', + 2 => 'The pact file was not able to be written.', + 3 => 'The pact for the given handle was not found.', + default => 'Unknown error', + }; + parent::__construct($message, $code); + } +} diff --git a/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php b/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php new file mode 100644 index 00000000..8af9a00b --- /dev/null +++ b/src/PhpPact/Consumer/Exception/PactNotRegisteredException.php @@ -0,0 +1,9 @@ +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'; - } - - /** - * @throws \JsonException - */ - 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(), null, 512, JSON_THROW_ON_ERROR), JSON_THROW_ON_ERROR); - } -} diff --git a/src/PhpPact/Consumer/InteractionBuilder.php b/src/PhpPact/Consumer/InteractionBuilder.php index 0e25dab5..000a08ae 100644 --- a/src/PhpPact/Consumer/InteractionBuilder.php +++ b/src/PhpPact/Consumer/InteractionBuilder.php @@ -2,37 +2,36 @@ namespace PhpPact\Consumer; +use PhpPact\Consumer\Driver\Interaction\InteractionDriverInterface; +use PhpPact\Consumer\Factory\InteractionDriverFactory; +use PhpPact\Consumer\Factory\InteractionDriverFactoryInterface; 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 */ class InteractionBuilder implements BuilderInterface { - protected MockServerHttpService $mockServerHttpService; - - protected MockServerConfigInterface $config; - + private InteractionDriverInterface $driver; private Interaction $interaction; - public function __construct(MockServerConfigInterface $config) + public function __construct(MockServerConfigInterface $config, ?InteractionDriverFactoryInterface $driverFactory = null) { - $this->config = $config; - $this->mockServerHttpService = new MockServerHttpService(new GuzzleClient(), $config); - $this->interaction = new Interaction(); + $this->driver = ($driverFactory ?? new InteractionDriverFactory())->create($config); + $this->interaction = new Interaction(); } /** * @param string $providerState what is given to the request + * @param array $params for that request + * @param bool $overwrite clear pass states completely and start this array */ - public function given(string $providerState): self + public function given(string $providerState, array $params = [], bool $overwrite = false): self { - $this->interaction->setProviderState($providerState); + $this->interaction->setProviderState($providerState, $params, $overwrite); return $this; } @@ -58,18 +57,15 @@ public function with(ConsumerRequest $request): self } /** - * Make the http request to the Mock Service to register the interaction. - * * @param ProviderResponse $response mock of response received * * @return bool returns true on success - * @throws \JsonException */ public function willRespondWith(ProviderResponse $response): bool { $this->interaction->setResponse($response); - return $this->mockServerHttpService->registerInteraction($this->interaction); + return $this->driver->registerInteraction($this->interaction); } /** @@ -77,33 +73,6 @@ public function willRespondWith(ProviderResponse $response): bool */ public function verify(): bool { - return $this->mockServerHttpService->verifyInteractions(); - } - - /** - * Writes the file to disk and deletes interactions from mock server. - * @throws \JsonException - */ - public function finalize(): bool - { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); - - // Delete the interactions. - $this->mockServerHttpService->deleteAllInteractions(); - - return true; - } - - /** - * {@inheritdoc} - * @throws \JsonException - */ - public function writePact(): bool - { - // Write the pact file to disk. - $this->mockServerHttpService->getPactJson(); - - return true; + return $this->driver->verifyInteractions(); } } diff --git a/src/PhpPact/Consumer/Listener/PactTestListener.php b/src/PhpPact/Consumer/Listener/PactTestListener.php deleted file mode 100644 index a27a1622..00000000 --- a/src/PhpPact/Consumer/Listener/PactTestListener.php +++ /dev/null @@ -1,128 +0,0 @@ - - */ - private array $testSuiteNames = []; - - private MockServerEnvConfig $mockServerConfig; - - private bool $failed = false; - - /** - * @param array $testSuiteNames test suite names that need evaluated with the listener - * - * @throws MissingEnvVariableException - */ - public function __construct(array $testSuiteNames) - { - $this->testSuiteNames = $testSuiteNames; - $this->mockServerConfig = new MockServerEnvConfig(); - } - - /** - * @throws Exception - */ - public function startTestSuite(TestSuite $suite): void - { - if (in_array($suite->getName(), $this->testSuiteNames)) { - $this->server = new MockServer($this->mockServerConfig); - $this->server->start(); - } - } - - public function addError(Test $test, Throwable $t, float $time): void - { - $this->failed = true; - } - - public function addFailure(Test $test, AssertionFailedError $e, float $time): void - { - $this->failed = true; - } - - /** - * Publish JSON results to PACT Broker and stop the Mock Server. - * @throws JsonException|ProcessException - */ - 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'))) { - print 'PACT_BROKER_URI environment variable was not set. Skipping PACT file upload.'; - } elseif (!($consumerVersion = getenv('PACT_CONSUMER_VERSION'))) { - print 'PACT_CONSUMER_VERSION environment variable was not set. Skipping PACT file upload.'; - } elseif (!($tag = getenv('PACT_CONSUMER_TAG'))) { - print 'PACT_CONSUMER_TAG environment variable was not set. Skipping PACT file upload.'; - } else { - $clientConfig = []; - 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'; - } - - $headers = []; - if ($bearerToken = getenv('PACT_BROKER_BEARER_TOKEN')) { - $headers['Authorization'] = 'Bearer ' . $bearerToken; - } - - $client = new GuzzleClient($clientConfig); - - $brokerHttpService = new BrokerHttpClient($client, new Uri($pactBrokerUri), $headers); - $brokerHttpService->tag($this->mockServerConfig->getConsumer(), $consumerVersion, $tag); - $brokerHttpService->publishJson($consumerVersion, $json); - print 'Pact file has been uploaded to the Broker successfully.'; - } - } - } -} diff --git a/src/PhpPact/Consumer/Matcher/HttpStatus.php b/src/PhpPact/Consumer/Matcher/HttpStatus.php new file mode 100644 index 00000000..527bd13e --- /dev/null +++ b/src/PhpPact/Consumer/Matcher/HttpStatus.php @@ -0,0 +1,30 @@ + + */ + public static function all(): array + { + return [ + self::INFORMATION, + self::SUCCESS, + self::REDIRECT, + self::CLIENT_ERROR, + self::SERVER_ERROR, + self::NON_ERROR, + self::ERROR, + ]; + } +} diff --git a/src/PhpPact/Consumer/Matcher/Matcher.php b/src/PhpPact/Consumer/Matcher/Matcher.php index 6d27e1d1..a8da98f2 100644 --- a/src/PhpPact/Consumer/Matcher/Matcher.php +++ b/src/PhpPact/Consumer/Matcher/Matcher.php @@ -8,7 +8,8 @@ use function preg_match; /** - * Matcher implementation. Builds the Ruby Mock Server specification json for interaction publishing. + * Matcher implementation. Builds the Pact FFI specification json for interaction publishing. + * @see https://docs.pact.io/implementation_guides/rust/pact_ffi/integrationjson */ class Matcher { @@ -45,47 +46,92 @@ public function somethingLike(mixed $value): array public function like(mixed $value): array { if ($value === null) { - throw new \Exception('Value must not be null.'); + throw new Exception('Value must not be null.'); } return [ - 'contents' => $value, - 'json_class' => 'Pact::SomethingLike', + 'value' => $value, + 'pact:matcher:type' => 'type', ]; } /** * Expect an array of similar data as the value passed in. * + * @return array + */ + public function eachLike(mixed $value): array + { + return $this->atLeastLike($value, 1); + } + + /** * @param mixed $value example of what the expected data would be * @param int $min minimum number of objects to verify against * * @return array */ - public function eachLike(mixed $value, int $min = 1): array + public function atLeastLike(mixed $value, int $min): array { - $result = [ - 'contents' => $value, - 'json_class' => 'Pact::ArrayLike', + return [ + 'value' => array_fill(0, $min, $value), + 'pact:matcher:type' => 'type', + 'min' => $min, + ]; + } + + /** + * @return array + */ + public function atMostLike(mixed $value, int $max): array + { + return [ + 'value' => [$value], + 'pact:matcher:type' => 'type', + 'max' => $max, ]; + } - $result['min'] = $min; + /** + * @param mixed $value example of what the expected data would be + * @param int $min minimum number of objects to verify against + * + * @return array + */ + public function atLeastAndMostLike(mixed $value, int $min, int $max): array + { + if ($min <= 0 || $min > $max) { + throw new Exception('Invalid minimum number of elements'); + } - return $result; + return [ + 'value' => array_fill(0, $min, $value), + 'pact:matcher:type' => 'type', + 'min' => $min, + 'max' => $max, + ]; } /** * Validate that a value will match a regex pattern. * - * @param mixed $value example of what the expected data would be + * @param string|null $value example of what the expected data would be * @param string $pattern valid Ruby regex pattern * * @return array * * @throws Exception */ - public function term(mixed $value, string $pattern): array + public function term(?string $value, string $pattern): array { + if (null === $value) { + return [ + 'regex' => $pattern, + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + } + $result = preg_match("/$pattern/", $value); if ($result === false || $result === 0) { @@ -95,15 +141,9 @@ public function term(mixed $value, string $pattern): array } return [ - 'data' => [ - 'generate' => $value, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => $pattern, - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $value, + 'regex' => $pattern, + 'pact:matcher:type' => 'regex', ]; } @@ -114,7 +154,7 @@ public function term(mixed $value, string $pattern): array * * @throws Exception */ - public function regex(mixed $value, string $pattern): array + public function regex(?string $value, string $pattern): array { return $this->term($value, $pattern); } @@ -219,14 +259,74 @@ public function decimal(float $float = 13.01): array return $this->like($float); } + /** + * @return array + */ + public function booleanV3(?bool $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomBoolean', + 'pact:matcher:type' => 'boolean', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'boolean', + ]; + } + + /** + * @return array + */ + public function integerV3(?int $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'integer', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'integer', + ]; + } + + /** + * @return array + */ + public function decimalV3(?float $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomDecimal', + 'pact:matcher:type' => 'decimal', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'decimal', + ]; + } + /** * @return array * * @throws Exception */ - public function hexadecimal(string $hex = '3F'): array + public function hexadecimal(?string $value = null): array { - return $this->term($hex, self::HEX_FORMAT); + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomHexadecimal', + ] + $this->term(null, self::HEX_FORMAT); + } + + return $this->term($value, self::HEX_FORMAT); } /** @@ -234,9 +334,15 @@ public function hexadecimal(string $hex = '3F'): array * * @throws Exception */ - public function uuid(string $uuid = 'ce118b6e-d8e1-11e7-9296-cec278b6b50a'): array + public function uuid(?string $value = null): array { - return $this->term($uuid, self::UUID_V4_FORMAT); + if (null === $value) { + return [ + 'pact:generator:type' => 'Uuid', + ] + $this->term(null, self::UUID_V4_FORMAT); + } + + return $this->term($value, self::UUID_V4_FORMAT); } /** @@ -262,10 +368,287 @@ public function ipv6Address(string $ip = '::ffff:192.0.2.128'): array /** * @return array * - * @throws \Exception + * @throws Exception */ public function email(string $email = 'hello@pact.io'): array { return $this->term($email, self::EMAIL_FORMAT); } + + /** + * @return array + * + * @throws Exception + */ + public function ipv4AddressV3(?string $ip = null): array + { + if (null === $ip) { + return $this->term(null, self::IPV4_FORMAT); + } + + return $this->ipv4Address($ip); + } + + /** + * @return array + * + * @throws Exception + */ + public function ipv6AddressV3(?string $ip = null): array + { + if (null === $ip) { + return $this->term(null, self::IPV6_FORMAT); + } + + return $this->ipv6Address($ip); + } + + /** + * @return array + * + * @throws Exception + */ + public function emailV3(?string $email = null): array + { + if (null === $email) { + return $this->term(null, self::EMAIL_FORMAT); + } + + return $this->email($email); + } + + /** + * Value that must be null. This will only match the JSON Null value. For other content types, it will + * match if the attribute is missing. + * + * @return array + */ + public function nullValue(): array + { + return [ + 'pact:matcher:type' => 'null', + ]; + } + + /** + * @return array + */ + public function date(string $format = 'yyyy-MM-dd', ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'Date', + 'pact:matcher:type' => 'date', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'date', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function time(string $format = 'HH:mm::ss', ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'time', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function datetime(string $format = "YYYY-mm-DD'T'HH:mm:ss", ?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'format' => $format, + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'datetime', + 'format' => $format, + ]; + } + + /** + * @return array + */ + public function string(?string $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomString', + ] + $this->like('some string'); // No matcher for string? + } + + return $this->like($value); // No matcher for string? + } + + /** + * @param array $macher + * + * @return array + */ + public function fromProviderState(array $macher, string $expression): array + { + return $macher + [ + 'pact:generator:type' => 'ProviderState', + 'expression' => $expression, + ]; + } + + /** + * Value that must be equal to the example. This is mainly used to reset the matching rules which cascade. + * + * @return array + */ + public function equal(mixed $value): array + { + return [ + 'pact:matcher:type' => 'equality', + 'value' => $value, + ]; + } + + /** + * Value that must include the example value as a substring. + * + * @return array + */ + public function includes(string $value): array + { + return [ + 'pact:matcher:type' => 'include', + 'value' => $value, + ]; + } + + /** + * Value must be a number + * + * @param int|float|null $value Example value. If omitted a random integer value will be generated. + * + * @return array + */ + public function number(int|float|null $value = null): array + { + if (null === $value) { + return [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'number', + ]; + } + + return [ + 'value' => $value, + 'pact:matcher:type' => 'number', + ]; + } + + /** + * Matches the items in an array against a number of variants. Matching is successful if each variant + * occurs once in the array. Variants may be objects containing matching rules. + * + * @param array $variants + * + * @return array + */ + public function arrayContaining(array $variants): array + { + return [ + 'pact:matcher:type' => 'arrayContains', + 'variants' => $variants, + ]; + } + + /** + * Value must be present and not empty (not null or the empty string or empty array or empty object) + * + * @return array + */ + public function notEmpty(mixed $value): array + { + return [ + 'value' => $value, + 'pact:matcher:type' => 'notEmpty', + ]; + } + + /** + * Value must be valid based on the semver specification + * + * @return array + */ + public function semver(string $value): array + { + return [ + 'value' => $value, + 'pact:matcher:type' => 'semver', + ]; + } + + /** + * Matches the response status code. + * + * @return array + */ + public function statusCode(string $status): array + { + if (!in_array($status, HttpStatus::all())) { + throw new Exception(sprintf("Status '%s' is not supported. Supported status are: %s", $status, implode(', ', HttpStatus::all()))); + } + + return [ + 'status' => $status, + 'pact:matcher:type' => 'statusCode', + ]; + } + + /** + * Match the values in a map, ignoring the keys + * + * @param array $values + * + * @return array + */ + public function values(array $values): array + { + return [ + 'value' => $values, + 'pact:matcher:type' => 'values', + ]; + } + + /** + * Match binary data by its content type (magic file check) + * + * @return array + */ + public function contentType(string $contentType): array + { + return [ + 'value' => $contentType, + 'pact:matcher:type' => 'contentType', + ]; + } } diff --git a/src/PhpPact/Consumer/MessageBuilder.php b/src/PhpPact/Consumer/MessageBuilder.php index 513d09c8..6d5be6d5 100644 --- a/src/PhpPact/Consumer/MessageBuilder.php +++ b/src/PhpPact/Consumer/MessageBuilder.php @@ -2,31 +2,27 @@ namespace PhpPact\Consumer; -use PhpPact\Consumer\Model\Message; +use PhpPact\Consumer\Driver\Interaction\MessageDriverInterface; use PhpPact\Config\PactConfigInterface; -use PhpPact\Standalone\PactMessage\PactMessage; +use PhpPact\Consumer\Factory\MessageDriverFactory; +use PhpPact\Consumer\Factory\MessageDriverFactoryInterface; /** * Build a message and send it to the Ruby Standalone Mock Service */ -class MessageBuilder implements BuilderInterface +class MessageBuilder extends AbstractMessageBuilder { - protected PactMessage $pactMessage; - - protected PactConfigInterface $config; + protected MessageDriverInterface $driver; /** * @var array */ protected array $callback; - private Message $message; - - public function __construct(PactConfigInterface $config) + public function __construct(PactConfigInterface $config, ?MessageDriverFactoryInterface $driverFactory = null) { - $this->config = $config; - $this->message = new Message(); - $this->pactMessage = new PactMessage(); + parent::__construct(); + $this->driver = ($driverFactory ?? new MessageDriverFactory())->create($config); } /** @@ -45,56 +41,14 @@ public function setCallback(callable $callback, ?string $description = null): se return $this; } - /** - * @param string $name what is given to the request - * @param array $params for that request - * @param bool $overwrite clear pass states completely and start this array - */ - public function given(string $name, array $params = [], bool $overwrite = false): self - { - $this->message->setProviderState($name, $params, $overwrite); - - return $this; - } - - /** - * @param string $description what is received when the request is made - */ - public function expectsToReceive(string $description): self - { - $this->message->setDescription($description); - - return $this; - } - - /** - * @param array $metadata what is the additional metadata of the message - */ - public function withMetadata(array $metadata): self - { - $this->message->setMetadata($metadata); - - return $this; - } - - /** - * Make the http request to the Mock Service to register the message. Content is required. - * - * @param mixed $contents required to be in the message - */ - public function withContent($contents): self - { - $this->message->setContents($contents); - - return $this; - } - /** * Run reify to create an example pact from the message (i.e. create messages from matchers) */ public function reify(): string { - return $this->pactMessage->reify($this->message); + $this->driver->registerMessage($this->message); + + return $this->driver->reify(); } /** @@ -107,18 +61,16 @@ public function verifyMessage(callable $callback, ?string $description = null): { $this->setCallback($callback, $description); - return $this->verify($description); + return $this->verify(); } /** * Verify the use of the pact by calling the callback * It also calls finalize to write the pact * - * @param null|string $description description of the pact and thus callback - * * @throws \Exception if callback is not set */ - public function verify(?string $description = null): bool + public function verify(): bool { if (\count($this->callback) < 1) { throw new \Exception('Callbacks need to exist to run verify.'); @@ -133,21 +85,9 @@ public function verify(?string $description = null): bool \call_user_func($callback, $pactJson); } - return $this->writePact(); + return $this->driver->writePactAndCleanUp(); } catch (\Exception $e) { return false; } } - - /** - * Write the Pact without deleting the interactions. - * @throws \JsonException - */ - public function writePact(): bool - { - // you do not want to save the reified json - $pactJson = \json_encode($this->message, JSON_THROW_ON_ERROR); - - return $this->pactMessage->update($pactJson, $this->config->getConsumer(), $this->config->getProvider(), $this->config->getPactDir()); - } } diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index cc5d797a..58d98740 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -2,142 +2,22 @@ namespace PhpPact\Consumer\Model; +use PhpPact\Consumer\Model\Interaction\BodyTrait; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Interaction\HeadersTrait; +use PhpPact\Consumer\Model\Interaction\MethodTrait; +use PhpPact\Consumer\Model\Interaction\PathTrait; +use PhpPact\Consumer\Model\Interaction\QueryTrait; + /** * Request initiated by the consumer. */ -class ConsumerRequest implements \JsonSerializable +class ConsumerRequest { - private string $method; - - /** - * @var string|array - */ - private string|array $path; - - /** - * @var array - */ - private array $headers = []; - - private mixed $body = null; - - private ?string $query = null; - - public function getMethod(): string - { - return $this->method; - } - - public function setMethod(string $method): self - { - $this->method = $method; - - return $this; - } - - /** - * @return string|array - */ - public function getPath(): string|array - { - return $this->path; - } - - /** - * @param string|array $path - */ - public function setPath(string|array $path): self - { - $this->path = $path; - - return $this; - } - - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * @param string[] $headers - */ - public function setHeaders(array $headers): self - { - $this->headers = $headers; - - return $this; - } - - public function addHeader(string $header, string $value): self - { - $this->headers[$header] = $value; - - return $this; - } - - public function getBody(): mixed - { - return $this->body; - } - - public function setBody(mixed $body): self - { - $this->body = $body; - - return $this; - } - - public function getQuery(): ?string - { - return $this->query; - } - - public function setQuery(string $query): self - { - $this->query = $query; - - return $this; - } - - public function addQueryParameter(string $key, string $value): self - { - if ($this->query === null) { - $this->query = "{$key}={$value}"; - } else { - $this->query = "{$this->query}&{$key}={$value}"; - } - - return $this; - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - $results = []; - - $results['method'] = $this->getMethod(); - - if (count($this->getHeaders()) > 0) { - $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; - } + use HeadersTrait; + use BodyTrait; + use ContentTypeTrait; + use MethodTrait; + use PathTrait; + use QueryTrait; } diff --git a/src/PhpPact/Consumer/Model/Interaction.php b/src/PhpPact/Consumer/Model/Interaction.php index 44feaf67..5cc23cab 100644 --- a/src/PhpPact/Consumer/Model/Interaction.php +++ b/src/PhpPact/Consumer/Model/Interaction.php @@ -3,13 +3,13 @@ namespace PhpPact\Consumer\Model; /** - * Request/Response Pair to be posted to the Ruby Standalone Mock Server for PACT tests. + * Request/Response Pair to be posted to the Mock Server for PACT tests. */ -class Interaction implements \JsonSerializable +class Interaction { - private string $description; + use ProviderStates; - private ?string $providerState = null; + private string $description; private ConsumerRequest $request; @@ -27,18 +27,6 @@ public function setDescription(string $description): self return $this; } - public function getProviderState(): ?string - { - return $this->providerState; - } - - public function setProviderState(string $providerState): self - { - $this->providerState = $providerState; - - return $this; - } - public function getRequest(): ConsumerRequest { return $this->request; @@ -62,25 +50,4 @@ public function setResponse(ProviderResponse $response): self return $this; } - - /** - * @return array - */ - public function jsonSerialize(): array - { - if ($this->getProviderState()) { - return [ - 'description' => $this->getDescription(), - 'providerState' => $this->getProviderState(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } - - return [ - 'description' => $this->getDescription(), - 'request' => $this->getRequest(), - 'response' => $this->getResponse(), - ]; - } } diff --git a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php new file mode 100644 index 00000000..3a7c6b37 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php @@ -0,0 +1,36 @@ +body; + } + + /** + * @param array|string|null $body + * + * @throws JsonException + */ + public function setBody(array|string|null $body): self + { + if (\is_string($body) || \is_null($body)) { + $this->body = $body; + } else { + $this->body = \json_encode($body, JSON_THROW_ON_ERROR); + if (!isset($this->contentType)) { + $this->setContentType('application/json'); + } + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php new file mode 100644 index 00000000..257f6cbe --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php @@ -0,0 +1,20 @@ +contentType; + } + + public function setContentType(?string $contentType): self + { + $this->contentType = $contentType; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php new file mode 100644 index 00000000..ebfa0486 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/HeadersTrait.php @@ -0,0 +1,52 @@ + + */ + private array $headers = []; + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): self + { + $this->headers = []; + foreach ($headers as $header => $value) { + $this->addHeader($header, $value); + } + + return $this; + } + + /** + * @param string[]|string $value + */ + public function addHeader(string $header, array|string $value): self + { + $this->headers[$header] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addHeaderValue($header, $value)); + } else { + $this->addHeaderValue($header, $value); + } + + return $this; + } + + private function addHeaderValue(string $header, string $value): void + { + $this->headers[$header][] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php b/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php new file mode 100644 index 00000000..84ab36cc --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/MethodTrait.php @@ -0,0 +1,20 @@ +method; + } + + public function setMethod(string $method): self + { + $this->method = $method; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/PathTrait.php b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php new file mode 100644 index 00000000..9eceebe2 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/PathTrait.php @@ -0,0 +1,27 @@ +path; + } + + /** + * @param string|array $path + * + * @throws JsonException + */ + public function setPath(array|string $path): self + { + $this->path = is_array($path) ? json_encode($path, JSON_THROW_ON_ERROR) : $path; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php new file mode 100644 index 00000000..5d907857 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/QueryTrait.php @@ -0,0 +1,52 @@ + + */ + private array $query = []; + + /** + * @return array + */ + public function getQuery(): array + { + return $this->query; + } + + /** + * @param array $query + */ + public function setQuery(array $query): self + { + $this->query = []; + foreach ($query as $key => $value) { + $this->addQueryParameter($key, $value); + } + + return $this; + } + + /** + * @param string|string[] $value + */ + public function addQueryParameter(string $key, array|string $value): self + { + $this->query[$key] = []; + if (is_array($value)) { + array_walk($value, fn (string $value) => $this->addQueryParameterValue($key, $value)); + } else { + $this->addQueryParameterValue($key, $value); + } + + return $this; + } + + private function addQueryParameterValue(string $key, string $value): void + { + $this->query[$key][] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php new file mode 100644 index 00000000..31e68f4b --- /dev/null +++ b/src/PhpPact/Consumer/Model/Interaction/StatusTrait.php @@ -0,0 +1,20 @@ +status; + } + + public function setStatus(int $status): self + { + $this->status = $status; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 8025b20d..796a683f 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -2,24 +2,25 @@ namespace PhpPact\Consumer\Model; +use JsonException; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; + /** - * Request/Response Pair to be posted to the Ruby Standalone Mock Server for PACT tests. + * Message metadata and contents to be posted to the Mock Server for PACT tests. */ -class Message implements \JsonSerializable +class Message { - private string $description; + use ProviderStates; + use ContentTypeTrait; - /** - * @var array - */ - private array $providerStates = []; + private string $description; /** * @var array */ - private array $metadata; + private array $metadata = []; - private mixed $contents; + private ?string $contents = null; public function getDescription(): string { @@ -33,46 +34,6 @@ public function setDescription(string $description): self return $this; } - /** - * @return array - */ - public function getProviderStates(): array - { - return $this->providerStates; - } - - /** - * @param array $params - * - * @return array - */ - public function setProviderState(string $name, array $params = [], bool $overwrite = true): array - { - $this->addProviderState($name, $params, $overwrite); - - return $this->providerStates; - } - - /** - * @param string $name - * @param array $params - * @param bool $overwrite - if true reset the entire state - */ - public function addProviderState(string $name, array $params, bool $overwrite = false): self - { - $providerState = new \stdClass(); - $providerState->name = $name; - $providerState->params = $params; - - if ($overwrite === true) { - $this->providerStates = []; - } - - $this->providerStates[] = $providerState; - - return $this; - } - /** * @return array */ @@ -86,45 +47,38 @@ public function getMetadata(): array */ public function setMetadata(array $metadata): self { - $this->metadata = $metadata; + $this->metadata = []; + foreach ($metadata as $key => $value) { + $this->setMetadataValue($key, $value); + } return $this; } - public function getContents(): mixed + private function setMetadataValue(string $key, string $value): void { - return $this->contents; + $this->metadata[$key] = $value; } - public function setContents(mixed $contents): self + public function getContents(): ?string { - $this->contents = $contents; - - return $this; + return $this->contents; } /** - * {@inheritdoc} - * - * @return array + * @throws JsonException */ - public function jsonSerialize(): array + public function setContents(mixed $contents): self { - $out = []; - $out['description'] = $this->getDescription(); - - if (\count($this->providerStates) > 0) { - $out['providerStates'] = $this->getProviderStates(); + if (\is_string($contents) || \is_null($contents)) { + $this->contents = $contents; + } else { + $this->contents = \json_encode($contents, JSON_THROW_ON_ERROR); + if (!isset($this->contentType)) { + $this->setContentType('application/json'); + } } - if ($this->metadata) { - $out['metadata'] = $this->getMetadata(); - } - - if ($this->contents) { - $out['contents'] = $this->getContents(); - } - - return $out; + return $this; } } diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 61b19298..3201811f 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -2,95 +2,18 @@ namespace PhpPact\Consumer\Model; +use PhpPact\Consumer\Model\Interaction\BodyTrait; +use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Interaction\HeadersTrait; +use PhpPact\Consumer\Model\Interaction\StatusTrait; + /** * Response expectation that would be in response to a Consumer request from the Provider. */ -class ProviderResponse implements \JsonSerializable +class ProviderResponse { - private int $status; - - /** - * @var array - */ - private array $headers = []; - - /** - * @var ?array - */ - private ?array $body = null; - - public function getStatus(): int - { - return $this->status; - } - - public function setStatus(int $status): self - { - $this->status = $status; - - return $this; - } - - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * @param array $headers - */ - public function setHeaders(array $headers): self - { - $this->headers = $headers; - - return $this; - } - - public function addHeader(string $header, string $value): self - { - $this->headers[$header] = $value; - - return $this; - } - - /** - * @return ?array - */ - public function getBody(): ?array - { - return $this->body; - } - - /** - * @param array $body - */ - public function setBody(array $body): self - { - $this->body = $body; - - return $this; - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - $results = [ - 'status' => $this->getStatus(), - ]; - - if (count($this->getHeaders()) > 0) { - $results['headers'] = $this->getHeaders(); - } - - if ($this->getBody() !== null) { - $results['body'] = $this->getBody(); - } - - return $results; - } + use HeadersTrait; + use BodyTrait; + use ContentTypeTrait; + use StatusTrait; } diff --git a/src/PhpPact/Consumer/Model/ProviderState.php b/src/PhpPact/Consumer/Model/ProviderState.php new file mode 100644 index 00000000..acfa571d --- /dev/null +++ b/src/PhpPact/Consumer/Model/ProviderState.php @@ -0,0 +1,46 @@ + + */ + private array $params = []; + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * @param array $params + */ + public function setParams(array $params = []): void + { + foreach ($params as $key => $value) { + $this->addParam($key, $value); + } + } + + public function addParam(string $key, string $value): void + { + $this->params[$key] = $value; + } +} diff --git a/src/PhpPact/Consumer/Model/ProviderStates.php b/src/PhpPact/Consumer/Model/ProviderStates.php new file mode 100644 index 00000000..09c09ece --- /dev/null +++ b/src/PhpPact/Consumer/Model/ProviderStates.php @@ -0,0 +1,55 @@ + + */ + private array $providerStates = []; + + /** + * @return array + */ + public function getProviderStates(): array + { + return $this->providerStates; + } + + /** + * @param string $name + * @param array $params + * @param bool $overwrite + * + * @return array + */ + public function setProviderState(string $name, array $params = [], bool $overwrite = true): array + { + $this->addProviderState($name, $params, $overwrite); + + return $this->providerStates; + } + + /** + * @param string $name + * @param array $params + * @param bool $overwrite - if true reset the entire state + * + * @return $this + */ + public function addProviderState(string $name, array $params, bool $overwrite = false): self + { + $providerState = new ProviderState(); + $providerState->setName($name); + $providerState->setParams($params); + + if ($overwrite === true) { + $this->providerStates = []; + } + + $this->providerStates[] = $providerState; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php new file mode 100644 index 00000000..82e1d76d --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/AbstractRegistry.php @@ -0,0 +1,24 @@ +id; + } + + abstract protected function newInteraction(string $description): self; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/AbstractBodyRegistry.php new file mode 100644 index 00000000..8f096540 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Contents/AbstractBodyRegistry.php @@ -0,0 +1,29 @@ +client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $contentType, $body); + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php new file mode 100644 index 00000000..3e3394ff --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php @@ -0,0 +1,8 @@ +client->call('pactffi_message_with_contents', $this->messageRegistry->getId(), $contentType, $data->getValue(), $data->getSize()); + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php new file mode 100644 index 00000000..1a4ed86e --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php @@ -0,0 +1,10 @@ +requestRegistry = $requestRegistry ?? new RequestRegistry($client, $this); + $this->responseRegistry = $responseRegistry ?? new ResponseRegistry($client, $this); + } + + public function registerInteraction(Interaction $interaction): bool + { + $this + ->newInteraction($interaction->getDescription()) + ->given($interaction->getProviderStates()) + ->uponReceiving($interaction->getDescription()) + ->with($interaction->getRequest()) + ->willRespondWith($interaction->getResponse()); + + return true; + } + + protected function newInteraction(string $description): self + { + $this->id = $this->client->call('pactffi_new_interaction', $this->pactRegistry->getId(), $description); + + return $this; + } + + private function uponReceiving(string $description): self + { + $this->client->call('pactffi_upon_receiving', $this->id, $description); + + return $this; + } + + /** + * @param ProviderState[] $providerStates + */ + private function given(array $providerStates): self + { + foreach ($providerStates as $providerState) { + $this->client->call('pactffi_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + + return $this; + } + + private function with(ConsumerRequest $request): self + { + $this->requestRegistry + ->withRequest($request->getMethod(), $request->getPath()) + ->withQueryParameters($request->getQuery()) + ->withHeaders($request->getHeaders()) + ->withBody($request->getContentType(), $request->getBody()); + + return $this; + } + + private function willRespondWith(ProviderResponse $response): self + { + $this->responseRegistry + ->withResponse($response->getStatus()) + ->withHeaders($response->getHeaders()) + ->withBody($response->getContentType(), $response->getBody()); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php new file mode 100644 index 00000000..c3f1c7fc --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistryInterface.php @@ -0,0 +1,10 @@ +messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); + } + + + public function registerMessage(Message $message): void + { + $this + ->newInteraction($message->getDescription()) + ->given($message->getProviderStates()) + ->expectsToReceive($message->getDescription()) + ->withMetadata($message->getMetadata()) + ->withContents($message->getContentType(), $message->getContents()); + } + + protected function newInteraction(string $description): self + { + $this->id = $this->client->call('pactffi_new_message_interaction', $this->pactRegistry->getId(), $description); + + return $this; + } + + private function withContents(?string $contentType = null, ?string $contents = null): self + { + $this->messageContentsRegistry->withContents($contentType, $contents); + + return $this; + } + + private function expectsToReceive(string $description): self + { + $this->client->call('pactffi_message_expects_to_receive', $this->id, $description); + + return $this; + } + + /** + * @param ProviderState[] $providerStates + */ + private function given(array $providerStates): self + { + foreach ($providerStates as $providerState) { + $this->client->call('pactffi_message_given', $this->id, $providerState->getName()); + foreach ($providerState->getParams() as $key => $value) { + $this->client->call('pactffi_message_given_with_param', $this->id, $providerState->getName(), (string) $key, (string) $value); + } + } + + return $this; + } + + /** + * @param array $metadata + */ + private function withMetadata(array $metadata): self + { + foreach ($metadata as $key => $value) { + $this->client->call('pactffi_message_with_metadata', $this->id, (string) $key, (string) $value); + } + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php new file mode 100644 index 00000000..ca3af5c2 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistryInterface.php @@ -0,0 +1,10 @@ +contentsRegistry->withContents($contentType, $body); + + return $this; + } + + public function withHeaders(array $headers): self + { + foreach ($headers as $header => $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_header_v2', $this->getInteractionId(), $this->getPart(), (string) $header, (int) $index, (string) $value); + } + } + + return $this; + } + + protected function getInteractionId(): int + { + return $this->interactionRegistry->getId(); + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php new file mode 100644 index 00000000..90a6516b --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php @@ -0,0 +1,13 @@ + $headers + */ + public function withHeaders(array $headers): self; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestPartTrait.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestPartTrait.php new file mode 100644 index 00000000..89897944 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestPartTrait.php @@ -0,0 +1,11 @@ +client->get('InteractionPart_Request'); + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php new file mode 100644 index 00000000..026dc57c --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php @@ -0,0 +1,39 @@ + $values) { + foreach (array_values($values) as $index => $value) { + $this->client->call('pactffi_with_query_parameter_v2', $this->getInteractionId(), (string) $key, (int) $index, (string) $value); + } + } + + return $this; + } + + public function withRequest(string $method, string $path): self + { + $this->client->call('pactffi_with_request', $this->getInteractionId(), $method, $path); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php new file mode 100644 index 00000000..548c964f --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistryInterface.php @@ -0,0 +1,13 @@ + $queryParams + */ + public function withQueryParameters(array $queryParams): self; + + public function withRequest(string $method, string $path): self; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php new file mode 100644 index 00000000..389fe949 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponsePartTrait.php @@ -0,0 +1,11 @@ +client->get('InteractionPart_Response'); + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php new file mode 100644 index 00000000..3649a408 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php @@ -0,0 +1,28 @@ +client->call('pactffi_response_status', $this->getInteractionId(), $status); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php new file mode 100644 index 00000000..05f56e52 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistryInterface.php @@ -0,0 +1,8 @@ +id)) { + throw new PactNotRegisteredException('New pact must be registered.'); + } + return $this->id; + } + + public function deletePact(): void + { + $this->client->call('pactffi_free_pact_handle', $this->id); + unset($this->id); + } + + public function registerPact(string $consumer, string $provider, int $specification): void + { + $this + ->newPact($consumer, $provider) + ->withSpecification($specification); + } + + private function newPact(string $consumer, string $provider): self + { + $this->id = $this->client->call('pactffi_new_pact', $consumer, $provider); + + return $this; + } + + private function withSpecification(int $specification): self + { + $this->client->call('pactffi_with_specification', $this->id, $specification); + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php b/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php new file mode 100644 index 00000000..c9ffb883 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Pact/PactRegistryInterface.php @@ -0,0 +1,12 @@ +client->call( + 'pactffi_create_mock_server_for_transport', + $this->pactRegistry->getId(), + $this->config->getHost(), + $this->config->getPort(), + $this->getTransport(), + $this->getTransportConfig() + ); + + if ($port < 0) { + throw new MockServerNotStartedException($port); + } + $this->config->setPort($port); + } + + public function verify(): bool + { + $matched = $this->client->call('pactffi_mock_server_matched', $this->config->getPort()); + + try { + if ($matched) { + $this->writePact(); + } + } finally { + $this->cleanUp(); + } + + return $matched; + } + + protected function getTransport(): string + { + return $this->config->isSecure() ? 'https' : 'http'; + } + + protected function getTransportConfig(): ?string + { + return null; + } + + private function writePact(): void + { + $error = $this->client->call( + 'pactffi_write_pact_file', + $this->config->getPort(), + $this->config->getPactDir(), + $this->config->getPactFileWriteMode() === PactConfigInterface::MODE_OVERWRITE + ); + if ($error) { + throw new MockServerNotWrotePactFileException($error); + } + } + + private function cleanUp(): void + { + $this->client->call('pactffi_cleanup_mock_server', $this->config->getPort()); + $this->pactRegistry->deletePact(); + } +} diff --git a/src/PhpPact/Consumer/Service/MockServerInterface.php b/src/PhpPact/Consumer/Service/MockServerInterface.php new file mode 100644 index 00000000..5737269c --- /dev/null +++ b/src/PhpPact/Consumer/Service/MockServerInterface.php @@ -0,0 +1,10 @@ +ffi = FFI::cdef($code, Scripts::getLibrary()); + } + + public function call(string $name, ...$arguments): mixed + { + return $this->ffi->{$name}(...$arguments); + } + + public function get(string $name): mixed + { + return $this->ffi->{$name}; + } +} diff --git a/src/PhpPact/FFI/ClientInterface.php b/src/PhpPact/FFI/ClientInterface.php new file mode 100644 index 00000000..c0e767ff --- /dev/null +++ b/src/PhpPact/FFI/ClientInterface.php @@ -0,0 +1,13 @@ + $arguments + */ + public function call(string $name, ...$arguments): mixed; + + public function get(string $name): mixed; +} diff --git a/src/PhpPact/FFI/Exception/CDataNotCreatedException.php b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php new file mode 100644 index 00000000..2e17f429 --- /dev/null +++ b/src/PhpPact/FFI/Exception/CDataNotCreatedException.php @@ -0,0 +1,9 @@ +items; + } + + public function getSize(): int + { + return $this->size; + } + + /** + * @param array $values + */ + public static function createFrom(array $values): ?self + { + $size = count($values); + if ($size === 0) { + return null; + } + + $items = FFI::new("char*[{$size}]"); + if ($items === null) { + return throw new CDataNotCreatedException(); + } + foreach ($values as $index => $value) { + $length = \strlen($value); + $itemSize = $length + 1; + $item = FFI::new("char[{$itemSize}]", false); + if ($item === null) { + return throw new CDataNotCreatedException(); + } + FFI::memcpy($item, $value, $length); + $items[$index] = $item; // @phpstan-ignore-line + } + + return new self($items, $size); + } + + public function __destruct() + { + for ($i=0; $i < $this->size; $i++) { + FFI::free($this->items[$i]); // @phpstan-ignore-line + } + } +} diff --git a/src/PhpPact/FFI/Model/StringData.php b/src/PhpPact/FFI/Model/StringData.php new file mode 100644 index 00000000..622f457a --- /dev/null +++ b/src/PhpPact/FFI/Model/StringData.php @@ -0,0 +1,35 @@ +value; + } + + public function getSize(): int + { + return $this->size; + } + + public static function createFrom(string $value): ?self + { + $length = \strlen($value); + $size = $length + 1; + $cData = FFI::new("uint8_t[{$size}]"); + FFI::memcpy($cData, $value, $length); + + return new self($cData, $size); + } +} diff --git a/src/PhpPact/Provider/MessageVerifier.php b/src/PhpPact/Provider/MessageVerifier.php deleted file mode 100644 index 7c5a7113..00000000 --- a/src/PhpPact/Provider/MessageVerifier.php +++ /dev/null @@ -1,208 +0,0 @@ - */ - protected array $callbacks = []; - - /** - * Default host name for the proxy server - */ - protected string $defaultProxyHost = 'localhost'; - - /** - * Default port for the proxy server to listen on - */ - protected int $defaultProxyPort = 7201; - - /** - * floor(provider-verification timeout / this value) = default verificationDelaySec - */ - protected int $defaultDelayFactor = 3; - - /** - * Set the number of seconds to delay the verification test to allow the proxy server to be stood up - * - * By default, it is a third of the provider-verification timeout - */ - protected float $verificationDelaySec; - - private ?LoggerInterface $logger = null; - - public function __construct(VerifierConfigInterface $config) - { - parent::__construct($config); - - $this->callbacks = []; - - $baseUrl = $this->config->getProviderBaseUrl(); - if ($baseUrl === null) { - $config->setProviderBaseUrl(new Uri("http://{$this->defaultProxyHost}:{$this->defaultProxyPort}")); - } - - // default verification delay - $this->setVerificationDelaySec(\floor($config->getProcessIdleTimeout() / $this->defaultDelayFactor)); - } - - /** - * @param array $callbacks - */ - public function setCallbacks(array $callbacks): self - { - $this->callbacks = $callbacks; - - return $this; - } - - /** - * Add an individual call back - * - * @throws \Exception - */ - public function addCallback(string $key, callable $callback): self - { - if (isset($this->callbacks[$key])) { - throw new \Exception("Callback with key ($key) already exists"); - } - - $this->callbacks[$key] = $callback; - - return $this; - } - - public function setVerificationDelaySec(float $verificationDelaySec): self - { - $this->verificationDelaySec = $verificationDelaySec; - - return $this; - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * @throws \Exception - */ - protected function verifyAction(array $arguments): void - { - if (\count($this->callbacks) < 1) { - throw new \Exception('Callback needs to bet set when using message pacts'); - } - - $callbacks = $this->callbacks; - $uri = $this->config->getProviderBaseUrl(); - - $arguments = \array_merge([Scripts::getProviderVerifier()], $arguments); - - /** - * @throws \Amp\Socket\SocketException - * @throws \Error - * @throws \TypeError - * - * @return \Generator - */ - $lambdaLoop = function () use ($callbacks, $arguments, $uri) { - // spin up a server - $url = "{$uri->getHost()}:{$uri->getPort()}"; - $servers = [ - Socket\Server::listen($url) - ]; - - $logger = $this->getLogger(); - - $server = new Server($servers, new CallableRequestHandler(function (Request $request) use ($callbacks) { - if (\count($callbacks) === 1) { - $callback = \array_pop($callbacks); - } else { - $payload = new Payload($request->getBody()); - $requestBody = yield $payload->buffer(); - $requestBody = \json_decode($requestBody); - $description = $requestBody->description; - - $callback = false; - - if (isset($this->callbacks[$description])) { - $callback = $this->callbacks[$description]; - } - - if ($callback === false) { - throw new \Exception("Pacts with multiple states need to have callbacks key'ed by the description"); - } - } - - //@todo pass $providerStates to the call back - $out = \call_user_func($callback); - - // return response should only happen if the \call_user_fun() - return new Response(Status::OK, [ - 'content-type' => 'application/json;', - ], $out); - }), $logger); - - yield $server->start(); - - // delay long enough for the server to be stood up - $delay = (int) ($this->verificationDelaySec * 1000); - - // call the provider-verification cmd - Loop::delay($delay, function () use ($arguments) { - $cmd = \implode(' ', $arguments); - $process = new Process($cmd); - yield $process->start(); - - $payload = new Payload($process->getStdout()); - print yield $payload->buffer(); - - $code = yield $process->join(); - - // if the provider verification cmd returns a non-zero number, the test failed - if ($code !== 0) { - $this->getLogger()->warning(yield $process->getStderr()->read()); - - throw new \Exception("Pact failed to validate. Exit code: {$code}"); - } - - Loop::stop(); - }); - }; - - Loop::run($lambdaLoop); - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('server'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/Broker/Broker.php b/src/PhpPact/Standalone/Broker/Broker.php deleted file mode 100644 index f7caa83e..00000000 --- a/src/PhpPact/Standalone/Broker/Broker.php +++ /dev/null @@ -1,260 +0,0 @@ -config = $config; - $this->command = Scripts::getBroker(); - } - - /** - * @throws \Exception - */ - public function canIDeploy(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'can-i-deploy', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--version=' . $this->config->getVersion() - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @return array parameters to be passed into the process - */ - public function getArguments(): array - { - $parameters = []; - - if ($this->config->getBrokerUri() !== null) { - $parameters[] = "--broker-base-url={$this->config->getBrokerUri()}"; - } - - if ($this->config->getBrokerToken() !== null) { - $parameters[] = "--broker-token={$this->config->getBrokerToken()}"; - } - - if ($this->config->getBrokerUsername() !== null) { - $parameters[] = "--broker-username={$this->config->getBrokerUsername()}"; - } - - if ($this->config->getBrokerPassword() !== null) { - $parameters[] = "--broker-password={$this->config->getBrokerPassword()}"; - } - - return $parameters; - } - - /** - * @throws \Exception - */ - public function createOrUpdatePacticipant(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-or-update-pacticipant', - '--name=' . $this->config->getName(), - '--repository-url=' . $this->config->getRepositoryUrl(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @throws \Exception - */ - public function createOrUpdateWebhook(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-or-update-webhook', - $this->config->getUrl(), - '--request=' . $this->config->getRequest(), - '--header=' . $this->config->getHeader(), - '--data=' . $this->config->getData(), - '--user=' . $this->config->getUser(), - '--consumer=' . $this->config->getConsumer(), - '--provider=' . $this->config->getProvider(), - '--description=' . $this->config->getDescription(), - '--uuid=' . $this->config->getUuid(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @throws \Exception - */ - public function createVersionTag(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-version-tag', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--version=' . $this->config->getVersion(), - '--tag=' . $this->config->getTag(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @throws \Exception - */ - public function createWebhook(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'create-webhook', - $this->config->getUrl(), - '--request=' . $this->config->getRequest(), - '--header=' . $this->config->getHeader(), - '--data=' . $this->config->getData(), - '--user=' . $this->config->getUser(), - '--consumer=' . $this->config->getConsumer(), - '--provider=' . $this->config->getProvider(), - '--description=' . $this->config->getDescription(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @throws \Exception - */ - public function describeVersion(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'describe-version', - '--pacticipant=\'' . $this->config->getPacticipant().'\'', - '--output=json', - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - /** - * @throws \Exception - */ - public function listLatestPactVersions(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'list-latest-pact-versions', - '--output=json', - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - public function publish(): void - { - $options = [ - 'publish', - $this->config->getPactLocations(), - '--consumer-app-version=' . $this->config->getConsumerVersion(), - ]; - - if (null !== $this->config->getBranch()) { - $options[] = '--branch=' . $this->config->getBranch(); - } - - if (null !== $this->config->getTag()) { - $options[] = '--tag=' . $this->config->getTag(); - } - - $runner = new ProcessRunner( - $this->command, - \array_merge( - $options, - $this->getArguments() - ) - ); - - $runner->runBlocking(); - } - - /** - * @throws \Exception - */ - public function testWebhook(): mixed - { - $runner = new ProcessRunner( - $this->command, - \array_merge( - [ - 'test-webhook', - '--uuid=' . $this->config->getUuid(), - ], - $this->getArguments() - ) - ); - $runner->runBlocking(); - - return \json_decode($runner->getOutput(), true, 512, JSON_THROW_ON_ERROR); - } - - public function generateUuid(): string - { - $runner = new ProcessRunner($this->command, ['generate-uuid']); - $runner->runBlocking(); - - return \rtrim($runner->getOutput()); - } -} diff --git a/src/PhpPact/Standalone/Broker/BrokerConfig.php b/src/PhpPact/Standalone/Broker/BrokerConfig.php deleted file mode 100644 index 1ab80b26..00000000 --- a/src/PhpPact/Standalone/Broker/BrokerConfig.php +++ /dev/null @@ -1,309 +0,0 @@ -repositoryUrl; - } - - public function setRepositoryUrl(?string $repositoryUrl): self - { - $this->repositoryUrl = $repositoryUrl; - - return $this; - } - - public function getUrl(): ?string - { - return $this->url; - } - - public function setUrl(?string $url): self - { - $this->url = $url; - - return $this; - } - - public function getVersion(): ?string - { - return $this->version; - } - - public function setVersion(?string $version): self - { - $this->version = $version; - - return $this; - } - - public function getBranch(): ?string - { - return $this->branch; - } - - public function setBranch(?string $branch): self - { - $this->branch = $branch; - - return $this; - } - - public function getTag(): ?string - { - return $this->tag; - } - - public function setTag(?string $tag): self - { - $this->tag = $tag; - - return $this; - } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): self - { - $this->name = $name; - - return $this; - } - - public function getRequest(): ?string - { - return $this->request; - } - - public function setRequest(?string $request): self - { - $this->request = $request; - - return $this; - } - - public function getHeader(): ?string - { - return $this->header; - } - - public function setHeader(?string $header): self - { - $this->header = $header; - - return $this; - } - - public function getData(): ?string - { - return $this->data; - } - - public function setData(?string $data): self - { - $this->data = $data; - - return $this; - } - - public function getUser(): ?string - { - return $this->user; - } - - public function setUser(?string $user): self - { - $this->user = $user; - - return $this; - } - - public function getConsumer(): ?string - { - return $this->consumer; - } - - public function setConsumer(?string $consumer): self - { - $this->consumer = $consumer; - - return $this; - } - - public function getProvider(): ?string - { - return $this->provider; - } - - public function setProvider(?string $provider): self - { - $this->provider = $provider; - - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): self - { - $this->description = $description; - - return $this; - } - - public function getUuid(): ?string - { - return $this->uuid; - } - - public function setUuid(?string $uuid): self - { - $this->uuid = $uuid; - - return $this; - } - - public function isVerbose(): bool - { - return $this->verbose; - } - - public function getBrokerUri(): ?UriInterface - { - return $this->brokerUri; - } - - public function setBrokerUri(?UriInterface $brokerUri): self - { - $this->brokerUri = $brokerUri; - - return $this; - } - - public function getBrokerToken(): ?string - { - return $this->brokerToken; - } - - public function setBrokerToken(?string $brokerToken): self - { - $this->brokerToken = $brokerToken; - - return $this; - } - - public function getBrokerUsername(): ?string - { - return $this->brokerUsername; - } - - public function setBrokerUsername(?string $brokerUsername): self - { - $this->brokerUsername = $brokerUsername; - - return $this; - } - - public function getBrokerPassword(): ?string - { - return $this->brokerPassword; - } - - public function setBrokerPassword(?string $brokerPassword): self - { - $this->brokerPassword = $brokerPassword; - - return $this; - } - - public function getPacticipant(): string - { - return $this->pacticipant; - } - - public function setPacticipant(?string $pacticipant): self - { - $this->pacticipant = $pacticipant; - - return $this; - } - - public function getConsumerVersion(): ?string - { - return $this->consumerVersion; - } - - public function setConsumerVersion(?string $consumerVersion): self - { - $this->consumerVersion = $consumerVersion; - - return $this; - } - - public function getPactLocations(): ?string - { - return $this->pactLocations; - } - - public function setPactLocations(string $locations): self - { - $this->pactLocations = $locations; - - return $this; - } -} diff --git a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php b/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php deleted file mode 100644 index a2e98a23..00000000 --- a/src/PhpPact/Standalone/Exception/HealthCheckFailedException.php +++ /dev/null @@ -1,16 +0,0 @@ -config = $config; - $this->httpService = $httpService ?: new MockServerHttpService(new GuzzleClient(), $this->config); - } - - /** - * Start the Mock Server. Verify that it is running. - * - * @throws Exception - * - * @return int process ID of the started Mock Server - */ - public function start(): int - { - $this->processRunner = new ProcessRunner(Scripts::getMockService(), $this->getArguments()); - - $processId = $this->processRunner->run(); - - $result = $this->verifyHealthCheck(); - if ($result) { - $retrySec = $this->config->getHealthCheckRetrySec(); - \sleep($retrySec); - } - - return $processId; - } - - /** - * Stop the Mock Server process. - * - * @return bool Was stopping successful? - * @throws ProcessException - */ - public function stop(): bool - { - return $this->processRunner->stop(); - } - - /** - * Build an array of command arguments. - * - * @return array - */ - private function getArguments(): array - { - $results = []; - - $logLevel = $this->config->getLogLevel(); - $consumer = \escapeshellarg($this->config->getConsumer()); - $provider = \escapeshellarg($this->config->getProvider()); - $pactDir = \escapeshellarg($this->config->getPactDir()); - - $results[] = 'service'; - $results[] = "--consumer={$consumer}"; - $results[] = "--provider={$provider}"; - $results[] = "--pact-dir={$pactDir}"; - $results[] = "--pact-file-write-mode={$this->config->getPactFileWriteMode()}"; - $results[] = "--host={$this->config->getHost()}"; - $results[] = "--port={$this->config->getPort()}"; - - if ($logLevel !== null) { - $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 ($this->config->getLog() !== null) { - $log = \escapeshellarg($this->config->getLog()); - $results[] = \sprintf('--log=%s', $log); - } - - return $results; - } - - /** - * Make sure the server starts as expected. - * - * @throws Exception - */ - 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 { - return $service->healthCheck(); - } catch (ConnectionException $e) { - \sleep($retrySec); - } - } while ($tries <= $maxTries); - - 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 5c0ce508..fb062e6f 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfig.php @@ -17,23 +17,14 @@ class MockServerConfig extends PactConfig implements MockServerConfigInterface private string $host = 'localhost'; /** - * Port on which to run the service. + * Port on which to run the service. A value of zero will result in the operating system allocating an available port. */ - private int $port = 7200; - - private bool $secure = false; - - private bool $cors = false; - - /** - * The max allowed attempts the mock server has to be available in. Otherwise it is considered as sick. - */ - private int $healthCheckTimeout; + private int $port = 0; /** - * The seconds between health checks of mock server + * @var bool */ - private int $healthCheckRetrySec; + private bool $secure = false; /** * {@inheritdoc} @@ -98,46 +89,4 @@ public function getBaseUri(): UriInterface return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); } - - public function hasCors(): bool - { - return $this->cors; - } - - public function setCors(mixed $flag): self - { - if ($flag === 'true') { - $this->cors = true; - } elseif ($flag === 'false') { - $this->cors = false; - } else { - $this->cors = (bool) $flag; - } - - return $this; - } - - public function setHealthCheckTimeout(int $timeout): MockServerConfigInterface - { - $this->healthCheckTimeout = $timeout; - - return $this; - } - - public function getHealthCheckTimeout(): int - { - return $this->healthCheckTimeout; - } - - public function setHealthCheckRetrySec(int $seconds): MockServerConfigInterface - { - $this->healthCheckRetrySec = $seconds; - - return $this; - } - - public function getHealthCheckRetrySec(): int - { - return $this->healthCheckRetrySec; - } } diff --git a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php index 2682d875..3b25bfe4 100644 --- a/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php +++ b/src/PhpPact/Standalone/MockService/MockServerConfigInterface.php @@ -2,12 +2,13 @@ namespace PhpPact\Standalone\MockService; +use PhpPact\Config\PactConfigInterface; use Psr\Http\Message\UriInterface; /** * Mock Server configuration interface to allow for simple overrides that are reusable. */ -interface MockServerConfigInterface +interface MockServerConfigInterface extends PactConfigInterface { /** * @return string the host of the mock service @@ -40,16 +41,4 @@ public function isSecure(): bool; public function setSecure(bool $secure): self; public function getBaseUri(): UriInterface; - - public function hasCors(): bool; - - public function setCors(mixed $flag): self; - - public function setHealthCheckTimeout(int $timeout): self; - - public function getHealthCheckTimeout(): int; - - public function setHealthCheckRetrySec(int $seconds): self; - - public function getHealthCheckRetrySec(): int; } diff --git a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php index 7a832881..a8183b2a 100644 --- a/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php +++ b/src/PhpPact/Standalone/MockService/MockServerEnvConfig.php @@ -14,12 +14,17 @@ class MockServerEnvConfig extends MockServerConfig */ public function __construct() { - $this->setHost($this->parseEnv('PACT_MOCK_SERVER_HOST')); - $this->setPort((int) $this->parseEnv('PACT_MOCK_SERVER_PORT')); + if ($host = $this->parseEnv('PACT_MOCK_SERVER_HOST', false)) { + $this->setHost($host); + } + + if ($port = $this->parseEnv('PACT_MOCK_SERVER_PORT', false)) { + $this->setPort((int) $port); + } + $this->setConsumer($this->parseEnv('PACT_CONSUMER_NAME')); $this->setProvider($this->parseEnv('PACT_PROVIDER_NAME')); $this->setPactDir($this->parseEnv('PACT_OUTPUT_DIR', false)); - $this->setCors($this->parseEnv('PACT_CORS', false)); if ($logDir = $this->parseEnv('PACT_LOG', false)) { $this->setLog($logDir); @@ -29,18 +34,6 @@ public function __construct() $this->setLogLevel($logLevel); } - $timeout = $this->parseEnv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT', false); - if (!$timeout) { - $timeout = 10; - } - $this->setHealthCheckTimeout($timeout); - - $seconds = $this->parseEnv('PACT_MOCK_SERVER_HEALTH_CHECK_RETRY_SEC', false); - if (!$seconds) { - $seconds = 1; - } - $this->setHealthCheckRetrySec($seconds); - $version = $this->parseEnv('PACT_SPECIFICATION_VERSION', false); if (!$version) { $version = static::DEFAULT_SPECIFICATION_VERSION; @@ -54,17 +47,12 @@ public function __construct() * * @throws MissingEnvVariableException */ - private function parseEnv(string $variableName, bool $required = true): mixed + private function parseEnv(string $variableName, bool $required = true): ?string { - $result = null; + $result = \getenv($variableName); - if (\getenv($variableName) === 'false') { - $result = false; - } elseif (\getenv($variableName) === 'true') { - $result = true; - } - if (\getenv($variableName) !== false) { - $result = \getenv($variableName); + if (is_bool($result)) { + $result = null; } if ($required === true && $result === null) { diff --git a/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php b/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php deleted file mode 100644 index c41bb2df..00000000 --- a/src/PhpPact/Standalone/MockService/Service/MockServerHttpService.php +++ /dev/null @@ -1,166 +0,0 @@ -client = $client; - $this->config = $config; - } - - /** - * {@inheritdoc} - */ - public function healthCheck(): bool - { - $uri = $this->config->getBaseUri()->withPath('/'); - - try { - $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.'); - } - } catch (RequestException $e) { - throw new ConnectionException('Failed to receive a successful response from the Mock Server.', $e); - } catch (GuzzleConnectionException $e) { - throw new ConnectionException('Failed to receive a successful response from the Mock Server.', $e); - } - - 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(), JSON_THROW_ON_ERROR); - - $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 - */ - public function registerMessage(Message $message): bool - { - $uri = $this->config->getBaseUri()->withPath('/interactions'); - - $body = \json_encode($message->jsonSerialize(), JSON_THROW_ON_ERROR); - - $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} - * @throws \JsonException - */ - 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()), JSON_THROW_ON_ERROR); - } - - /** - * 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. - * @throws \JsonException - */ - 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 caf095f5..00000000 --- a/src/PhpPact/Standalone/MockService/Service/MockServerHttpServiceInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -runBlocking(); - - $output = $process->getOutput(); - \preg_replace("/\r|\n/", '', $output); - - return $output; - } - - /** - * Update a pact with the given message, or create the pact if it does not exist. The MESSAGE_JSON may be in the legacy Ruby JSON format or the v2+ format. - */ - public function update(string $pactJson, string $consumer, string $provider, string $pactDir): bool - { - $arguments = []; - $arguments[] = 'update'; - $arguments[] = "--consumer={$consumer}"; - $arguments[] = "--provider={$provider}"; - $arguments[] = "--pact-dir={$pactDir}"; - $arguments[] = "'" . $pactJson . "'"; - - $process = new ProcessRunner(Scripts::getPactMessage(), $arguments); - $process->runBlocking(); - - \sleep(1); - - return true; - } -} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php new file mode 100644 index 00000000..2b7630fa --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingApp.php @@ -0,0 +1,33 @@ +name; + } + + public function setName(?string $name): CallingAppInterface + { + $this->name = $name; + + return $this; + } + + public function getVersion(): ?string + { + return $this->version; + } + + public function setVersion(?string $version): CallingAppInterface + { + $this->version = $version; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php new file mode 100644 index 00000000..bae0ec05 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/CallingAppInterface.php @@ -0,0 +1,14 @@ + + */ + private array $filterConsumerNames = []; + + public function setFilterConsumerNames(array $filterConsumerNames): self + { + $this->filterConsumerNames = []; + foreach ($filterConsumerNames as $filterConsumerName) { + $this->addFilterConsumerName($filterConsumerName); + } + + return $this; + } + + public function addFilterConsumerName(string $filterConsumerName): self + { + $this->filterConsumerNames[] = $filterConsumerName; + + return $this; + } + + public function getFilterConsumerNames(): array + { + return $this->filterConsumerNames; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php new file mode 100644 index 00000000..91402e7b --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ConsumerFiltersInterface.php @@ -0,0 +1,18 @@ + $filterConsumerNames + */ + public function setFilterConsumerNames(array $filterConsumerNames): self; + + public function addFilterConsumerName(string $filterConsumerName): self; + + /** + * @return array + */ + public function getFilterConsumerNames(): array; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php new file mode 100644 index 00000000..1f5cdce6 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfo.php @@ -0,0 +1,46 @@ +filterDescription; + } + + public function setFilterDescription(?string $filterDescription): self + { + $this->filterDescription = $filterDescription; + + return $this; + } + + public function getFilterNoState(): bool + { + return $this->filterNoState; + } + + public function setFilterNoState(bool $filterNoState): self + { + $this->filterNoState = $filterNoState; + + return $this; + } + + public function getFilterState(): ?string + { + return $this->filterState; + } + + public function setFilterState(?string $filterState): self + { + $this->filterState = $filterState; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php new file mode 100644 index 00000000..51420ae8 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/FilterInfoInterface.php @@ -0,0 +1,18 @@ +pluginDir; + } + + public function setPluginDir(?string $pluginDir): self + { + $this->pluginDir = $pluginDir; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php new file mode 100644 index 00000000..375af81c --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfo.php @@ -0,0 +1,72 @@ +name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getHost(): ?string + { + return $this->host; + } + + public function setHost(string $host): self + { + $this->host = $host; + + return $this; + } + + public function getScheme(): ?string + { + return $this->scheme; + } + + public function setScheme(?string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function setPort(?int $port): self + { + $this->port = $port; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): self + { + $this->path = $path; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php new file mode 100644 index 00000000..d76d297a --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderInfoInterface.php @@ -0,0 +1,26 @@ +stateChangeUrl; + } + + public function setStateChangeUrl(?UriInterface $stateChangeUrl): self + { + $this->stateChangeUrl = $stateChangeUrl; + + return $this; + } + + public function setStateChangeAsBody(bool $stateChangeAsBody): self + { + $this->stateChangeAsBody = $stateChangeAsBody; + + return $this; + } + + public function isStateChangeAsBody(): bool + { + return $this->stateChangeAsBody; + } + + public function setStateChangeTeardown(bool $stateChangeTeardown): self + { + $this->stateChangeTeardown = $stateChangeTeardown; + + return $this; + } + + public function isStateChangeTeardown(): bool + { + return $this->stateChangeTeardown; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php new file mode 100644 index 00000000..a51f8619 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderStateInterface.php @@ -0,0 +1,20 @@ +protocol; + } + + public function setProtocol(?string $protocol): self + { + $this->protocol = $protocol; + + return $this; + } + + public function getScheme(): ?string + { + return $this->scheme; + } + + public function setScheme(?string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function setPort(?int $port): self + { + $this->port = $port; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(?string $path): self + { + $this->path = $path; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php new file mode 100644 index 00000000..ad269b16 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/ProviderTransportInterface.php @@ -0,0 +1,28 @@ + + */ + private array $providerTags = []; + private string $providerVersion; + private ?UriInterface $buildUrl = null; + private ?string $providerBranch = null; + + public function getProviderTags(): array + { + return $this->providerTags; + } + + public function setProviderTags(array $providerTags): self + { + $this->providerTags = []; + foreach ($providerTags as $providerTag) { + $this->addProviderTag($providerTag); + } + + return $this; + } + + public function addProviderTag(string $providerTag): self + { + $this->providerTags[] = $providerTag; + + return $this; + } + + public function getProviderVersion(): string + { + return $this->providerVersion; + } + + public function setProviderVersion(string $providerVersion): self + { + $this->providerVersion = $providerVersion; + + return $this; + } + + public function getBuildUrl(): ?UriInterface + { + return $this->buildUrl; + } + + public function setBuildUrl(?UriInterface $buildUrl): self + { + $this->buildUrl = $buildUrl; + + return $this; + } + + public function getProviderBranch(): ?string + { + return $this->providerBranch; + } + + public function setProviderBranch(?string $providerBranch): self + { + $this->providerBranch = $providerBranch; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php new file mode 100644 index 00000000..00429e89 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/PublishOptionsInterface.php @@ -0,0 +1,32 @@ + + */ + public function getProviderTags(): array; + + /** + * @param array $providerTags + */ + public function setProviderTags(array $providerTags): self; + + public function addProviderTag(string $providerTag): self; + + public function getProviderVersion(): string; + + public function setProviderVersion(string $providerVersion): self; + + public function getBuildUrl(): ?UriInterface; + + public function setBuildUrl(UriInterface $buildUrl): self; + + public function getProviderBranch(): ?string; + + public function setProviderBranch(?string $providerBranch): self; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php new file mode 100644 index 00000000..5e6c25a8 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptions.php @@ -0,0 +1,33 @@ +disableSslVerification; + } + + public function setDisableSslVerification(bool $disableSslVerification): self + { + $this->disableSslVerification = $disableSslVerification; + + return $this; + } + + public function setRequestTimeout(int $requestTimeout): self + { + $this->requestTimeout = $requestTimeout; + + return $this; + } + + public function getRequestTimeout(): int + { + return $this->requestTimeout; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php new file mode 100644 index 00000000..093fbbf7 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Config/VerificationOptionsInterface.php @@ -0,0 +1,14 @@ +> */ - private array $selectors; + /** @var array */ + private array $selectors = []; /** * @param array $selectors @@ -30,37 +30,31 @@ public function addSelector(string $selector): self return $this; } - #[\ReturnTypeWillChange] - public function current() + public function current(): string { return $this->selectors[$this->position]; } - #[\ReturnTypeWillChange] - public function next() + public function next(): void { ++$this->position; } - #[\ReturnTypeWillChange] public function key(): int { return $this->position; } - #[\ReturnTypeWillChange] public function valid(): bool { return isset($this->selectors[$this->position]); } - #[\ReturnTypeWillChange] - public function rewind() + public function rewind(): void { $this->position = 0; } - #[\ReturnTypeWillChange] public function count(): int { return \count($this->selectors); diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php new file mode 100644 index 00000000..8b1cc29d --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Broker.php @@ -0,0 +1,118 @@ + + */ + private array $providerTags = []; + protected ?string $providerBranch = null; + protected ConsumerVersionSelectors $consumerVersionSelectors; + /** + * @var array + */ + private array $consumerVersionTags = []; + + public function __construct() + { + $this->consumerVersionSelectors = new ConsumerVersionSelectors(); + } + + public function isEnablePending(): bool + { + return $this->enablePending; + } + + public function setEnablePending(bool $enablePending): self + { + $this->enablePending = $enablePending; + + return $this; + } + + public function setIncludeWipPactSince(?string $date): self + { + $this->wipPactSince = $date; + + return $this; + } + + public function getIncludeWipPactSince(): ?string + { + return $this->wipPactSince; + } + + public function getProviderTags(): array + { + return $this->providerTags; + } + + public function setProviderTags(array $providerTags): self + { + $this->providerTags = []; + foreach ($providerTags as $providerTag) { + $this->addProviderTag($providerTag); + } + + return $this; + } + + public function addProviderTag(string $providerTag): self + { + $this->providerTags[] = $providerTag; + + return $this; + } + + public function getProviderBranch(): ?string + { + return $this->providerBranch; + } + + public function setProviderBranch(?string $providerBranch): self + { + $this->providerBranch = $providerBranch; + + return $this; + } + + public function getConsumerVersionSelectors(): ConsumerVersionSelectors + { + return $this->consumerVersionSelectors; + } + + public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self + { + $this->consumerVersionSelectors = $selectors; + + return $this; + } + + public function getConsumerVersionTags(): array + { + return $this->consumerVersionTags; + } + + public function setConsumerVersionTags(array $consumerVersionTags): self + { + $this->consumerVersionTags = []; + foreach ($consumerVersionTags as $consumerVersionTag) { + $this->addConsumerVersionTag($consumerVersionTag); + } + + return $this; + } + + public function addConsumerVersionTag(string $consumerVersionTag): self + { + $this->consumerVersionTags[] = $consumerVersionTag; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php new file mode 100644 index 00000000..45419b23 --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerInterface.php @@ -0,0 +1,52 @@ + + */ + public function getProviderTags(): array; + + /** + * @param array $providerTags + */ + public function setProviderTags(array $providerTags): self; + + public function addProviderTag(string $providerTag): self; + + public function getProviderBranch(): ?string; + + public function setProviderBranch(?string $providerBranch): self; + + public function getConsumerVersionSelectors(): ConsumerVersionSelectors; + + public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self; + + /** + * @return array + */ + public function getConsumerVersionTags(): array; + + /** + * @param array $consumerVersionTags + */ + public function setConsumerVersionTags(array $consumerVersionTags): self; + + public function addConsumerVersionTag(string $consumerVersionTag): self; +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php new file mode 100644 index 00000000..eb3dd1fb --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/Url.php @@ -0,0 +1,61 @@ +url; + } + + public function setUrl(UriInterface $url): self + { + $this->url = $url; + + return $this; + } + + public function getToken(): ?string + { + return $this->token; + } + + public function setToken(?string $token): self + { + $this->token = $token; + + return $this; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } +} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php new file mode 100644 index 00000000..cfc8effa --- /dev/null +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlInterface.php @@ -0,0 +1,24 @@ + - */ - private array $providerVersionTag = []; - - private bool $publishResults = false; - - private ?UriInterface $brokerUri = null; - - private ?string $brokerToken = null; - - private ?string $brokerUsername = null; - - private ?string $brokerPassword = null; + private CallingAppInterface $callingApp; + private ProviderInfoInterface $providerInfo; /** - * @var array + * @var array */ - private array $customProviderHeaders = []; - - private bool $verbose = false; - - private ?string $logDirectory = null; - - private ?string $format = null; - - private int $processTimeout = 60; - - private int $processIdleTimeout = 10; + private array $providerTransports = []; - private bool $enablePending = false; - - private ?string $wipPactSince = null; - - /** - * @var array - */ - private array $consumerVersionTag = []; - - private ConsumerVersionSelectors $consumerVersionSelectors; - - /** @var null|callable */ - private $requestFilter = null; + private FilterInfoInterface $filterInfo; + private ProviderStateInterface $providerState; + private VerificationOptionsInterface $verificationOptions; + private ?PublishOptionsInterface $publishOptions = null; + private ConsumerFiltersInterface $consumerFilters; public function __construct() { - $this->consumerVersionSelectors = new ConsumerVersionSelectors(); - } - - /** - * {@inheritdoc} - */ - public function getProviderBaseUrl(): ?UriInterface - { - return $this->providerBaseUrl; - } - - /** - * {@inheritdoc} - */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): VerifierConfigInterface - { - $this->providerBaseUrl = $providerBaseUrl; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderStatesSetupUrl(): ?string - { - return $this->providerStatesSetupUrl; - } - - /** - * {@inheritdoc} - */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): VerifierConfigInterface - { - $this->providerStatesSetupUrl = $providerStatesSetupUrl; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderName(): ?string - { - return $this->providerName; - } - - /** - * {@inheritdoc} - */ - public function setProviderName(string $providerName): VerifierConfigInterface - { - $this->providerName = $providerName; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderVersion(): ?string - { - return $this->providerVersion; - } - - /** - * {@inheritdoc} - */ - public function setProviderVersion(string $providerVersion): VerifierConfigInterface - { - $this->providerVersion = $providerVersion; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderVersionTag(): array - { - return $this->providerVersionTag; - } - - /** - * {@inheritdoc} - */ - public function setProviderVersionTag(string $providerVersionTag): VerifierConfigInterface - { - return $this->addProviderVersionTag($providerVersionTag); - } - - /** - * {@inheritdoc} - */ - public function getConsumerVersionTag(): array - { - return $this->consumerVersionTag; - } - - /** - * {@inheritdoc} - */ - public function addConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface - { - $this->consumerVersionTag[] = $consumerVersionTag; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function addProviderVersionTag(string $providerVersionTag): VerifierConfigInterface - { - $this->providerVersionTag[] = $providerVersionTag; - - return $this; - } - - public function setConsumerVersionTag(string $consumerVersionTag): VerifierConfigInterface - { - return $this->addConsumerVersionTag($consumerVersionTag); - } - - public function getConsumerVersionSelectors(): ConsumerVersionSelectors - { - return $this->consumerVersionSelectors; - } - - public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): VerifierConfigInterface - { - $this->consumerVersionSelectors = $selectors; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function isPublishResults(): bool - { - return $this->publishResults; + $this->callingApp = new CallingApp(); + $this->providerInfo = new ProviderInfo(); + $this->filterInfo = new FilterInfo(); + $this->providerState = new ProviderState(); + $this->verificationOptions = new VerificationOptions(); + $this->consumerFilters = new ConsumerFilters(); } - /** - * {@inheritdoc} - */ - public function setPublishResults(bool $publishResults): VerifierConfigInterface + public function setCallingApp(CallingAppInterface $callingApp): self { - $this->publishResults = $publishResults; + $this->callingApp = $callingApp; return $this; } - /** - * {@inheritdoc} - */ - public function getBrokerUri(): ?UriInterface + public function getCallingApp(): CallingAppInterface { - return $this->brokerUri; + return $this->callingApp; } - /** - * {@inheritdoc} - */ - public function setBrokerUri(UriInterface $brokerUri): VerifierConfigInterface + public function setProviderInfo(ProviderInfoInterface $providerInfo): self { - $this->brokerUri = $brokerUri; + $this->providerInfo = $providerInfo; return $this; } - /** - * {@inheritdoc}} - */ - public function getBrokerToken(): ?string + public function getProviderInfo(): ProviderInfoInterface { - return $this->brokerToken; - } - - /** - * {@inheritdoc } - */ - public function setBrokerToken(?string $brokerToken): VerifierConfigInterface - { - $this->brokerToken = $brokerToken; - - return $this; + return $this->providerInfo; } /** * {@inheritdoc} */ - public function getBrokerUsername(): ?string + public function setProviderTransports(array $providerTransports): self { - return $this->brokerUsername; - } - - /** - * {@inheritdoc} - */ - public function setBrokerUsername(string $brokerUsername): VerifierConfigInterface - { - $this->brokerUsername = $brokerUsername; + $this->providerTransports = []; + foreach ($providerTransports as $providerTransport) { + $this->addProviderTransport($providerTransport); + } return $this; } - /** - * {@inheritdoc} - */ - public function getBrokerPassword(): ?string + public function addProviderTransport(ProviderTransportInterface $providerTransport): self { - return $this->brokerPassword; - } - - /** - * {@inheritdoc} - */ - public function setBrokerPassword(string $brokerPassword): self - { - $this->brokerPassword = $brokerPassword; + $this->providerTransports[] = $providerTransport; return $this; } @@ -294,169 +95,73 @@ public function setBrokerPassword(string $brokerPassword): self /** * {@inheritdoc} */ - public function getCustomProviderHeaders(): array - { - return $this->customProviderHeaders; - } - - /** - * {@inheritdoc} - */ - public function setCustomProviderHeaders(array $customProviderHeaders): VerifierConfigInterface + public function getProviderTransports(): array { - $this->customProviderHeaders = $customProviderHeaders; - - return $this; + return $this->providerTransports; } - public function addCustomProviderHeader(string $name, string $value): VerifierConfigInterface + public function setFilterInfo(FilterInfoInterface $filterInfo): self { - $this->customProviderHeaders[] = "$name: $value"; + $this->filterInfo = $filterInfo; return $this; } - /** - * {@inheritdoc} - */ - public function isVerbose(): bool + public function getFilterInfo(): FilterInfoInterface { - return $this->verbose; + return $this->filterInfo; } - /** - * {@inheritdoc} - */ - public function setVerbose(bool $verbose): VerifierConfigInterface + public function setProviderState(ProviderStateInterface $providerState): self { - $this->verbose = $verbose; + $this->providerState = $providerState; return $this; } - /** - * {@inheritdoc} - */ - public function getLogDirectory(): ?string + public function getProviderState(): ProviderStateInterface { - return $this->logDirectory; + return $this->providerState; } - /** - * {@inheritdoc} - */ - public function setLogDirectory(string $log): VerifierConfigInterface - { - $this->logDirectory = $log; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getFormat(): ?string - { - return $this->format; - } - - /** - * {@inheritdoc} - */ - public function setFormat(string $format): VerifierConfigInterface + public function setPublishOptions(?PublishOptionsInterface $publishOptions): self { - $this->format = $format; + $this->publishOptions = $publishOptions; return $this; } - public function setProcessTimeout(int $timeout): VerifierConfigInterface - { - $this->processTimeout = $timeout; - - return $this; - } - - public function setProcessIdleTimeout(int $timeout): VerifierConfigInterface - { - $this->processIdleTimeout = $timeout; - - return $this; - } - - public function getProcessTimeout(): int - { - return $this->processTimeout; - } - - public function getProcessIdleTimeout(): int - { - return $this->processIdleTimeout; - } - - /** - * {@inheritdoc} - */ - public function isEnablePending(): bool + public function getPublishOptions(): ?PublishOptionsInterface { - return $this->enablePending; + return $this->publishOptions; } - /** - * {@inheritdoc} - */ - public function setEnablePending(bool $pending): VerifierConfigInterface + public function isPublishResults(): bool { - $this->enablePending = $pending; - - return $this; + return $this->publishOptions !== null; } - /** - * {@inheritdoc} - */ - public function setIncludeWipPactSince(string $date): VerifierConfigInterface + public function setConsumerFilters(ConsumerFiltersInterface $consumerFilters): self { - $this->wipPactSince = $date; + $this->consumerFilters = $consumerFilters; return $this; } - /** - * {@inheritdoc} - */ - public function getIncludeWipPactSince(): ?string - { - return $this->wipPactSince; - } - - public function getRequestFilter(): ?callable + public function getConsumerFilters(): ConsumerFiltersInterface { - return $this->requestFilter; + return $this->consumerFilters; } - public function setRequestFilter(callable $requestFilter): VerifierConfigInterface + public function setVerificationOptions(VerificationOptionsInterface $verificationOptions): self { - $this->requestFilter = $requestFilter; + $this->verificationOptions = $verificationOptions; return $this; } - /** - * {@inheritdoc} - */ - public function setProviderBranch(string $providerBranch): VerifierConfigInterface - { - $this->providerBranch = $providerBranch; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getProviderBranch(): ?string + public function getVerificationOptions(): VerificationOptionsInterface { - return $this->providerBranch; + return $this->verificationOptions; } } diff --git a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php index 9b748f9e..6f13cb83 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigInterface.php @@ -2,223 +2,64 @@ namespace PhpPact\Standalone\ProviderVerifier\Model; -use Psr\Http\Message\UriInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\CallingAppInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ConsumerFiltersInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\FilterInfoInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderInfoInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderStateInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\ProviderTransportInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\PublishOptionsInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Config\VerificationOptionsInterface; -/** - * Configuration to use with the verifier server. - */ interface VerifierConfigInterface { - /** - * @return null|UriInterface providers base url - */ - public function getProviderBaseUrl(): ?UriInterface; + public function setCallingApp(CallingAppInterface $callingApp): self; - /** - * @param UriInterface $providerBaseUrl providers base url - */ - public function setProviderBaseUrl(UriInterface $providerBaseUrl): self; + public function getCallingApp(): CallingAppInterface; - /** - * @return null|string Base URL to setup the provider states at - */ - public function getProviderStatesSetupUrl(): ?string; + public function setProviderInfo(ProviderInfoInterface $providerInfo): self; - /** - * @param string $providerStatesSetupUrl Base URL to setup the provider states at - */ - public function setProviderStatesSetupUrl(string $providerStatesSetupUrl): self; + public function getProviderInfo(): ProviderInfoInterface; /** - * @return null|string name of the provider + * @param array $providerTransports */ - public function getProviderName(): ?string; + public function setProviderTransports(array $providerTransports): self; - /** - * @param string $providerName Name of the provider - */ - public function setProviderName(string $providerName): self; + public function addProviderTransport(ProviderTransportInterface $providerTransport): self; /** - * @return null|string providers version + * @return array */ - public function getProviderVersion(): ?string; + public function getProviderTransports(): array; - /** - * @param string $providerVersion providers version - */ - public function setProviderVersion(string $providerVersion): self; + public function setFilterInfo(FilterInfoInterface $filterInfo): self; - /** - * @param string $providerBranch providers branch name - */ - public function setProviderBranch(string $providerBranch): self; - - /** - * @return array providers version tag - */ - public function getProviderVersionTag(): array; - - /** - * @return null|string providers branch name - */ - public function getProviderBranch(): ?string; - - /** - * @param string $providerVersionTag providers version tag - */ - public function setProviderVersionTag(string $providerVersionTag): self; - - /** - * @return array consumers version tag - */ - public function getConsumerVersionTag(): array; + public function getFilterInfo(): FilterInfoInterface; - /** - * @param string $consumerVersionTag consumers version tag - */ - public function addConsumerVersionTag(string $consumerVersionTag): self; + public function setProviderState(ProviderStateInterface $providerState): self; - /** - * @param string $providerVersionTag provider version tag - */ - public function addProviderVersionTag(string $providerVersionTag): self; + public function getProviderState(): ProviderStateInterface; - public function getConsumerVersionSelectors(): ConsumerVersionSelectors; + public function setPublishOptions(?PublishOptionsInterface $publishOptions): self; - /** - * @param ConsumerVersionSelectors $selectors Consumer version selectors - */ - public function setConsumerVersionSelectors(ConsumerVersionSelectors $selectors): self; + public function getPublishOptions(): ?PublishOptionsInterface; - /** - * @return bool are results going to be published - */ public function isPublishResults(): bool; - /** - * @param bool $publishResults flag to publish results - */ - public function setPublishResults(bool $publishResults): self; - - /** - * @return null|UriInterface url to the pact broker - */ - public function getBrokerUri(): ?UriInterface; - - /** - * @param UriInterface $brokerUri uri to the pact broker - */ - public function setBrokerUri(UriInterface $brokerUri): self; - - /** - * @return null|string token for the pact broker - */ - public function getBrokerToken(): ?string; - - /** - * @param null|string $brokerToken token for the pact broker - */ - public function setBrokerToken(?string $brokerToken): self; - - /** - * @return null|string username for the pact broker if secured - */ - public function getBrokerUsername(): ?string; - - /** - * @param string $brokerUsername username for the pact broker if secured - */ - public function setBrokerUsername(string $brokerUsername): self; - - /** - * @return null|string password for the pact broker if secured - */ - public function getBrokerPassword(): ?string; - - /** - * @param string $brokerPassword password for the pact broker if secured - */ - public function setBrokerPassword(string $brokerPassword): self; - - /** - * @return array custom headers for the request to the provider such as authorization - */ - public function getCustomProviderHeaders(): array; - - /** - * @param array $customProviderHeaders custom headers for the requests to the provider such as authorization - */ - public function setCustomProviderHeaders(array $customProviderHeaders): self; - - public function addCustomProviderHeader(string $name, string $value): self; - - /** - * @return bool is verbosity level increased - */ - public function isVerbose(): bool; - - /** - * @param bool $verbose increase verbosity level - */ - public function setVerbose(bool $verbose): self; - - /** - * @return null|string set the directory for the pact.log file - */ - public function getLogDirectory(): ?string; - - /** - * @param string $log set the directory for the pact.log file - */ - public function setLogDirectory(string $log): self; + public function setConsumerFilters(ConsumerFiltersInterface $consumerFilters): self; - /** - * @return null|string RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used - */ - public function getFormat(): ?string; + public function getConsumerFilters(): ConsumerFiltersInterface; - /** - * @param string $format RSpec formatter. Defaults to custom Pact formatter. json and RspecJunitFormatter may also be used - */ - public function setFormat(string $format): self; - - public function setProcessTimeout(int $timeout): self; + public function setVerificationOptions(VerificationOptionsInterface $verificationOptions): self; - public function setProcessIdleTimeout(int $timeout): self; + public function getVerificationOptions(): VerificationOptionsInterface; - public function getProcessTimeout(): int; + public function getLogLevel(): ?string; - public function getProcessIdleTimeout(): int; + public function setLogLevel(string $logLevel): self; - /** - * @param bool $pending allow pacts which are in pending state to be verified without causing the overall task to fail - */ - public function setEnablePending(bool $pending): self; + public function getPluginDir(): ?string; - /** - * @return bool is enabled pending pacts - */ - public function isEnablePending(): bool; - - /** - * @param string $date Includes pact marked as WIP since this date. - * Accepted formats: Y-m-d (2020-01-30) or c (ISO 8601 date 2004-02-12T15:19:21+00:00) - */ - public function setIncludeWipPactSince(string $date): self; - - /** - * @return null|string get start date of included WIP Pacts - */ - public function getIncludeWipPactSince(); - - /** - * @return null|callable - */ - public function getRequestFilter(): ?callable; - - /** - * @param callable $requestFilter - */ - public function setRequestFilter(callable $requestFilter): self; + public function setPluginDir(?string $pluginDir): self; } diff --git a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php b/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php deleted file mode 100644 index 257e44a6..00000000 --- a/src/PhpPact/Standalone/ProviderVerifier/ProcessRunnerFactory.php +++ /dev/null @@ -1,30 +0,0 @@ -providerVerifier = $providerVerifier ?: Scripts::getProviderVerifier(); - } - - /** - * @param array $arguments - */ - public function createRunner(array $arguments, LoggerInterface $logger = null): ProcessRunner - { - $processRunner = new ProcessRunner($this->providerVerifier, $arguments); - if ($logger) { - $processRunner->setLogger($logger); - } - - return $processRunner; - } -} diff --git a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php index 2bc678b3..d5b6d900 100644 --- a/src/PhpPact/Standalone/ProviderVerifier/Verifier.php +++ b/src/PhpPact/Standalone/ProviderVerifier/Verifier.php @@ -2,264 +2,223 @@ namespace PhpPact\Standalone\ProviderVerifier; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Http\GuzzleClient; +use FFI\CData; +use PhpPact\FFI\Client; +use PhpPact\FFI\ClientInterface; +use PhpPact\FFI\Model\ArrayData; +use PhpPact\Standalone\ProviderVerifier\Model\Source\BrokerInterface; +use PhpPact\Standalone\ProviderVerifier\Model\Source\UrlInterface; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfigInterface; -/** - * Wrapper for the Ruby Standalone Verifier service. - */ class Verifier { - protected int $processTimeout = 60; + protected ClientInterface $client; + protected CData $handle; - protected int $processIdleTimeout = 10; - - protected VerifierConfigInterface $config; - - protected ?BrokerHttpClientInterface $brokerHttpClient = null; - - protected ?VerifierProcess $verifierProcess = null; - - public function __construct( - VerifierConfigInterface $config, - VerifierProcess $verifierProcess = null, - BrokerHttpClient $brokerHttpClient = null - ) { - $this->config = $config; - $this->verifierProcess = $verifierProcess ?: new VerifierProcess(); - $this->processTimeout = $config->getProcessTimeout(); - $this->processIdleTimeout = $config->getProcessIdleTimeout(); - - if ($brokerHttpClient) { - $this->brokerHttpClient = $brokerHttpClient; - } + public function __construct(VerifierConfigInterface $config) + { + $this->client = new Client(); + $this + ->newHandle($config) + ->setProviderInfo($config) + ->setProviderTransports($config) + ->setFilterInfo($config) + ->setProviderState($config) + ->setVerificationOptions($config) + ->setPublishOptions($config) + ->setConsumerFilters($config) + ->setLogLevel($config) + ->setPluginDir($config); } - /** - * @throws \Exception - * - * @return array parameters to be passed into the process - */ - public function getArguments(): array + private function newHandle(VerifierConfigInterface $config): self { - $parameters = []; - - if ($this->config->getProviderName() !== null) { - $parameters[] = "--provider='{$this->config->getProviderName()}'"; - } - - if ($this->config->getProviderBaseUrl() !== null) { - $parameters[] = "--provider-base-url={$this->config->getProviderBaseUrl()}"; - } - - if ($this->config->getProviderVersion() !== null) { - $parameters[] = "--provider-app-version={$this->config->getProviderVersion()}"; - } - - if ($this->config->getProviderBranch() !== null) { - $parameters[] = "--provider-version-branch={$this->config->getProviderBranch()}"; - } - - if (\count($this->config->getConsumerVersionTag()) > 0) { - foreach ($this->config->getConsumerVersionTag() as $tag) { - $parameters[] = "--consumer-version-tag={$tag}"; - } - } + $this->handle = $this->client->call( + 'pactffi_verifier_new_for_application', + $config->getCallingApp()->getName(), + $config->getCallingApp()->getVersion() + ); - if (\count($this->config->getConsumerVersionSelectors()) > 0) { - foreach ($this->config->getConsumerVersionSelectors() as $selector) { - $parameters[] = "--consumer-version-selector='{$selector}'"; - } - } + return $this; + } - if (\count($this->config->getProviderVersionTag()) > 0) { - foreach ($this->config->getProviderVersionTag() as $tag) { - $parameters[] = "--provider-version-tag={$tag}"; - } - } + private function setProviderInfo(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_provider_info', + $this->handle, + $config->getProviderInfo()->getName(), + $config->getProviderInfo()->getScheme(), + $config->getProviderInfo()->getHost(), + $config->getProviderInfo()->getPort(), + $config->getProviderInfo()->getPath() + ); - if ($this->config->getProviderStatesSetupUrl() !== null) { - $parameters[] = "--provider-states-setup-url={$this->config->getProviderStatesSetupUrl()}"; - } + return $this; + } - if ($this->config->isPublishResults() === true) { - $parameters[] = '--publish-verification-results'; + private function setProviderTransports(VerifierConfigInterface $config): self + { + foreach ($config->getProviderTransports() as $transport) { + $this->client->call( + 'pactffi_verifier_add_provider_transport', + $this->handle, + $transport->getProtocol(), + $transport->getPort(), + $transport->getPath(), + $transport->getScheme() + ); } - if ($this->config->getBrokerToken() !== null) { - $parameters[] = "--broker-token={$this->config->getBrokerToken()}"; - } + return $this; + } - if ($this->config->getBrokerUsername() !== null) { - $parameters[] = "--broker-username={$this->config->getBrokerUsername()}"; - } + private function setFilterInfo(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_provider_state', + $this->handle, + $config->getProviderState()->getStateChangeUrl() ? (string) $config->getProviderState()->getStateChangeUrl() : null, + $config->getProviderState()->isStateChangeTeardown(), + $config->getProviderState()->isStateChangeAsBody() + ); - if ($this->config->getBrokerPassword() !== null) { - $parameters[] = "--broker-password={$this->config->getBrokerPassword()}"; - } + return $this; + } - if (count($this->config->getCustomProviderHeaders()) > 0) { - foreach ($this->config->getCustomProviderHeaders() as $customProviderHeader) { - $parameters[] = "--custom-provider-header=\"{$customProviderHeader}\""; - } - } + private function setProviderState(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_filter_info', + $this->handle, + $config->getFilterInfo()->getFilterDescription(), + $config->getFilterInfo()->getFilterState(), + $config->getFilterInfo()->getFilterNoState() + ); - if ($this->config->isVerbose() === true) { - $parameters[] = '--verbose=VERBOSE'; - } + return $this; + } - if ($this->config->getLogDirectory() !== null) { - $parameters[] = "--log-dir={$this->config->getLogDirectory()}"; - } + private function setVerificationOptions(VerifierConfigInterface $config): self + { + $this->client->call( + 'pactffi_verifier_set_verification_options', + $this->handle, + $config->getVerificationOptions()->isDisableSslVerification(), + $config->getVerificationOptions()->getRequestTimeout() + ); - if ($this->config->getFormat() !== null) { - $parameters[] = "--format={$this->config->getFormat()}"; - } + return $this; + } - if ($this->config->isEnablePending() === true) { - $parameters[] = '--enable-pending'; + private function setPublishOptions(VerifierConfigInterface $config): self + { + if ($config->isPublishResults()) { + $providerTags = ArrayData::createFrom($config->getPublishOptions()->getProviderTags()); + $this->client->call( + 'pactffi_verifier_set_publish_options', + $this->handle, + $config->getPublishOptions()->getProviderVersion(), + $config->getPublishOptions()->getBuildUrl(), + $providerTags?->getItems(), + $providerTags?->getSize(), + $config->getPublishOptions()->getProviderBranch() + ); } - if ($this->config->getIncludeWipPactSince() !== null) { - $parameters[] = "--include-wip-pacts-since={$this->config->getIncludeWipPactSince()}"; - } + return $this; + } - if ($this->config->getBrokerUri() !== null) { - $parameters[] = "--pact-broker-base-url={$this->config->getBrokerUri()->__toString()}"; - } + private function setConsumerFilters(VerifierConfigInterface $config): self + { + $filterConsumerNames = ArrayData::createFrom($config->getConsumerFilters()->getFilterConsumerNames()); + $this->client->call( + 'pactffi_verifier_set_consumer_filters', + $this->handle, + $filterConsumerNames?->getItems(), + $filterConsumerNames?->getSize() + ); - return $parameters; + return $this; } - /** - * Make the request to the PACT Verifier Service to run a Pact file tests from the Pact Broker. - * - * @param string $consumerName name of the consumer to be compared against - * @param null|string $tag optional tag of the consumer such as a branch name - * @param null|string $consumerVersion optional specific version of the consumer; this is overridden by tag - * @throws \Exception - */ - public function verify(string $consumerName, string $tag = null, string $consumerVersion = null): self + private function setLogLevel(VerifierConfigInterface $config): self { - $path = "/pacts/provider/{$this->config->getProviderName()}/consumer/{$consumerName}/"; - - if ($tag) { - $path .= "latest/{$tag}/"; - } elseif ($consumerVersion) { - $path .= "version/{$consumerVersion}/"; - } else { - $path .= 'latest/'; + if ($logLevel = $config->getLogLevel()) { + $this->client->call('pactffi_init_with_log_level', $logLevel); } - $uri = $this->config->getBrokerUri()->withPath($path); - - $arguments = \array_merge([$uri->__toString()], $this->getArguments()); - - $this->verifyAction($arguments); - return $this; } - /** - * Provides a way to validate local Pact JSON files. - * - * @param array $files paths to pact json files - * @throws \Exception - */ - public function verifyFiles(array $files): self + private function setPluginDir(VerifierConfigInterface $config): self { - $arguments = \array_merge($files, $this->getArguments()); - - $this->verifyAction($arguments); + if ($pluginDir = $config->getPluginDir()) { + \putenv("PACT_PLUGIN_DIR={$pluginDir}"); + } return $this; } - /** - * Verify all Pacts from the Pact Broker are valid for the Provider. - * @throws \Exception - */ - public function verifyAll(): void + public function addFile(string $file): self { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrls($this->config->getProviderName()); + $this->client->call('pactffi_verifier_add_file_source', $this->handle, $file); - $arguments = \array_merge($arguments, $this->getArguments()); - - $this->verifyAction($arguments); + return $this; } - /** - * Verify all PACTs for a given tag. - * @throws \Exception - */ - public function verifyAllForTag(string $tag): void + public function addDirectory(string $directory): self { - $arguments = $this->getBrokerHttpClient()->getAllConsumerUrlsForTag($this->config->getProviderName(), $tag); + $this->client->call('pactffi_verifier_add_directory_source', $this->handle, $directory); - $arguments = \array_merge($arguments, $this->getArguments()); - - $this->verifyAction($arguments); + return $this; } - /** - * Verify all PACTs that match the VerifierConfig - * @throws \Exception - */ - public function verifyFromConfig(): void + public function addUrl(UrlInterface $url): self { - $this->verifyAction($this->getArguments()); - } + $this->client->call( + 'pactffi_verifier_url_source', + $this->handle, + (string) $url->getUrl(), + $url->getUsername(), + $url->getPassword(), + $url->getToken() + ); - /** - * @return array - */ - public function getTimeoutValues(): array - { - return ['process_timeout' => $this->processTimeout, 'process_idle_timeout' => $this->processIdleTimeout]; + return $this; } - /** - * Trigger execution of the Pact Verifier Service. - * - * @param array $arguments - * @throws \Exception - */ - protected function verifyAction(array $arguments): void + public function addBroker(BrokerInterface $broker): self { - $this->verifierProcess->run($arguments, $this->processTimeout, $this->processIdleTimeout); + $providerTags = ArrayData::createFrom($broker->getProviderTags()); + $consumerVersionSelectors = ArrayData::createFrom(iterator_to_array($broker->getConsumerVersionSelectors())); + $consumerVersionTags = ArrayData::createFrom($broker->getConsumerVersionTags()); + $this->client->call( + 'pactffi_verifier_broker_source_with_selectors', + $this->handle, + (string) $broker->getUrl(), + $broker->getUsername(), + $broker->getPassword(), + $broker->getToken(), + $broker->isEnablePending(), + $broker->getIncludeWipPactSince(), + $providerTags?->getItems(), + $providerTags?->getSize(), + $broker->getProviderBranch(), + $consumerVersionSelectors?->getItems(), + $consumerVersionSelectors?->getSize(), + $consumerVersionTags?->getItems(), + $consumerVersionTags?->getSize() + ); + + return $this; } - protected function getBrokerHttpClient(): BrokerHttpClientInterface + public function verify(): bool { - if (!$this->brokerHttpClient) { - $user = $this->config->getBrokerUsername(); - $password = $this->config->getBrokerPassword(); - $token = $this->config->getBrokerToken(); - $reqFilter = $this->config->getRequestFilter(); - - $config = []; - if (\strlen($token) > 0) { - $config = ['headers' => ['Authorization' => 'Bearer ' . $token]]; - } elseif ($user && $password) { - $config = ['auth' => [$user, $password]]; - } - if (\is_callable($reqFilter)) { - $stack = HandlerStack::create(); - $stack->push(Middleware::mapRequest($reqFilter), 'requestFilter'); - $config['handler'] = $stack; - } - if (($sslVerify = \getenv('PACT_BROKER_SSL_VERIFY'))) { - $client['verify'] = $sslVerify !== 'no'; - } - $client = new GuzzleClient($config); - - $this->brokerHttpClient = new BrokerHttpClient($client, $this->config->getBrokerUri()); - } + $error = $this->client->call('pactffi_verifier_execute', $this->handle); + $this->client->call('pactffi_verifier_shutdown', $this->handle); - return $this->brokerHttpClient; + return !$error; } } diff --git a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php b/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php deleted file mode 100644 index 6201c53f..00000000 --- a/src/PhpPact/Standalone/ProviderVerifier/VerifierProcess.php +++ /dev/null @@ -1,57 +0,0 @@ -processRunnerFactory = $processRunnerFactory ?: new ProcessRunnerFactory(); - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - /** - * @param array $arguments - * @throws \Exception - */ - public function run(array $arguments, ?int $processTimeout = null, ?int $processIdleTimeout = null): void - { - $logger = $this->getLogger(); - $processRunner = $this->processRunnerFactory->createRunner( - $arguments, - $logger - ); - - $logger->info("Verifying PACT with script:\n{$processRunner->getCommand()}\n\n"); - - $processRunner->runBlocking(); - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('console'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/Runner/ProcessRunner.php b/src/PhpPact/Standalone/Runner/ProcessRunner.php deleted file mode 100644 index 2da9eb15..00000000 --- a/src/PhpPact/Standalone/Runner/ProcessRunner.php +++ /dev/null @@ -1,193 +0,0 @@ - $arguments - */ - public function __construct(string $command, array $arguments) - { - $this->exitCode = -1; - $this->process = new Process($command . ' ' . \implode(' ', $arguments)); - } - - public function setLogger(LoggerInterface $logger): self - { - $this->logger = $logger; - - return $this; - } - - public function getOutput(): string - { - return $this->output; - } - - public function setOutput(string $output): void - { - $this->output = $output; - } - - public function getExitCode(): int - { - return $this->exitCode; - } - - public function setExitCode(int $exitCode): void - { - $this->exitCode = $exitCode; - } - - public function getCommand(): string - { - return $this->process->getCommand(); - } - - public function getStderr(): string - { - return $this->stderr; - } - - public function setStderr(string $stderr): void - { - $this->stderr = $stderr; - } - - /** - * Run a blocking, synchronous process - */ - public function runBlocking(): int - { - $logger = $this->getLogger(); - $pid = null; - $lambdaLoop = function () use ($logger, &$pid) { - $logger->debug("Process command: {$this->process->getCommand()}"); - - $pid = yield $this->process->start(); - - $this->output .= yield ByteStream\buffer($this->process->getStdout()); - $this->stderr .= yield ByteStream\buffer($this->process->getStderr()); - - $exitCode = yield $this->process->join(); - $this->setExitCode($exitCode); - $logger->debug("Exit code: {$this->getExitCode()}"); - - if ($this->getExitCode() !== 0) { - $this->logger->info('out > ' . $this->getOutput()); - $this->logger->error('err > ' . $this->getStderr()); - throw new \Exception("PactPHP Process returned non-zero exit code: {$this->getExitCode()}", $this->getExitCode()); - } - - Loop::stop(); - }; - - Loop::run($lambdaLoop); - - return $pid; - } - - /** - * Run a blocking, synchronous process - */ - public function runNonBlocking(): int - { - $logger = $this->getLogger(); - - $pid = null; - - $lambdaLoop = function () use ($logger, &$pid) { - $logger->debug("start background command: {$this->process->getCommand()}"); - - $pid = yield $this->process->start(); - - $this->process->getStdout()->read()->onResolve(function (\Throwable $reason = null, $value) { - $this->output .= $value; - }); - $this->process->getStderr()->read()->onResolve(function (\Throwable $reason = null, $value) { - $this->output .= $value; - }); - - Loop::stop(); - }; - - Loop::run($lambdaLoop); - - $logger->debug("started process pid=$pid"); - - return $pid; - } - - /** - * Run the process and set output - * - * @return int Process Id - */ - public function run(bool $blocking = false): int - { - return $blocking - ? $this->runBlocking() - : $this->runNonBlocking(); - } - - /** - * Stop the running process - * - * @throws ProcessException - */ - public function stop(): bool - { - $pid = $this->process->getPid(); - - print "\nStopping Process Id: {$pid}\n"; - - if ('\\' === \DIRECTORY_SEPARATOR) { - \exec(\sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); - } - - $this->process->kill(); - - if ($this->process->isRunning()) { - throw new ProcessException(\sprintf('Error while killing process "%s".', $pid)); - } - - return true; - } - - private function getLogger(): LoggerInterface - { - if (null === $this->logger) { - $logHandler = new StreamHandler(new ResourceOutputStream(\STDOUT)); - $logHandler->setFormatter(new ConsoleFormatter(null, null, true)); - $this->logger = new Logger('server'); - $this->logger->pushHandler($logHandler); - } - - return $this->logger; - } -} diff --git a/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php b/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php new file mode 100644 index 00000000..05de2a78 --- /dev/null +++ b/src/PhpPact/Standalone/StubService/Exception/LogLevelNotSupportedException.php @@ -0,0 +1,9 @@ +config = $config; } - /** - * {@inheritdoc} - */ - public function healthCheck(): bool - { - $uri = $this->config->getBaseUri()->withPath('/'); - - $response = $this->client->get($uri, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Pact-Mock-Service' => true, - ], - ]); - - $body = $response->getBody()->getContents(); - - if ($response->getStatusCode() !== 200 - || $body !== "Mock service running\n") { - throw new ConnectionException('Failed to receive a successful response from the Stub Server.'); - } - - return true; - } - /** * {@inheritdoc} * @throws \JsonException */ - public function getJson(): string + public function getJson(string $endpoint): string { - $uri = $this->config->getBaseUri()->withPath('/' . $this->config->getEndpoint()); + $uri = $this->config->getBaseUri()->withPath('/' . $endpoint); $response = $this->client->get($uri, [ 'headers' => [ 'Content-Type' => 'application/json', diff --git a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php index 307a9448..325637e2 100644 --- a/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php +++ b/src/PhpPact/Standalone/StubService/Service/StubServerHttpServiceInterface.php @@ -2,19 +2,10 @@ namespace PhpPact\Standalone\StubService\Service; -use PhpPact\Exception\ConnectionException; - interface StubServerHttpServiceInterface { - /** - * Verify that the Ruby PhpPact Stub Server is running. - * - * @throws ConnectionException - */ - public function healthCheck(): bool; - /** * Get the current state of the PACT JSON file and write it to disk. */ - 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 f19b4c67..81934bdf 100644 --- a/src/PhpPact/Standalone/StubService/StubServer.php +++ b/src/PhpPact/Standalone/StubService/StubServer.php @@ -2,10 +2,10 @@ namespace PhpPact\Standalone\StubService; -use Amp\Process\ProcessException; use Exception; use PhpPact\Standalone\Installer\Model\Scripts; -use PhpPact\Standalone\Runner\ProcessRunner; +use PhpPact\Standalone\StubService\Exception\LogLevelNotSupportedException; +use Symfony\Component\Process\Process; /** * Ruby Standalone Stub Server Wrapper @@ -14,7 +14,7 @@ class StubServer { private StubServerConfigInterface $config; - private ProcessRunner $processRunner; + private Process $process; public function __construct(StubServerConfigInterface $config) { @@ -24,31 +24,47 @@ public function __construct(StubServerConfigInterface $config) /** * Start the Stub Server. Verify that it is running. * - * @param int $wait seconds to delay for the server to come up - * * @throws Exception * - * @return int process ID of the started Stub Server + * @return int|null process ID of the started Stub Server if running, null otherwise */ - public function start(int $wait = 1): int + public function start(): ?int { - $this->processRunner = new ProcessRunner(Scripts::getStubService(), $this->getArguments()); + $this->process = new Process([Scripts::getStubService(), ...$this->getArguments()], null, ['PACT_BROKER_BASE_URL' => false]); + + $this->process->start(function (string $type, string $buffer) { + echo $buffer; + }); - $processId = $this->processRunner->run(); - \sleep($wait); // wait for server to start + $logLevel = $this->config->getLogLevel(); + if (is_null($logLevel) || in_array($logLevel, ['info', 'debug', 'trace'])) { + $this->process->waitUntil(function (string $type, string $output) { + $result = preg_match('/Server started on port (\d+)/', $output, $matches); + if ($result === 1 && $this->config->getPort() === 0) { + $this->config->setPort((int)$matches[1]); + } + + return $result; + }); + } else { + if ($this->config->getPort() === 0) { + throw new LogLevelNotSupportedException(sprintf("Setting random port for stub server required log level 'info', 'debug' or 'trace'. '%s' given.", $logLevel)); + } + } - return $processId; + return $this->process->getPid(); } /** * Stop the Stub Server process. * * @return bool Was stopping successful? - * @throws ProcessException */ public function stop(): bool { - return $this->processRunner->stop(); + $this->process->stop(); + + return true; } /** @@ -60,12 +76,64 @@ private function getArguments(): array { $results = []; - $results[] = $this->config->getPactLocation(); - $results[] = "--host={$this->config->getHost()}"; - $results[] = "--port={$this->config->getPort()}"; + if ($this->config->getBrokerUrl() !== null) { + $results[] = "--broker-url={$this->config->getBrokerUrl()}"; + } + + foreach ($this->config->getDirs() as $dir) { + $results[] = "--dir={$dir}"; + } + + if ($this->config->getExtension() !== null) { + $results[] = "--extension={$this->config->getExtension()}"; + } + + foreach ($this->config->getFiles() as $file) { + $results[] = "--file={$file}"; + } + + if ($this->config->getLogLevel() !== null) { + $results[] = "--loglevel={$this->config->getLogLevel()}"; + } + + if ($this->config->getPort() !== null) { + $results[] = "--port={$this->config->getPort()}"; + } + + if ($this->config->getProviderState() !== null) { + $results[] = "--provider-state={$this->config->getProviderState()}"; + } + + if ($this->config->getProviderStateHeaderName() !== null) { + $results[] = "--provider-state-header-name={$this->config->getProviderStateHeaderName()}"; + } + + if ($this->config->getToken() !== null) { + $results[] = "--token={$this->config->getToken()}"; + } + + foreach ($this->config->getUrls() as $url) { + $results[] = "--url={$url}"; + } + + if ($this->config->getUser() !== null) { + $results[] = "--user={$this->config->getUser()}"; + } + + if ($this->config->isCors()) { + $results[] = '--cors'; + } + + if ($this->config->isCorsReferer()) { + $results[] = '--cors-referer'; + } + + if ($this->config->isEmptyProviderState()) { + $results[] = '--empty-provider-state'; + } - if ($this->config->getLog() !== null) { - $results[] = "--log={$this->config->getLog()}"; + if ($this->config->isInsecureTls()) { + $results[] = '--insecure-tls'; } return $results; diff --git a/src/PhpPact/Standalone/StubService/StubServerConfig.php b/src/PhpPact/Standalone/StubService/StubServerConfig.php index e12779a6..9258eada 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfig.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfig.php @@ -10,55 +10,107 @@ */ class StubServerConfig implements StubServerConfigInterface { + private ?UriInterface $brokerUrl = null; + private int $port = 0; + + private ?string $extension = null; + private ?string $logLevel = null; + private ?string $providerState = null; + private ?string $providerStateHeaderName = null; + private ?string $token = null; + private ?string $user = null; + /** - * Host on which to bind the service. + * @var array */ - private string $host = 'localhost'; - + private array $dirs = []; /** - * Port on which to run the service. + * @var array */ - private int $port = 7201; - - private bool $secure = false; - + private array $files = []; /** - * File to which to log output. + * @var array */ - private ?string $log = null; - - private string $pactLocation; - private string $endpoint; - + private array $urls = []; /** - * {@inheritdoc} + * @var array */ - public function getHost(): string + private array $consumerNames = []; + /** + * @var array + */ + private array $providerNames = []; + + private bool $cors = false; + private bool $corsReferer = false; + private bool $emptyProviderState = false; + private bool $insecureTls = false; + + public function getBrokerUrl(): ?UriInterface { - return $this->host; + return $this->brokerUrl; } - /** - * {@inheritdoc} - */ - public function setHost(string $host): StubServerConfigInterface + public function setBrokerUrl(UriInterface $brokerUrl): StubServerConfigInterface { - $this->host = $host; + $this->brokerUrl = $brokerUrl; return $this; } - /** - * {@inheritdoc} - */ + public function setDirs(array $dirs): StubServerConfigInterface + { + $this->dirs = array_map(fn (string $dir) => $dir, $dirs); + + return $this; + } + + public function getDirs(): array + { + return $this->dirs; + } + + public function getExtension(): ?string + { + return $this->extension; + } + + public function setExtension(string $extension): StubServerConfigInterface + { + $this->extension = $extension; + + return $this; + } + + public function setFiles(array $files): StubServerConfigInterface + { + $this->files = array_map(fn (string $file) => $file, $files); + + return $this; + } + + public function getFiles(): array + { + return $this->files; + } + + public function setLogLevel(string $logLevel): StubServerConfigInterface + { + $this->logLevel = $logLevel; + + return $this; + } + + public function getLogLevel(): ?string + { + return $this->logLevel; + } + public function getPort(): int { return $this->port; } - /** - * {@inheritdoc} - */ public function setPort(int $port): StubServerConfigInterface { $this->port = $port; @@ -66,73 +118,140 @@ public function setPort(int $port): StubServerConfigInterface return $this; } - /** - * {@inheritdoc} - */ - public function isSecure(): bool + public function getProviderState(): ?string { - return $this->secure; + return $this->providerState; } - /** - * {@inheritdoc} - */ - public function setSecure(bool $secure): StubServerConfigInterface + public function setProviderState(string $providerState): StubServerConfigInterface { - $this->secure = $secure; + $this->providerState = $providerState; return $this; } - /** - * {@inheritdoc} - */ - public function getBaseUri(): UriInterface + public function getProviderStateHeaderName(): ?string + { + return $this->providerStateHeaderName; + } + + public function setProviderStateHeaderName(string $providerStateHeaderName): StubServerConfigInterface { - $protocol = $this->secure ? 'https' : 'http'; + $this->providerStateHeaderName = $providerStateHeaderName; - return new Uri("{$protocol}://{$this->getHost()}:{$this->getPort()}"); + return $this; } - /** - * {@inheritdoc} - */ - public function getLog(): ?string + public function getToken(): ?string { - return $this->log; + return $this->token; } - /** - * {@inheritdoc} - */ - public function setLog(string $log): StubServerConfigInterface + public function setToken(?string $token): StubServerConfigInterface + { + $this->token = $token; + + return $this; + } + + public function setUrls(array $urls): StubServerConfigInterface + { + $this->urls = array_map(fn (string $url) => $url, $urls); + + return $this; + } + + public function getUrls(): array + { + return $this->urls; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function setUser(string $user): StubServerConfigInterface + { + $this->user = $user; + + return $this; + } + + public function isCors(): bool + { + return $this->cors; + } + + public function setCors(bool $cors): StubServerConfigInterface { - $this->log = $log; + $this->cors = $cors; return $this; } - public function getPactLocation(): string + public function isCorsReferer(): bool { - return $this->pactLocation; + return $this->corsReferer; } - public function setPactLocation(string $location): self + public function setCorsReferer(bool $corsReferer): StubServerConfigInterface { - $this->pactLocation = $location; + $this->corsReferer = $corsReferer; return $this; } - public function getEndpoint(): string + public function isEmptyProviderState(): bool { - return $this->endpoint; + return $this->emptyProviderState; } - public function setEndpoint(string $endpoint): self + public function setEmptyProviderState(bool $emptyProviderState): StubServerConfigInterface { - $this->endpoint = $endpoint; + $this->emptyProviderState = $emptyProviderState; return $this; } + + public function isInsecureTls(): bool + { + return $this->insecureTls; + } + + public function setInsecureTls(bool $insecureTls): StubServerConfigInterface + { + $this->insecureTls = $insecureTls; + + return $this; + } + + public function setConsumerNames(array $consumerNames): StubServerConfigInterface + { + $this->consumerNames = array_map(fn (string $consumerName) => $consumerName, $consumerNames); + + return $this; + } + + public function getConsumerNames(): array + { + return $this->consumerNames; + } + + public function setProviderNames(array $providerNames): StubServerConfigInterface + { + $this->providerNames = array_map(fn (string $providerName) => $providerName, $providerNames); + + return $this; + } + + public function getProviderNames(): array + { + return $this->providerNames; + } + + public function getBaseUri(): UriInterface + { + return new Uri("http://localhost:{$this->getPort()}"); + } } diff --git a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php index 7f232923..a1f8c641 100644 --- a/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php +++ b/src/PhpPact/Standalone/StubService/StubServerConfigInterface.php @@ -10,14 +10,48 @@ interface StubServerConfigInterface { /** - * @return string the host of the stub service + * @return null|UriInterface url to the pact broker */ - public function getHost(): string; + public function getBrokerUrl(): ?UriInterface; /** - * @param string $host The host of the stub service + * @param UriInterface $brokerUrl URL of the pact broker to fetch pacts from */ - public function setHost(string $host): self; + public function setBrokerUrl(UriInterface $brokerUrl): self; + + /** + * @param array $dirs Directory of pact files to load + */ + public function setDirs(array $dirs): self; + + /** + * @return array + */ + public function getDirs(): array; + + public function getExtension(): ?string; + + /** + * @param string $extension File extension to use when loading from a directory (default is json) + */ + public function setExtension(string $extension): self; + + /** + * @param array $files Pact file to load + */ + public function setFiles(array $files): self; + + /** + * @return array + */ + public function getFiles(): array; + + public function getLogLevel(): ?string; + + /** + * @param string $logLevel Log level (defaults to info) [possible values: error, warn, info, debug, trace, none] + */ + public function setLogLevel(string $logLevel): self; /** * @return int the port of the stub service @@ -25,40 +59,95 @@ public function setHost(string $host): self; public function getPort(): int; /** - * @param int $port the port of the stub service + * @param int $port Port to run on (defaults to random port assigned by the OS) */ public function setPort(int $port): self; /** - * @return bool true if https + * @return null|string state of the provider */ - public function isSecure(): bool; + public function getProviderState(): ?string; /** - * @param bool $secure set to true for https + * @param string $providerState Provider state regular expression to filter the responses by */ - public function setSecure(bool $secure): self; + public function setProviderState(string $providerState): self; /** - * @return UriInterface + * @return null|string name of the header */ - public function getBaseUri(): UriInterface; + public function getProviderStateHeaderName(): ?string; + + /** + * @param string $providerStateHeaderName Name of the header parameter containing the provider state to be used in case multiple matching interactions are found + */ + public function setProviderStateHeaderName(string $providerStateHeaderName): self; + + /** + * @return null|string token for the pact broker + */ + public function getToken(): ?string; /** - * @return ?string directory for log output + * @param null|string $token Bearer token to use when fetching pacts from URLS or Pact Broker */ - public function getLog(): ?string; + public function setToken(?string $token): self; /** - * @param string $log directory for log output + * @param array $urls URL of pact file to fetch */ - public function setLog(string $log): self; + public function setUrls(array $urls): self; - public function getPactLocation(): string; + /** + * @return array + */ + public function getUrls(): array; + + /** + * @return null|string user and password + */ + public function getUser(): ?string; + + /** + * @param string $user User and password to use when fetching pacts from URLS or Pact Broker in user:password form + */ + public function setUser(string $user): self; + + public function isCors(): bool; + + public function setCors(bool $cors): self; + + public function isCorsReferer(): bool; + + public function setCorsReferer(bool $corsReferer): self; + + public function isEmptyProviderState(): bool; + + public function setEmptyProviderState(bool $emptyProviderState): self; - public function setPactLocation(string $location): self; + public function isInsecureTls(): bool; - public function getEndpoint(): string; + public function setInsecureTls(bool $insecureTls): self; - public function setEndpoint(string $endpoint): self; + /** + * @param array $consumerNames Consumer name to use to filter the Pacts fetched from the Pact broker + */ + public function setConsumerNames(array $consumerNames): self; + + /** + * @return array + */ + public function getConsumerNames(): array; + + /** + * @param array $providerNames Provider name to use to filter the Pacts fetched from the Pact broker + */ + public function setProviderNames(array $providerNames): self; + + /** + * @return array + */ + public function getProviderNames(): array; + + public function getBaseUri(): UriInterface; } diff --git a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php b/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php deleted file mode 100644 index 7c510e79..00000000 --- a/tests/PhpPact/Broker/Service/BrokerHttpClientTest.php +++ /dev/null @@ -1,53 +0,0 @@ - [ - 'pacts' => [ - ['href' => 'pact-url-1'], - ['href' => 'pact-url-2'], - ], - ], - ] - ); - - $streamMock = $this->createMock(StreamInterface::class); - $streamMock->expects($this->once()) - ->method('getContents') - ->will($this->returnValue($expectedContents)); - - $responseMock = $this->createMock(ResponseInterface::class); - $responseMock->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($streamMock)); - - $httpClientMock = $this->createMock(ClientInterface::class); - $httpClientMock->expects($this->once()) - ->method('get') - ->will($this->returnValue($responseMock)); - - $uriMock = $this->createMock(UriInterface::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($this->equalTo($expectedPath)) - ->will($this->returnValue($uriMock)); - - $broker = new BrokerHttpClient($httpClientMock, $uriMock); - $broker->getAllConsumerUrls($provider); - } -} diff --git a/tests/PhpPact/Config/PactConfigTest.php b/tests/PhpPact/Config/PactConfigTest.php index 9b43621c..cb01ee86 100644 --- a/tests/PhpPact/Config/PactConfigTest.php +++ b/tests/PhpPact/Config/PactConfigTest.php @@ -20,7 +20,7 @@ public function testSetters(): void $provider = 'test-provider'; $consumer = 'test-consumer'; $pactDir = 'test-pact-dir/'; - $pactSpecificationVersion = '2.0.0'; + $pactSpecificationVersion = '3.0.0'; $log = 'test-log-dir/'; $logLevel = 'ERROR'; $pactFileWriteMode = 'merge'; diff --git a/tests/PhpPact/Consumer/InteractionBuilderTest.php b/tests/PhpPact/Consumer/InteractionBuilderTest.php index b9bf75b3..6d97ccbf 100644 --- a/tests/PhpPact/Consumer/InteractionBuilderTest.php +++ b/tests/PhpPact/Consumer/InteractionBuilderTest.php @@ -6,37 +6,12 @@ use PhpPact\Consumer\Matcher\Matcher; use PhpPact\Consumer\Model\ConsumerRequest; use PhpPact\Consumer\Model\ProviderResponse; -use PhpPact\Http\GuzzleClient; use PhpPact\Standalone\Exception\MissingEnvVariableException; -use PhpPact\Standalone\MockService\MockServer; use PhpPact\Standalone\MockService\MockServerEnvConfig; -use PhpPact\Standalone\MockService\Service\MockServerHttpService; -use PhpPact\Standalone\MockService\Service\MockServerHttpServiceInterface; use PHPUnit\Framework\TestCase; class InteractionBuilderTest extends TestCase { - private MockServerHttpServiceInterface $service; - - private MockServer $mockServer; - - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - protected function setUp(): void - { - $config = new MockServerEnvConfig(); - $this->mockServer = new MockServer($config); - $this->mockServer->start(); - $this->service = new MockServerHttpService(new GuzzleClient(), $config); - } - - protected function tearDown(): void - { - $this->mockServer->stop(); - } - /** * @throws MissingEnvVariableException * @throws \Exception @@ -61,13 +36,15 @@ public function testSimpleGet() ->addHeader('Content-Type', 'application/json'); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') + $builder + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } /** @@ -99,13 +76,15 @@ public function testPostWithBody() ]); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') + $builder + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } /** @@ -133,12 +112,14 @@ public function testBuildWithEachLikeMatcher() ]); $builder = new InteractionBuilder(new MockServerEnvConfig()); - $result = $builder - ->given('A test request.') + $builder + ->given('A test request.', ['key' => 'value']) ->uponReceiving('A test response.') ->with($request) ->willRespondWith($response); - $this->assertTrue($result); + $verifyResult = $builder->verify(); + + $this->assertFalse($verifyResult); } } diff --git a/tests/PhpPact/Consumer/Matcher/MatcherTest.php b/tests/PhpPact/Consumer/Matcher/MatcherTest.php index 8b72a7ac..39b0e168 100644 --- a/tests/PhpPact/Consumer/Matcher/MatcherTest.php +++ b/tests/PhpPact/Consumer/Matcher/MatcherTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Matcher; use Exception; +use PhpPact\Consumer\Matcher\HttpStatus; use PhpPact\Consumer\Matcher\Matcher; use PHPUnit\Framework\TestCase; @@ -31,31 +32,101 @@ public function testLike() { $json = \json_encode($this->matcher->like(12)); - $this->assertEquals('{"contents":12,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":12,"pact:matcher:type":"type"}', $json); } /** - * @throws Exception + * @dataProvider dataProviderForEachLikeTest */ - public function testEachLikeStdClass() + public function testEachLike(object|array $value) + { + $expected = \json_encode([ + 'value' => [ + [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ], + ], + 'pact:matcher:type' => 'type', + 'min' => 1, + ]); + + $actual = \json_encode($this->matcher->eachLike($value)); + + $this->assertEquals($expected, $actual); + } + + public function dataProviderForEachLikeTest() { + $value1Matcher = [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ]; + $object = new \stdClass(); - $object->value1 = $this->matcher->like(1); + $object->value1 = $value1Matcher; $object->value2 = 2; + $array = [ + 'value1' => $value1Matcher, + 'value2' => 2, + ]; + + return [ + [$object], + [$array], + ]; + } + + /** + * @dataProvider dataProviderForEachLikeTest + */ + public function testAtLeastLike(object|array $value) + { + $eachValueMatcher = [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, + ]; + $expected = \json_encode([ + 'value' => [ + $eachValueMatcher, + $eachValueMatcher, + ], + 'pact:matcher:type' => 'type', + 'min' => 2, + ]); + + $actual = \json_encode($this->matcher->atLeastLike($value, 2)); + + $this->assertEquals($expected, $actual); + } + + /** + * @dataProvider dataProviderForEachLikeTest + */ + public function testAtMostLike(object|array $value) + { $expected = \json_encode([ - 'contents' => [ - 'value1' => [ - 'contents' => 1, - 'json_class' => 'Pact::SomethingLike', + 'value' => [ + [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], + 'value2' => 2, ], - 'value2' => 2, ], - 'json_class' => 'Pact::ArrayLike', - 'min' => 1, + 'pact:matcher:type' => 'type', + 'max' => 2, ]); - $actual = \json_encode($this->matcher->eachLike($object, 1)); + $actual = \json_encode($this->matcher->atMostLike($value, 2)); $this->assertEquals($expected, $actual); } @@ -63,26 +134,36 @@ public function testEachLikeStdClass() /** * @throws Exception */ - public function testEachLikeArray() + public function testAtLeastAndMostLikeInvalidMin() { - $object = [ - 'value1' => $this->matcher->like(1), + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid minimum number of elements'); + $this->matcher->atLeastAndMostLike('text', 10, 1); + } + + /** + * @dataProvider dataProviderForEachLikeTest + */ + public function testAtLeastAndMostLike(object|array $value) + { + $eachValueMatcher = [ + 'value1' => [ + 'value' => 1, + 'pact:matcher:type' => 'type', + ], 'value2' => 2, ]; - $expected = \json_encode([ - 'contents' => [ - 'value1' => [ - 'contents' => 1, - 'json_class' => 'Pact::SomethingLike', - ], - 'value2' => 2, + 'value' => [ + $eachValueMatcher, + $eachValueMatcher, ], - 'json_class' => 'Pact::ArrayLike', - 'min' => 1, + 'pact:matcher:type' => 'type', + 'min' => 2, + 'max' => 4, ]); - $actual = \json_encode($this->matcher->eachLike($object, 1)); + $actual = \json_encode($this->matcher->atLeastAndMostLike($value, 2, 4)); $this->assertEquals($expected, $actual); } @@ -93,6 +174,7 @@ public function testEachLikeArray() public function testRegexNoMatch() { $this->expectException(Exception::class); + $this->expectExceptionMessage('The pattern BadPattern is not valid for value SomeWord. Failed with error code 0.'); $this->matcher->regex('SomeWord', 'BadPattern'); } @@ -102,15 +184,9 @@ public function testRegexNoMatch() public function testRegex() { $expected = [ - 'data' => [ - 'generate' => 'Games', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => 'Games|Other', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'Games', + 'regex' => 'Games|Other', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->regex('Games', 'Games|Other'); @@ -121,18 +197,12 @@ public function testRegex() /** * @throws Exception */ - public function testDate() + public function testDateISO8601() { $expected = [ - 'data' => [ - 'generate' => '2010-01-17', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '2010-01-17', + 'regex' => '^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))?)$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateISO8601('2010-01-17'); @@ -145,18 +215,12 @@ public function testDate() * * @throws Exception */ - public function testTime($time) + public function testTimeISO8601($time) { $expected = [ - 'data' => [ - 'generate' => $time, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $time, + 'regex' => '^(T\\d\\d:\\d\\d(:\\d\\d)?(\\.\\d+)?([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?)$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->timeISO8601($time); @@ -185,18 +249,12 @@ public function dataProviderForTimeTest() * * @throws Exception */ - public function testDateTime($dateTime) + public function testDateTimeISO8601($dateTime) { $expected = [ - 'data' => [ - 'generate' => $dateTime, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $dateTime, + 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateTimeISO8601($dateTime); @@ -223,18 +281,12 @@ public function dataProviderForDateTimeTest() * * @throws Exception */ - public function testDateTimeWithMillis($dateTime) + public function testDateTimeWithMillisISO8601($dateTime) { $expected = [ - 'data' => [ - 'generate' => $dateTime, - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => $dateTime, + 'regex' => '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d{3}([+-][0-2]\\d(?:|:?[0-5]\\d)|Z)?$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->dateTimeWithMillisISO8601($dateTime); @@ -262,15 +314,9 @@ public function dataProviderForDateTimeWithMillisTest() public function testTimestampRFC3339() { $expected = [ - 'data' => [ - 'generate' => 'Mon, 31 Oct 2016 15:21:41 -0400', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'Mon, 31 Oct 2016 15:21:41 -0400', + 'regex' => '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s\\d{2}\\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\s(\\+|-)\\d{4}$', + 'pact:matcher:type' => 'regex', ]; $actual = $this->matcher->timestampRFC3339('Mon, 31 Oct 2016 15:21:41 -0400'); @@ -285,7 +331,7 @@ public function testInteger() { $json = \json_encode($this->matcher->integer()); - $this->assertEquals('{"contents":13,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":13,"pact:matcher:type":"type"}', $json); } /** @@ -295,7 +341,7 @@ public function testBoolean() { $json = \json_encode($this->matcher->boolean()); - $this->assertEquals('{"contents":true,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":true,"pact:matcher:type":"type"}', $json); } /** @@ -305,7 +351,73 @@ public function testDecimal() { $json = \json_encode($this->matcher->decimal()); - $this->assertEquals('{"contents":13.01,"json_class":"Pact::SomethingLike"}', $json); + $this->assertEquals('{"value":13.01,"pact:matcher:type":"type"}', $json); + } + + public function testIntegerV3() + { + $expected = [ + 'value' => 13, + 'pact:matcher:type' => 'integer', + ]; + $actual = $this->matcher->integerV3(13); + + $this->assertEquals($expected, $actual); + } + + public function testRandomIntegerV3() + { + $expected = [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'integer', + ]; + $actual = $this->matcher->integerV3(); + + $this->assertEquals($expected, $actual); + } + + public function testBooleanV3() + { + $expected = [ + 'value' => true, + 'pact:matcher:type' => 'boolean', + ]; + $actual = $this->matcher->booleanV3(true); + + $this->assertEquals($expected, $actual); + } + + public function testRandomBooleanV3() + { + $expected = [ + 'pact:generator:type' => 'RandomBoolean', + 'pact:matcher:type' => 'boolean', + ]; + $actual = $this->matcher->booleanV3(); + + $this->assertEquals($expected, $actual); + } + + public function testDecimalV3() + { + $expected = [ + 'value' => 13.01, + 'pact:matcher:type' => 'decimal', + ]; + $actual = $this->matcher->decimalV3(13.01); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDecimalV3() + { + $expected = [ + 'pact:generator:type' => 'RandomDecimal', + 'pact:matcher:type' => 'decimal', + ]; + $actual = $this->matcher->decimalV3(); + + $this->assertEquals($expected, $actual); } /** @@ -314,18 +426,28 @@ public function testDecimal() public function testHexadecimal() { $expected = [ - 'data' => [ - 'generate' => '3F', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^[0-9a-fA-F]+$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '3F', + 'regex' => '^[0-9a-fA-F]+$', + 'pact:matcher:type' => 'regex', ]; + $actual = $this->matcher->hexadecimal('3F'); - $this->assertEquals($expected, $this->matcher->hexadecimal()); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomHexadecimal() + { + $expected = [ + 'regex' => '^[0-9a-fA-F]+$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'RandomHexadecimal', + ]; + $actual = $this->matcher->hexadecimal(); + + $this->assertEquals($expected, $actual); } /** @@ -334,18 +456,28 @@ public function testHexadecimal() public function testUuid() { $expected = [ - 'data' => [ - 'generate' => 'ce118b6e-d8e1-11e7-9296-cec278b6b50a', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => 'ce118b6e-d8e1-11e7-9296-cec278b6b50a', + 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', + 'pact:matcher:type' => 'regex', + ]; + $actual = $this->matcher->uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a'); + + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomUuid() + { + $expected = [ + 'pact:generator:type' => 'Uuid', + 'regex' => '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$', + 'pact:matcher:type' => 'regex', ]; + $actual = $this->matcher->uuid(); - $this->assertEquals($expected, $this->matcher->uuid()); + $this->assertEquals($expected, $actual); } /** @@ -354,15 +486,9 @@ public function testUuid() public function testIpv4Address() { $expected = [ - 'data' => [ - 'generate' => '127.0.0.13', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(\\d{1,3}\\.)+\\d{1,3}$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '127.0.0.13', + 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->ipv4Address()); @@ -374,38 +500,343 @@ public function testIpv4Address() public function testIpv6Address() { $expected = [ - 'data' => [ - 'generate' => '::ffff:192.0.2.128', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', - ], - ], - 'json_class' => 'Pact::Term', + 'value' => '::ffff:192.0.2.128', + 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', + 'pact:matcher:type' => 'regex', ]; $this->assertEquals($expected, $this->matcher->ipv6Address()); } - /** * @throws Exception */ public function testEmail() { + $expected = [ + 'value' => 'hello@pact.io', + 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', + 'pact:matcher:type' => 'regex', + ]; + $this->assertEquals($expected, $this->matcher->email()); + } + + /** + * @throws Exception + */ + public function testIpv4AddressV3() + { + $expected = $this->matcher->ipv4Address(); + $actual = $this->matcher->ipv4AddressV3('127.0.0.13'); + $this->assertEquals($expected, $actual); + } + /** + * @throws Exception + */ + public function testIpv6AddressV3() + { + $expected = $this->matcher->ipv6Address(); + $actual = $this->matcher->ipv6AddressV3('::ffff:192.0.2.128'); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testEmailV3() + { + $expected = $this->matcher->email(); + $actual = $this->matcher->emailV3('hello@pact.io'); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomIpv4AddressV3() + { $expected = [ - 'data' => [ - 'generate' => 'hello@pact.io', - 'matcher' => [ - 'json_class' => 'Regexp', - 'o' => 0, - 's' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', - ], + 'regex' => '^(\\d{1,3}\\.)+\\d{1,3}$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->ipv4AddressV3(); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomIpv6AddressV3() + { + $expected = [ + 'regex' => '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->ipv6AddressV3(); + $this->assertEquals($expected, $actual); + } + + /** + * @throws Exception + */ + public function testRandomEmailV3() + { + $expected = [ + 'regex' => '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', + 'pact:matcher:type' => 'regex', + 'pact:generator:type' => 'Regex', + ]; + $actual = $this->matcher->emailV3(); + $this->assertEquals($expected, $actual); + } + + public function testNullValue() + { + $expected = [ + 'pact:matcher:type' => 'null', + ]; + $actual = $this->matcher->nullValue(); + $this->assertEquals($expected, $actual); + } + + public function testDate() + { + $expected = [ + 'value' => '2022-11-21', + 'pact:matcher:type' => 'date', + 'format' => 'yyyy-MM-dd', + ]; + $actual = $this->matcher->date('yyyy-MM-dd', '2022-11-21'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDate() + { + $expected = [ + 'pact:generator:type' => 'Date', + 'pact:matcher:type' => 'date', + 'format' => 'yyyy-MM-dd', + ]; + $actual = $this->matcher->date(); + + $this->assertEquals($expected, $actual); + } + + public function testTime() + { + $expected = [ + 'value' => '21:45::31', + 'pact:matcher:type' => 'time', + 'format' => 'HH:mm::ss', + ]; + $actual = $this->matcher->time('HH:mm::ss', '21:45::31'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomTime() + { + $expected = [ + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'format' => 'HH:mm::ss', + ]; + $actual = $this->matcher->time(); + + $this->assertEquals($expected, $actual); + } + + public function testDateTime() + { + $expected = [ + 'value' => '2015-08-06T16:53:10', + 'pact:matcher:type' => 'datetime', + 'format' => "YYYY-mm-DD'T'HH:mm:ss", + ]; + $actual = $this->matcher->datetime("YYYY-mm-DD'T'HH:mm:ss", '2015-08-06T16:53:10'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomDateTime() + { + $expected = [ + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'format' => "YYYY-mm-DD'T'HH:mm:ss", + ]; + $actual = $this->matcher->datetime(); + + $this->assertEquals($expected, $actual); + } + + public function testString() + { + $expected = [ + 'pact:matcher:type' => 'type', + 'value' => 'test string', + ]; + $actual = $this->matcher->string('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testRandomString() + { + $expected = [ + 'pact:generator:type' => 'RandomString', + 'pact:matcher:type' => 'type', + 'value' => 'some string', + ]; + $actual = $this->matcher->string(); + + $this->assertEquals($expected, $actual); + } + + public function testFromProviderState() + { + $expected = [ + 'regex' => Matcher::UUID_V4_FORMAT, + 'pact:matcher:type' => 'regex', + 'value' => 'f2392c53-6e55-48f7-8e08-18e4bf99c795', + 'pact:generator:type' => 'ProviderState', + 'expression' => '${id}', + ]; + $actual = $this->matcher->fromProviderState($this->matcher->uuid('f2392c53-6e55-48f7-8e08-18e4bf99c795'), '${id}'); + + $this->assertEquals($expected, $actual); + } + + public function testEqual() + { + $expected = [ + 'pact:matcher:type' => 'equality', + 'value' => 'test string', + ]; + $actual = $this->matcher->equal('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testIncludes() + { + $expected = [ + 'pact:matcher:type' => 'include', + 'value' => 'test string', + ]; + $actual = $this->matcher->includes('test string'); + + $this->assertEquals($expected, $actual); + } + + public function testNumber() + { + $expected = [ + 'value' => 13.01, + 'pact:matcher:type' => 'number', + ]; + $actual = $this->matcher->number(13.01); + + $this->assertEquals($expected, $actual); + } + + public function testRandomNumber() + { + $expected = [ + 'pact:generator:type' => 'RandomInt', + 'pact:matcher:type' => 'number', + ]; + $actual = $this->matcher->number(); + + $this->assertEquals($expected, $actual); + } + + public function testArrayContaining() + { + $expected = [ + 'pact:matcher:type' => 'arrayContains', + 'variants' => [ + 'item 1', + 'item 2' ], - 'json_class' => 'Pact::Term', ]; - $this->assertEquals($expected, $this->matcher->email()); + $actual = $this->matcher->arrayContaining([ + 'item 1', + 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + + public function testNotEmpty() + { + $expected = [ + 'value' => 'not empty string', + 'pact:matcher:type' => 'notEmpty', + ]; + $actual = $this->matcher->notEmpty('not empty string'); + + $this->assertEquals($expected, $actual); + } + + public function testSemver() + { + $expected = [ + 'value' => '1.2.3', + 'pact:matcher:type' => 'semver', + ]; + $actual = $this->matcher->semver('1.2.3'); + + $this->assertEquals($expected, $actual); + } + + public function testInvalidStatusCode() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Status 'invalid' is not supported. Supported status are: info, success, redirect, clientError, serverError, nonError, error"); + $this->matcher->statusCode('invalid'); + } + + public function testValidStatusCode() + { + $expected = [ + 'status' => 'success', + 'pact:matcher:type' => 'statusCode', + ]; + $actual = $this->matcher->statusCode(HttpStatus::SUCCESS); + + $this->assertEquals($expected, $actual); + } + + public function testValues() + { + $expected = [ + 'pact:matcher:type' => 'values', + 'value' => [ + 'item 1', + 'item 2' + ], + ]; + $actual = $this->matcher->values([ + 'item 1', + 'item 2' + ]); + + $this->assertEquals($expected, $actual); + } + + public function testContentType() + { + $expected = [ + 'value' => 'image/jpeg', + 'pact:matcher:type' => 'contentType', + ]; + $actual = $this->matcher->contentType('image/jpeg'); + + $this->assertEquals($expected, $actual); } } diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index 4e760bbe..f30aeca7 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -15,16 +15,16 @@ public function testSerializing() ->setMethod('PUT') ->setPath('/somepath') ->addHeader('Content-Type', 'application/json') + ->addQueryParameter('fruit', ['apple', 'banana']) ->setBody([ '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(['fruit' => ['apple', 'banana']], $model->getQuery()); + $this->assertEquals('/somepath', $model->getPath()); + $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); } public function testSerializingWhenPathUsingMatcher() @@ -36,17 +36,15 @@ public function testSerializingWhenPathUsingMatcher() ->setMethod('PATCH') ->setPath($matcher->regex("/somepath/$pathVariable/status", '\/somepath\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\/status')) ->addHeader('Content-Type', 'application/json') + ->addQueryParameter('food', 'milk') ->setBody([ 'status' => 'finished', ]); - $data = \json_decode(\json_encode($model->jsonSerialize()), true); - - $this->assertEquals('PATCH', $data['method']); - $this->assertEquals('application/json', $data['headers']['Content-Type']); - $this->assertIsArray($data['path']); - $this->assertArrayHasKey('data', $data['path']); - $this->assertArrayHasKey('json_class', $data['path']); - $this->assertEquals('finished', $data['body']['status']); + $this->assertEquals('PATCH', $model->getMethod()); + $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); + $this->assertEquals(['food' => ['milk']], $model->getQuery()); + $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); + $this->assertEquals('{"status":"finished"}', $model->getBody()); } } diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php new file mode 100644 index 00000000..165ed8a8 --- /dev/null +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -0,0 +1,34 @@ + 'bar']; + $metadata = ['queue' => 'foo', 'routing_key' => 'bar']; + $contents = 'test'; + + $subject = (new Message()) + ->setDescription($description) + ->addProviderState($providerStateName, $providerStateParams) + ->setMetadata($metadata) + ->setContents($contents); + + static::assertSame($description, $subject->getDescription()); + $providerStates = $subject->getProviderStates(); + static::assertCount(1, $providerStates); + static::assertContainsOnlyInstancesOf(ProviderState::class, $providerStates); + static::assertEquals($providerStateName, $providerStates[0]->getName()); + static::assertEquals($providerStateParams, $providerStates[0]->getParams()); + static::assertSame($metadata, $subject->getMetadata()); + static::assertSame($contents, $subject->getContents()); + } +} diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 49e2c7f1..2da664be 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -17,10 +17,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/FFI/Model/ArrayDataTest.php b/tests/PhpPact/FFI/Model/ArrayDataTest.php new file mode 100644 index 00000000..b55a5f65 --- /dev/null +++ b/tests/PhpPact/FFI/Model/ArrayDataTest.php @@ -0,0 +1,21 @@ +assertSame(count($branches), $arrayData->getSize()); + foreach ($branches as $index => $branch) { + $this->assertSame($branch, FFI::string($arrayData->getItems()[$index])); + } + } +} diff --git a/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php b/tests/PhpPact/Standalone/Broker/BrokerConfigTest.php deleted file mode 100644 index c5b2bc5c..00000000 --- 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/Broker/BrokerTest.php b/tests/PhpPact/Standalone/Broker/BrokerTest.php deleted file mode 100644 index 13b67b38..00000000 --- a/tests/PhpPact/Standalone/Broker/BrokerTest.php +++ /dev/null @@ -1,99 +0,0 @@ -setBrokerToken('someToken') - ->setBrokerUsername('someusername') - ->setBrokerPassword('somepassword') - ))->getArguments(); - - $this->assertContains('--broker-token=someToken', $arguments); - $this->assertContains('--broker-username=someusername', $arguments); - $this->assertContains('--broker-password=somepassword', $arguments); - } - - /** - * @test - */ - public function getArgumentsEmptyConfig(): void - { - $this->assertEmpty((new Broker(new BrokerConfig()))->getArguments()); - } - - /** - * @test - */ - //public function generateUuid(): void - //{ - // $this->assertContains('-', (new Broker(new BrokerConfig()))->generateUuid()); - //} - - /** - * @test - * - * @throws \Exception - */ - public function describeVersion(): void - { - $config = new BrokerConfig(); - $config->setPacticipant('Animal Profile Service') - ->setBrokerUri(new Uri('https://test.pactflow.io')) - ->setBrokerUsername('dXfltyFMgNOFZAxr8io9wJ37iUpY42M') - ->setBrokerPassword('O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1'); - $broker = new Broker($config); - - $result = $broker->describeVersion(); - - $this->assertArrayHasKey('number', $result); - } - - /** - * @test - * - * @throws \Exception - */ - public function listLatestPactVersions(): void - { - $config = new BrokerConfig(); - $config->setPacticipant("\"Animal Profile Service\"") - ->setBrokerUri(new Uri('https://test.pactflow.io')) - ->setBrokerUsername('dXfltyFMgNOFZAxr8io9wJ37iUpY42M') - ->setBrokerPassword('O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1'); - $broker = new Broker($config); - - $result = $broker->listLatestPactVersions(); - $this->assertArrayHasKey('pacts', $result); - } - - /** - * @test - * - * @throws \Exception - */ - public function publishLogsStdError(): void - { - $config = new BrokerConfig(); - $config->setPactLocations('not a directory'); - $broker = new Broker($config); - try { - $broker->publish(); - } catch(\Exception $e) { - $this->assertEquals(1, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: 1", $e->getMessage()); - } - } -} diff --git a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php index 2bca2957..29d8fd52 100644 --- a/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php +++ b/tests/PhpPact/Standalone/MockServer/MockServerConfigTest.php @@ -15,25 +15,23 @@ public function testSetters() $consumer = 'test-consumer'; $pactDir = 'test-pact-dir/'; $pactFileWriteMode = 'merge'; + $logLevel = 'INFO'; $log = 'test-log-dir/'; - $cors = true; - $healthCheckTimeout = 11; - $healthCheckRetrySec = 22; - $pactSpecificationVersion = '2.0'; + $pactSpecificationVersion = '3.0.0'; + $secure = false; - $subject = (new MockServerConfig()) - ->setHost($host) + $subject = new MockServerConfig(); + $subject->setHost($host) ->setPort($port) ->setProvider($provider) ->setConsumer($consumer) ->setPactDir($pactDir) ->setPactFileWriteMode($pactFileWriteMode) + ->setLogLevel($logLevel) ->setLog($log) - ->setPactSpecificationVersion($pactSpecificationVersion) - ->setCors($cors) - ->setHealthCheckTimeout($healthCheckTimeout) - ->setHealthCheckRetrySec($healthCheckRetrySec); + ->setPactSpecificationVersion($pactSpecificationVersion); + static::assertSame($secure, $subject->isSecure()); static::assertSame($host, $subject->getHost()); static::assertSame($port, $subject->getPort()); static::assertSame($provider, $subject->getProvider()); @@ -41,9 +39,7 @@ public function testSetters() static::assertSame($pactDir, $subject->getPactDir()); static::assertSame($pactFileWriteMode, $subject->getPactFileWriteMode()); static::assertSame($log, $subject->getLog()); + static::assertSame($logLevel, $subject->getLogLevel()); static::assertSame($pactSpecificationVersion, $subject->getPactSpecificationVersion()); - static::assertSame($cors, $subject->hasCors()); - static::assertSame($healthCheckTimeout, $subject->getHealthCheckTimeout()); - static::assertSame($healthCheckRetrySec, $subject->getHealthCheckRetrySec()); } } diff --git a/tests/PhpPact/Standalone/MockServer/MockServerTest.php b/tests/PhpPact/Standalone/MockServer/MockServerTest.php deleted file mode 100644 index 6d356742..00000000 --- a/tests/PhpPact/Standalone/MockServer/MockServerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -start(); - $this->assertTrue(\is_int($pid)); - } finally { - $result = $mockServer->stop(); - $this->assertTrue($result); - } - } - - /** - * @throws MissingEnvVariableException - * @throws \Exception - */ - public function testStartAndStopWithRecognizedTimeout() - { - // the mock server actually takes more than one second to be ready - // we use this fact to test the timeout - $orig = \getenv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT'); - \putenv('PACT_MOCK_SERVER_HEALTH_CHECK_TIMEOUT=1'); - - $httpService = $this->getMockBuilder(MockServerHttpService::class) - ->disableOriginalConstructor() - ->getMock(); - - $connectionException = $this->getMockBuilder(ConnectionException::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 c389dad7..00000000 --- a/tests/PhpPact/Standalone/MockServer/Service/MockServerHttpServiceTest.php +++ /dev/null @@ -1,209 +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(): void - { - $result = $this->service->healthCheck(); - $this->assertTrue($result); - } - - public function testRegisterInteraction(): void - { - $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(): void - { - $result = $this->service->deleteAllInteractions(); - $this->assertTrue($result); - } - - public function testVerifyInteractions(): void - { - $result = $this->service->verifyInteractions(); - $this->assertTrue($result); - } - - public function testVerifyInteractionsFailure(): void - { - $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(): void - { - $result = $this->service->getPactJson(); - $this->assertEquals('{"consumer":{"name":"someConsumer"},"provider":{"name":"someProvider"},"interactions":[],"metadata":{"pactSpecification":{"version":"2.0.0"}}}', $result); - } - - public function testFullGetInteraction(): void - { - $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(): void - { - $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/Model/Source/BrokerTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php new file mode 100644 index 00000000..23106df1 --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/BrokerTest.php @@ -0,0 +1,37 @@ +addSelector('{"tag":"foo","latest":true}') + ->addSelector('{"tag":"bar","latest":true}'); + $consumerVersionTags = ['dev']; + + $subject = (new Broker()) + ->setEnablePending($enablePending) + ->setIncludeWipPactSince($wipPactSince) + ->setProviderTags($providerTags) + ->setProviderBranch($providerBranch) + ->setConsumerVersionSelectors($consumerVersionSelectors) + ->setConsumerVersionTags($consumerVersionTags); + + static::assertSame($enablePending, $subject->isEnablePending()); + static::assertSame($wipPactSince, $subject->getIncludeWipPactSince()); + static::assertSame($providerTags, $subject->getProviderTags()); + static::assertSame($providerBranch, $subject->getProviderBranch()); + static::assertSame($consumerVersionSelectors, $subject->getConsumerVersionSelectors()); + static::assertSame($consumerVersionTags, $subject->getConsumerVersionTags()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php new file mode 100644 index 00000000..20716b8a --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/Source/UrlTest.php @@ -0,0 +1,29 @@ +setUrl($url) + ->setToken($token) + ->setUsername($username) + ->setPassword($password); + + static::assertSame($url, $subject->getUrl()); + static::assertSame($token, $subject->getToken()); + static::assertSame($username, $subject->getUsername()); + static::assertSame($password, $subject->getPassword()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php new file mode 100644 index 00000000..48446b0a --- /dev/null +++ b/tests/PhpPact/Standalone/ProviderVerifier/Model/VerifierConfigTest.php @@ -0,0 +1,88 @@ +getProviderInfo() + ->setName($providerName) + ->setScheme($scheme) + ->setHost($host) + ->setPort($port) + ->setPath($basePath); + $subject->getFilterInfo() + ->setFilterDescription($filterDescription) + ->setFilterNoState($filterNoState) + ->setFilterState($filterState); + $subject->getProviderState() + ->setStateChangeUrl($stateChangeUrl) + ->setStateChangeAsBody($stateChangeAsBody) + ->setStateChangeTeardown($stateChangeTeardown); + $subject->getVerificationOptions() + ->setRequestTimeout($requestTimeout) + ->setDisableSslVerification($disableSslVerification); + $publishOptions = new PublishOptions(); + $publishOptions + ->setProviderTags($providerTags) + ->setProviderVersion($providerVersion) + ->setBuildUrl($buildUrl) + ->setProviderBranch($providerBranch); + $subject->setPublishOptions($publishOptions); + $subject->getConsumerFilters() + ->setFilterConsumerNames($filterConsumerNames); + + $providerInfo = $subject->getProviderInfo(); + static::assertSame($providerName, $providerInfo->getName()); + static::assertSame($scheme, $providerInfo->getScheme()); + static::assertSame($host, $providerInfo->getHost()); + static::assertSame($port, $providerInfo->getPort()); + static::assertSame($basePath, $providerInfo->getPath()); + $filterInfo = $subject->getFilterInfo(); + static::assertSame($filterDescription, $filterInfo->getFilterDescription()); + static::assertSame($filterNoState, $filterInfo->getFilterNoState()); + static::assertSame($filterState, $filterInfo->getFilterState()); + $providerState = $subject->getProviderState(); + static::assertSame($stateChangeUrl, $providerState->getStateChangeUrl()); + static::assertSame($stateChangeAsBody, $providerState->isStateChangeAsBody()); + static::assertSame($stateChangeTeardown, $providerState->isStateChangeTeardown()); + $verificationOptions = $subject->getVerificationOptions(); + static::assertSame($requestTimeout, $verificationOptions->getRequestTimeout()); + static::assertSame($disableSslVerification, $verificationOptions->isDisableSslVerification()); + static::assertSame($publishResults, $subject->isPublishResults()); + $publishOptions = $subject->getPublishOptions(); + static::assertSame($providerTags, $publishOptions->getProviderTags()); + static::assertSame($providerVersion, $publishOptions->getProviderVersion()); + static::assertSame($buildUrl, $publishOptions->getBuildUrl()); + static::assertSame($providerBranch, $publishOptions->getProviderBranch()); + $consumerFilters = $subject->getConsumerFilters(); + static::assertSame($filterConsumerNames, $consumerFilters->getFilterConsumerNames()); + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php deleted file mode 100644 index 72449939..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierProcessTest.php +++ /dev/null @@ -1,77 +0,0 @@ - 'bar']; - - $logger = $this->createMock(LoggerInterface::class); - - $processRunner = $this->createMock(ProcessRunner::class); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments), $this->equalTo($logger)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->setLogger($logger); - $process->run($arguments, 42, 23); - } - - public function testRunWithDefaultLogger() - { - $arguments = ['foo' => 'bar']; - - $processRunner = $this->createMock(ProcessRunner::class); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->run($arguments, 42, 23); - } - - public function testRunForwardsException() - { - $this->expectExceptionMessage('foo'); - $this->expectException(\RuntimeException::class); - - $arguments = ['foo' => 'bar']; - - $expectedException = new \RuntimeException('foo'); - - $processRunner = $this->createMock(ProcessRunner::class); - $processRunner->expects($this->once()) - ->method('runBlocking') - ->will( - $this->returnCallback( - function () use ($expectedException) { - throw $expectedException; - } - ) - ); - - $processRunnerFactory = $this->createMock(ProcessRunnerFactory::class); - $processRunnerFactory->expects($this->once()) - ->method('createRunner') - ->with($this->equalTo($arguments)) - ->will($this->returnValue($processRunner)); - - $process = new VerifierProcess($processRunnerFactory); - $process->run($arguments, 42, 23); - } -} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 8a5d896a..ba3fb34c 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -2,235 +2,55 @@ namespace PhpPactTest\Standalone\ProviderVerifier; -use GuzzleHttp\Psr7\Uri; -use Monolog\Handler\TestHandler; -use Monolog\Logger; -use PhpPact\Broker\Service\BrokerHttpClient; -use PhpPact\Broker\Service\BrokerHttpClientInterface; -use PhpPact\Standalone\ProviderVerifier\Model\ConsumerVersionSelectors; use PhpPact\Standalone\ProviderVerifier\Model\VerifierConfig; -use PhpPact\Standalone\ProviderVerifier\ProcessRunnerFactory; use PhpPact\Standalone\ProviderVerifier\Verifier; -use PhpPact\Standalone\ProviderVerifier\VerifierProcess; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; +use Symfony\Component\Process\Process; class VerifierTest extends TestCase { - public function testGetArguments() - { - $consumerVersionSelectors = (new ConsumerVersionSelectors()) - ->addSelector('{"tag":"foo","latest":true}') - ->addSelector('{"tag":"bar","latest":true}'); - - $config = new VerifierConfig(); - $config - ->setProviderName('some provider with whitespace') - ->setProviderVersion('1.0.0') - ->setProviderBranch('main') - ->addProviderVersionTag('prod') - ->addProviderVersionTag('dev') - ->addConsumerVersionTag('dev') - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setPublishResults(true) - ->setBrokerToken('someToken') - ->setBrokerUsername('someusername') - ->setBrokerPassword('somepassword') - ->setBrokerUri(new Uri('https://example.broker/')) - ->addCustomProviderHeader('key1', 'value1') - ->addCustomProviderHeader('key2', 'value2') - ->setVerbose(true) - ->setLogDirectory('my/log/directory') - ->setFormat('someformat') - ->setProcessTimeout(30) - ->setProcessIdleTimeout(5) - ->setEnablePending(true) - ->setIncludeWipPactSince('2020-01-30') - ->setRequestFilter( - function (RequestInterface $r) { - return $r->withHeader('MY_SPECIAL_HEADER', 'my special value'); - } - ) - ->setConsumerVersionSelectors($consumerVersionSelectors); - - /** @var BrokerHttpClientInterface $brokerHttpService */ - $server = new Verifier($config); - $arguments = $server->getArguments(); - - $this->assertContains('--provider-base-url=http://myprovider:1234', $arguments); - $this->assertContains('--provider-states-setup-url=http://someurl:1234', $arguments); - $this->assertContains('--publish-verification-results', $arguments); - $this->assertContains('--broker-token=someToken', $arguments); - $this->assertContains('--broker-username=someusername', $arguments); - $this->assertContains('--broker-password=somepassword', $arguments); - $this->assertContains('--custom-provider-header="key1: value1"', $arguments); - $this->assertContains('--custom-provider-header="key2: value2"', $arguments); - $this->assertContains('--verbose=VERBOSE', $arguments); - $this->assertContains('--log-dir=my/log/directory', $arguments); - $this->assertContains('--format=someformat', $arguments); - $this->assertContains('--provider-version-tag=prod', $arguments); - $this->assertContains('--provider-version-tag=dev', $arguments); - $this->assertContains('--provider-version-branch=main', $arguments); - $this->assertContains('--consumer-version-tag=dev', $arguments); - $this->assertSame(['process_timeout' => 30, 'process_idle_timeout' => 5], $server->getTimeoutValues()); - $this->assertContains('--enable-pending', $arguments); - $this->assertContains('--include-wip-pacts-since=2020-01-30', $arguments); - $this->assertContains('--consumer-version-selector=\'{"tag":"foo","latest":true}\'', $this->stripSpaces($arguments)); - $this->assertContains('--consumer-version-selector=\'{"tag":"bar","latest":true}\'', $this->stripSpaces($arguments)); - $this->assertContains('--provider=\'some provider with whitespace\'', $arguments); - $this->assertContains('--pact-broker-base-url=https://example.broker/', $arguments); - } + /** @var Process */ + private Process $process; /** - * Strip spaces for Windows CMD + * Run the PHP build-in web server. */ - private function stripSpaces($arr) - { - $newArr = []; - foreach ($arr as $str) { - $newArr[] = str_ireplace(' ', '', $str); - } - return $newArr; - } - - public function testGetArgumentsEmptyConfig() + protected function setUp(): void { - $this->assertEmpty((new Verifier(new VerifierConfig()))->getArguments()); - } - - /** - * @dataProvider dataProviderForBrokerPathTest - * - * @param string $consumerName - * @param string $providerName - * @param null|string $tag - * @param null|string $version - * @param string $path - */ - public function testBuildValidPathToPactBroker($consumerName, $providerName, $tag, $version, $path) - { - $expectedUrltoBroker = 'http://mock/' . $path; - - /** @var Uri $uriMock */ - $uriMock = $this->createMock(Uri::class); - $uriMock->expects($this->once()) - ->method('withPath') - ->with($path) - ->willReturn($uriMock); - - $uriMock->expects($this->any()) - ->method('__toString') - ->willReturn($expectedUrltoBroker); - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrltoBroker) { - return \in_array($expectedUrltoBroker, $args); - }) - ); - - $config = new VerifierConfig(); - $config->setProviderName($providerName) - ->setProviderBaseUrl(new Uri('http://myprovider:1234')) - ->setProviderStatesSetupUrl(new Uri('http://someurl:1234')) - ->setBrokerUri($uriMock) - ->setVerbose(true); - - $verifier = new Verifier($config, $verifierProcessMock); + $publicPath = __DIR__ . '/../../../_public/'; - $verifier->verify($consumerName, $tag, $version); - } - - public function dataProviderForBrokerPathTest() - { - $consumerName = 'someProviderName'; - $providerName = 'someProviderName'; - $tag = '1.0.0'; - $version = '11111'; + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); - return [ - [$consumerName, $providerName, null, $version, "/pacts/provider/$providerName/consumer/$consumerName/version/$version/"], - [$consumerName, $providerName, $tag, null, "/pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, $tag, $version, "/pacts/provider/$providerName/consumer/$consumerName/latest/$tag/"], - [$consumerName, $providerName, null, null, "/pacts/provider/$providerName/consumer/$consumerName/latest/"], - ]; + $this->process->start(); + $this->process->waitUntil(fn () => is_resource(@fsockopen('127.0.0.1', 7202))); } /** - * @dataProvider provideDataForVerifyAll - * - * @param string $providerName - * @param string $providerVersion - * @param bool $forceLatest - * @param mixed $expectedProviderVersion + * Stop the web server process once complete. */ - public function testIfDataForVerifyAllIsConvertedCorrectly($providerName, $providerVersion) + protected function tearDown(): void { - $expectedUrl1 = 'expectedUrl1'; - $expectedUrl2 = 'expectedUrl2'; - $expectedPactUrls = [$expectedUrl1, $expectedUrl2]; - - $verifierProcessMock = $this->createMock(VerifierProcess::class); - $verifierProcessMock->expects($this->once()) - ->method('run') - ->with( - $this->callback(function ($args) use ($expectedUrl1, $expectedUrl2) { - return \in_array($expectedUrl1, $args) && \in_array($expectedUrl2, $args); - }) - ); - - $brokerHttpClient = $this->createMock(BrokerHttpClient::class); - - $brokerHttpClient->expects($this->once()) - ->method('getAllConsumerUrls') - ->with($this->equalTo($providerName)) - ->willReturn($expectedPactUrls); - - $config = new VerifierConfig(); - $config->setProviderName($providerName); - $config->setProviderVersion($providerVersion); - - $verifier = new Verifier($config, $verifierProcessMock, $brokerHttpClient); - $verifier->verifyAll(); + $this->process->stop(); } - public function provideDataForVerifyAll() + public function testVerify(): void { - return [ - ['someProvider', '1.0.0'], - ['someProvider', '1.2.3'], - ]; - } - - public function testRunShouldLogOutputIfCmdFails() - { - if ('\\' !== \DIRECTORY_SEPARATOR) { - $cmd = __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.sh'; - } else { - $cmd = 'cmd /c' . __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.bat'; - } - - $process = new VerifierProcess(new ProcessRunnerFactory($cmd)); - - $logger = new Logger('console', [$handler = new TestHandler()]); - $process->setLogger($logger); - - try { - $exception = null; - $process->run([], 60, 10); - } catch (\Exception $e) { - $exception = $e; + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('someProvider') + ->setHost('localhost') + ->setPort(7202) + ->setScheme('http') + ->setPath('/'); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); } - $logMessages = $handler->getRecords(); + $verifier = new Verifier($config); + $verifier->addDirectory(__DIR__ . '/../../../_resources'); - $this->assertGreaterThan(2, \count($logMessages)); - $this->assertStringContainsString('first line', $logMessages[\count($logMessages) - 2]['message']); - $this->assertStringContainsString('second line', $logMessages[\count($logMessages) - 1]['message']); + $verifyResult = $verifier->verify(); - $this->assertNotNull($exception); + $this->assertTrue($verifyResult); } } diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat b/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat deleted file mode 100755 index 4cdede33..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.bat +++ /dev/null @@ -1,8 +0,0 @@ -@ECHO OFF - -REM this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -ECHO "first line" -ECHO "second line" 1>&2 - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh b/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh deleted file mode 100755 index c196300a..00000000 --- a/tests/PhpPact/Standalone/ProviderVerifier/verifier.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -echoerr() { echo "$@" 1>&2; } - -echo "first line" -echoerr "second line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php b/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php deleted file mode 100644 index ff1b45f4..00000000 --- a/tests/PhpPact/Standalone/Runner/ProcessRunnerTest.php +++ /dev/null @@ -1,74 +0,0 @@ -runBlocking(); - $exitCode = $p->getExitCode(); - - $this->assertEquals($exitCode, 0, 'Expect the exit code to be 0'); - $this->assertStringContainsString($expectedOutput, $p->getOutput(), "Expect '{$expectedOutput}' to be in the output"); - $this->assertEquals(null, $p->getStderr(), 'Expect a null stderr'); - - // try an app that does not exists - if ('\\' !== \DIRECTORY_SEPARATOR) { - $p = new ProcessRunner('failedApp', []); - $expectedErr = 'failedApp'; - } else { - $p = new ProcessRunner('cmd /c echo myError 1>&2 && exit 42', []); - $expectedErr = 'myError'; - } - - try { - $p->runBlocking(); - } catch (\Exception $e) { - $exitCode = $p->getExitCode(); - $this->assertEquals($exitCode, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: $exitCode", $e->getMessage()); - $this->assertNotEquals($exitCode, 0, 'Expect the exit code to be non-zero: ' . $exitCode); - $this->assertStringContainsString($expectedErr, $p->getStderr(), "Expect '{$expectedErr}' to be in the stderr"); - $this->assertEquals(null, $p->getOutput(), 'Expect a null stdout'); - } - } - - /** - * @throws \Exception - */ - public function testProcessRunnerShouldReturnCompleteOutput() - { - if ('\\' !== \DIRECTORY_SEPARATOR) { - $cmd = __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.sh'; - } else { - $cmd = 'cmd /c' . __DIR__ . \DIRECTORY_SEPARATOR . 'verifier.bat'; - } - - $p = new ProcessRunner($cmd, []); - $expectedOutput = 'third line'; - $expectedErr = 'fourth line'; - try { - $p->runBlocking(); - } catch (\Exception $e) { - $this->assertEquals(42, $e->getCode()); - $this->assertStringContainsString("PactPHP Process returned non-zero exit code: 42", $e->getMessage()); - } - $this->assertTrue((\stripos($p->getOutput(), $expectedOutput) !== false), "Expect '{$expectedOutput}' to be in the output:"); - $this->assertTrue((\stripos($p->getStderr(), $expectedErr) !== false), "Expect '{$expectedErr}' to be in the stderr"); - } -} diff --git a/tests/PhpPact/Standalone/Runner/verifier.bat b/tests/PhpPact/Standalone/Runner/verifier.bat deleted file mode 100755 index 3639d40f..00000000 --- a/tests/PhpPact/Standalone/Runner/verifier.bat +++ /dev/null @@ -1,11 +0,0 @@ -@ECHO OFF - -REM this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -ECHO "first line" -ECHO "second line" 1>&2 -ECHO "third line" -ECHO "fourth line" 1>&2 -ECHO "fifth line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/Runner/verifier.sh b/tests/PhpPact/Standalone/Runner/verifier.sh deleted file mode 100755 index 8290c308..00000000 --- a/tests/PhpPact/Standalone/Runner/verifier.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -# this script simulates a command (like pact-verifier) which prints several lines to stdout and stderr - -echoerr() { echo "$@" 1>&2; } - -echo "first line" -echoerr "second line" -echo "third line" -echoerr "fourth line" -echo "fifth line" - -exit 42 \ No newline at end of file diff --git a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php index 2990bd58..734e978c 100644 --- a/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php +++ b/tests/PhpPact/Standalone/StubServer/Service/StubServerHttpServiceTest.php @@ -14,13 +14,13 @@ class StubServerHttpServiceTest extends TestCase { /** @var StubServerHttpServiceInterface */ - private $service; + private StubServerHttpServiceInterface $service; /** @var StubServer */ - private $stubServer; + private StubServer $stubServer; /** @var StubServerConfigInterface */ - private $config; + private StubServerConfigInterface $config; /** * @throws MissingEnvVariableException @@ -28,19 +28,15 @@ class StubServerHttpServiceTest extends TestCase */ protected function setUp(): void { - $pactLocation = __DIR__ . '/../../../../_resources/someconsumer-someprovider.json'; - $host = 'localhost'; - $port = 7201; - $endpoint = 'test'; + $files = [__DIR__ . '/../../../../_resources/someconsumer-someprovider.json']; + $port = 7201; $this->config = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($files) + ->setPort($port); $this->stubServer = new StubServer($this->config); - $this->stubServer->start(10); + $this->stubServer->start(); $this->service = new StubServerHttpService(new GuzzleClient(), $this->config); } @@ -49,15 +45,10 @@ protected function tearDown(): void $this->stubServer->stop(); } - public function testHealthCheck() - { - $result = $this->service->healthCheck(); - $this->assertTrue($result); - } - public function testGetJson() { - $result = $this->service->getJson(); + $endpoint = 'test'; + $result = $this->service->getJson($endpoint); $this->assertEquals('{"results":[{"name":"Games"}]}', $result); } } diff --git a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php index db9aed12..6ae7c400 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Standalone\StubServer; +use GuzzleHttp\Psr7\Uri; use PhpPact\Standalone\StubService\StubServerConfig; use PHPUnit\Framework\TestCase; @@ -9,20 +10,59 @@ class StubServerConfigTest extends TestCase { public function testSetters() { - $pactLocation = __DIR__ . '/../../../_resources/someconsumer-someprovider.json'; - $host = 'test-host'; - $port = 1234; - $log = 'test-log-dir/'; + $brokerUrl = new Uri('http://localhost'); + $port = 1234; + $extension = 'json'; + $logLevel = 'debug'; + $providerState = 'state'; + $providerStateHeaderName = 'header'; + $token = 'token'; + $user = 'user:password'; + $dirs = [__DIR__ . '/../../../_resources']; + $files = ['/path/to/pact.json']; + $urls = ['http://example.com/path/to/file.json']; + $consumerNames = ['consumer-1', 'consumer-2']; + $providerNames = ['provider-1', 'provider-2']; + $cors = true; + $corsReferer = true; + $emptyProviderState = true; + $insecureTls = true; $subject = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) + ->setBrokerUrl($brokerUrl) ->setPort($port) - ->setLog($log); + ->setExtension($extension) + ->setLogLevel($logLevel) + ->setProviderState($providerState) + ->setProviderStateHeaderName($providerStateHeaderName) + ->setToken($token) + ->setUser($user) + ->setDirs($dirs) + ->setFiles($files) + ->setUrls($urls) + ->setConsumerNames($consumerNames) + ->setProviderNames($providerNames) + ->setCors($cors) + ->setCorsReferer($corsReferer) + ->setEmptyProviderState($emptyProviderState) + ->setInsecureTls($insecureTls); - static::assertSame($pactLocation, $subject->getPactLocation()); - static::assertSame($host, $subject->getHost()); + static::assertSame($brokerUrl, $subject->getBrokerUrl()); static::assertSame($port, $subject->getPort()); - static::assertSame($log, $subject->getLog()); + static::assertSame($extension, $subject->getExtension()); + static::assertSame($logLevel, $subject->getLogLevel()); + static::assertSame($providerState, $subject->getProviderState()); + static::assertSame($providerStateHeaderName, $subject->getProviderStateHeaderName()); + static::assertSame($token, $subject->getToken()); + static::assertSame($user, $subject->getUser()); + static::assertSame($dirs, $subject->getDirs()); + static::assertSame($files, $subject->getFiles()); + static::assertSame($urls, $subject->getUrls()); + static::assertSame($consumerNames, $subject->getConsumerNames()); + static::assertSame($providerNames, $subject->getProviderNames()); + static::assertSame($cors, $subject->isCors()); + static::assertSame($corsReferer, $subject->isCorsReferer()); + static::assertSame($emptyProviderState, $subject->isEmptyProviderState()); + static::assertSame($insecureTls, $subject->isInsecureTls()); } } diff --git a/tests/PhpPact/Standalone/StubServer/StubServerTest.php b/tests/PhpPact/Standalone/StubServer/StubServerTest.php index 963fabf8..82410525 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerTest.php @@ -14,16 +14,12 @@ class StubServerTest extends TestCase public function testStartAndStop() { try { - $pactLocation = __DIR__ . '/../../../_resources/someconsumer-someprovider.json'; - $host = 'localhost'; - $port = 7201; - $endpoint = 'test'; + $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; + $port = 7201; $subject = (new StubServerConfig()) - ->setPactLocation($pactLocation) - ->setHost($host) - ->setPort($port) - ->setEndpoint($endpoint); + ->setFiles($files) + ->setPort($port); $stubServer = new StubServer($subject); $pid = $stubServer->start(); @@ -33,4 +29,24 @@ public function testStartAndStop() $this->assertTrue($result); } } + + /** + * @throws \Exception + */ + public function testRandomPort(): void + { + try { + $files = [__DIR__ . '/../../../_resources/someconsumer-someprovider.json']; + + $subject = (new StubServerConfig()) + ->setFiles($files); + + $stubServer = new StubServer($subject); + $stubServer->start(); + $this->assertGreaterThan(0, $subject->getPort()); + } finally { + $result = $stubServer->stop(); + $this->assertTrue($result); + } + } } diff --git a/tests/_public/index.php b/tests/_public/index.php new file mode 100644 index 00000000..cc71ddcc --- /dev/null +++ b/tests/_public/index.php @@ -0,0 +1,10 @@ + [ + [ + 'name' => 'g', + ], + ], +]);