From d333bc1c06ff1172f3d08fbf75e8b4cae448af21 Mon Sep 17 00:00:00 2001 From: "tien.xuan.vo" Date: Sat, 7 Oct 2023 23:20:21 +0700 Subject: [PATCH] feat: Add compatibility suite --- .gitmodules | 3 + .php-cs-fixer.php | 1 + behat.yml | 20 + compatibility-suite/pact-compatibility-suite | 1 + compatibility-suite/pacts/v1/.gitignore | 1 + .../tests/Context/Shared/ConsumerContext.php | 194 +++ .../Context/Shared/InteractionsContext.php | 21 + .../Context/Transform/InteractionsContext.php | 26 + compatibility-suite/tests/FeatureContext.php | 1385 +++++++++++++++++ .../tests/Service/InteractionsStorage.php | 39 + .../Service/InteractionsStorageInterface.php | 17 + .../Service/InteractionsTableTransformer.php | 182 +++ .../InteractionsTableTransformerInterface.php | 7 + .../tests/Service/MockServer.php | 49 + .../tests/Service/MockServerInterface.php | 17 + .../Service/TableTransformerInterface.php | 10 + composer.json | 7 +- 17 files changed, 1978 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 100644 behat.yml create mode 160000 compatibility-suite/pact-compatibility-suite create mode 100644 compatibility-suite/pacts/v1/.gitignore create mode 100644 compatibility-suite/tests/Context/Shared/ConsumerContext.php create mode 100644 compatibility-suite/tests/Context/Shared/InteractionsContext.php create mode 100644 compatibility-suite/tests/Context/Transform/InteractionsContext.php create mode 100644 compatibility-suite/tests/FeatureContext.php create mode 100644 compatibility-suite/tests/Service/InteractionsStorage.php create mode 100644 compatibility-suite/tests/Service/InteractionsStorageInterface.php create mode 100644 compatibility-suite/tests/Service/InteractionsTableTransformer.php create mode 100644 compatibility-suite/tests/Service/InteractionsTableTransformerInterface.php create mode 100644 compatibility-suite/tests/Service/MockServer.php create mode 100644 compatibility-suite/tests/Service/MockServerInterface.php create mode 100644 compatibility-suite/tests/Service/TableTransformerInterface.php diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..470a2e3ff --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "compatibility-suite/pact-compatibility-suite"] + path = compatibility-suite/pact-compatibility-suite + url = https://github.com/pact-foundation/pact-compatibility-suite.git diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index cf3181886..43a2591df 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -4,6 +4,7 @@ ->in(__DIR__ . '/src') ->in(__DIR__ . '/tests') ->in(__DIR__ . '/example') + ->in(__DIR__ . '/compatibility-suite/tests') ->name('*.php'); $config = new PhpCsFixer\Config(); diff --git a/behat.yml b/behat.yml new file mode 100644 index 000000000..a9f63bc95 --- /dev/null +++ b/behat.yml @@ -0,0 +1,20 @@ +default: + suites: + default: + paths: [ '%paths.base%/compatibility-suite/pact-compatibility-suite/features' ] + contexts: + - 'PhpPactTest\CompatibilitySuite\FeatureContext' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\InteractionsContext': + - '@interactions_storage' + - 'PhpPactTest\CompatibilitySuite\Context\Shared\ConsumerContext': + - '@interactions_storage' + - '@mock_server' + - 'PhpPactTest\CompatibilitySuite\Context\Transform\InteractionsContext': + - '@interactions_table_transformer' + services: + interactions_table_transformer: + class: 'PhpPactTest\CompatibilitySuite\Service\InteractionsTableTransformer' + interactions_storage: + class: 'PhpPactTest\CompatibilitySuite\Service\InteractionsStorage' + mock_server: + class: 'PhpPactTest\CompatibilitySuite\Service\MockServer' diff --git a/compatibility-suite/pact-compatibility-suite b/compatibility-suite/pact-compatibility-suite new file mode 160000 index 000000000..d22d4667c --- /dev/null +++ b/compatibility-suite/pact-compatibility-suite @@ -0,0 +1 @@ +Subproject commit d22d4667c0bda76d408676044cb33db834e7167e diff --git a/compatibility-suite/pacts/v1/.gitignore b/compatibility-suite/pacts/v1/.gitignore new file mode 100644 index 000000000..a6c57f5fb --- /dev/null +++ b/compatibility-suite/pacts/v1/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/compatibility-suite/tests/Context/Shared/ConsumerContext.php b/compatibility-suite/tests/Context/Shared/ConsumerContext.php new file mode 100644 index 000000000..53be18456 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/ConsumerContext.php @@ -0,0 +1,194 @@ +path); + } + + /** + * @When the mock server is started with interaction :id + */ + public function theMockServerIsStartedWithInteraction(int $id): void + { + $interaction = $this->storage->get($id); + $this->mockServer->register($interaction); + } + + /** + * @When request :id is made to the mock server + */ + public function requestIsMadeToTheMockServer(int $id): void + { + $request = $this->storage->get($id)->getRequest(); + $options = []; + $options['query'] = $request->getQuery(); + $options['headers'] = $request->getHeaders(); + $body = $request->getBody(); + if ($body instanceof Text) { + $options['body'] = $body->getContents(); + $options['headers']['Content-Type'] = $body->getContentType(); + } + $options['http_errors'] = false; + $client = new Client(); + $this->response = $client->request($request->getMethod(), $this->mockServer->getBaseUri()->withPath($request->getPath()), $options); + } + + /** + * @Then a :code success response is returned + */ + public function aSuccessResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->response->getStatusCode()); + } + + /** + * @Then the payload will contain the :name JSON document + */ + public function thePayloadWillContainTheJsonDocument(string $name): void + { + $path = sprintf(__DIR__ . '/../../../pact-compatibility-suite/fixtures/%s.json', $name); + Assert::assertSame(file_get_contents($path), (string) $this->response->getBody()); + } + + /** + * @Then the content type will be set as :contentType + */ + public function theContentTypeWillBeSetAs(string $contentType): void + { + Assert::assertSame($contentType, $this->response->getHeaderLine('Content-Type')); + } + + /** + * @When the pact test is done + */ + public function thePactTestIsDone(): void + { + $this->verifyResult = $this->mockServer->verify(); + } + + /** + * @Then the mock server status will be OK + */ + public function theMockServerStatusWillBeOk(): void + { + Assert::assertTrue($this->verifyResult); + } + + /** + * @Then the mock server will write out a Pact file for the interaction when done + */ + public function theMockServerWillWriteOutAPactFileForTheInteractionWhenDone(): void + { + Assert::assertTrue(file_exists($this->path)); + } + + /** + * @Then the pact file will contain {:num} interaction(s) + */ + public function thePactFileWillContainInteraction(int $num): void + { + $this->pact = json_decode(file_get_contents($this->path), true); + Assert::assertEquals($num, count($this->pact['interactions'] ?? [])); + } + + /** + * @Then the {first} interaction request will be for a :method + */ + public function theFirstInteractionRequestWillBeForA(string $method): void + { + Assert::assertSame($method, $this->pact['interactions'][0]['request']['method'] ?? null); + } + + /** + * @Then the {first} interaction response will contain the :fixture document + */ + public function theFirstInteractionResponseWillContainTheDocument(string $fixture): void + { + $path = sprintf(__DIR__ . '/../../../pact-compatibility-suite/fixtures/%s', $fixture); + Assert::assertEquals(json_decode(file_get_contents($path), true), $this->pact['interactions'][0]['response']['body'] ?? null); + } + + /** + * @When the mock server is started with interactions :ids + */ + public function theMockServerIsStartedWithInteractions(string $ids): void + { + $ids = array_map(fn (string $id) => (int) trim($id), explode(',', $ids)); + $interactions = array_map(fn (int $id) => $this->storage->get($id), $ids); + $this->mockServer->register(...$interactions); + } + + /** + * @Then the mock server status will NOT be OK + */ + public function theMockServerStatusWillNotBeOk(): void + { + Assert::assertFalse($this->verifyResult); + } + + /** + * @Then the mock server will NOT write out a Pact file for the interactions when done + */ + public function theMockServerWillNotWriteOutAPactFileForTheInteractionsWhenDone(): void + { + Assert::assertFalse(file_exists($this->path)); + } + + /** + * @Then the mock server status will be an expected but not received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnExpectedButNotReceivedErrorForInteraction(int $id): void + { + $request = $this->storage->get($id)->getRequest(); + $mismatches = $this->mockServer->getMismatches(); + Assert::assertCount(1, $mismatches); + $mismatch = current($mismatches); + Assert::assertSame('missing-request', $mismatch['type']); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + Assert::assertSame($request->getQuery(), $mismatch['request']['query']); + // TODO assert headers, body + } + + /** + * @Then a :code error response is returned + */ + public function aErrorResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->response->getStatusCode()); + } + + /** + * @Then the mock server status will be an unexpected :method request received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnUnexpectedRequestReceivedErrorForInteraction(string $method, int $id): void + { + $request = $this->storage->get($id)->getRequest(); + $mismatches = $this->mockServer->getMismatches(); + Assert::assertCount(2, $mismatches); + $notFoundRequests = array_filter($mismatches, fn (array $mismatch) => $mismatch['type'] === 'request-not-found'); + $mismatch = current($notFoundRequests); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + // TODO assert query, headers, body + } +} diff --git a/compatibility-suite/tests/Context/Shared/InteractionsContext.php b/compatibility-suite/tests/Context/Shared/InteractionsContext.php new file mode 100644 index 000000000..eabb21820 --- /dev/null +++ b/compatibility-suite/tests/Context/Shared/InteractionsContext.php @@ -0,0 +1,21 @@ +storage->set($interactions); + } +} diff --git a/compatibility-suite/tests/Context/Transform/InteractionsContext.php b/compatibility-suite/tests/Context/Transform/InteractionsContext.php new file mode 100644 index 000000000..a7d257512 --- /dev/null +++ b/compatibility-suite/tests/Context/Transform/InteractionsContext.php @@ -0,0 +1,26 @@ + + */ + public function getInteractions(TableNode $table): array + { + return $this->transformer->transform($table); + } +} diff --git a/compatibility-suite/tests/FeatureContext.php b/compatibility-suite/tests/FeatureContext.php new file mode 100644 index 000000000..8c225e5b5 --- /dev/null +++ b/compatibility-suite/tests/FeatureContext.php @@ -0,0 +1,1385 @@ + {string} + */ + public function theMismatchesWillContainAMismatchWithError2($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given an expected request with an :arg1 header of :arg2 + */ + public function anExpectedRequestWithAnHeaderOf($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a request is received with an :arg1 header of :arg2 + */ + public function aRequestIsReceivedWithAnHeaderOf($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for interaction :arg1 is to be verified with the following provider states defined: + */ + public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingProviderStatesDefined($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the provider state callback will receive a setup call with :arg1 and the following parameters: + */ + public function theProviderStateCallbackWillReceiveASetupCallWithAndTheFollowingParameters($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the provider state callback will receive a teardown call :arg1 and the following parameters: + */ + public function theProviderStateCallbackWillReceiveATeardownCallAndTheFollowingParameters($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given an expected request configured with the following: + */ + public function anExpectedRequestConfiguredWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given a request is received with the following: + */ + public function aRequestIsReceivedWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given the following requests are received: + */ + public function theFollowingRequestsAreReceived(TableNode $table) + { + throw new PendingException(); + } + + /** + * @When the requests are compared to the expected one + */ + public function theRequestsAreComparedToTheExpectedOne() + { + throw new PendingException(); + } + + /** + * @Given a message integration is being defined for a consumer test + */ + public function aMessageIntegrationIsBeingDefinedForAConsumerTest() + { + throw new PendingException(); + } + + /** + * @Given the message payload contains the :arg1 JSON document + */ + public function theMessagePayloadContainsTheJsonDocument($arg1) + { + throw new PendingException(); + } + + /** + * @When the message is successfully processed + */ + public function theMessageIsSuccessfullyProcessed() + { + throw new PendingException(); + } + + /** + * @Then the received message payload will contain the :arg1 JSON document + */ + public function theReceivedMessagePayloadWillContainTheJsonDocument($arg1) + { + throw new PendingException(); + } + + /** + * @Then the received message content type will be :arg1 + */ + public function theReceivedMessageContentTypeWillBe($arg1) + { + throw new PendingException(); + } + + /** + * @Then the consumer test will have passed + */ + public function theConsumerTestWillHavePassed() + { + throw new PendingException(); + } + + /** + * @Then a Pact file for the message interaction will have been written + */ + public function aPactFileForTheMessageInteractionWillHaveBeenWritten() + { + throw new PendingException(); + } + + /** + * @Then the pact file will contain :arg1 message interaction + */ + public function thePactFileWillContainMessageInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain the :arg1 document + */ + public function theFirstMessageInThePactFileWillContainTheDocument($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file content type will be :arg1 + */ + public function theFirstMessageInThePactFileContentTypeWillBe($arg1) + { + throw new PendingException(); + } + + /** + * @When the message is NOT successfully processed with a :arg1 exception + */ + public function theMessageIsNotSuccessfullyProcessedWithAException($arg1) + { + throw new PendingException(); + } + + /** + * @Then the consumer test will have failed + */ + public function theConsumerTestWillHaveFailed() + { + throw new PendingException(); + } + + /** + * @Then the consumer test error will be :arg1 + */ + public function theConsumerTestErrorWillBe($arg1) + { + throw new PendingException(); + } + + /** + * @Then a Pact file for the message interaction will NOT have been written + */ + public function aPactFileForTheMessageInteractionWillNotHaveBeenWritten() + { + throw new PendingException(); + } + + /** + * @Given the message contains the following metadata: + */ + public function theMessageContainsTheFollowingMetadata(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the received message metadata will contain :arg1 == :arg2 + */ + public function theReceivedMessageMetadataWillContain($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message metadata will contain :arg1 == "JSON: { \:arg2: \:arg3, \:arg4: :arg5 }" + */ + public function theReceivedMessageMetadataWillContainJson($arg1, $arg2, $arg3, $arg4, $arg5) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain the message metadata :arg1 == :arg2 + */ + public function theFirstMessageInThePactFileWillContainTheMessageMetadata($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain the message metadata :arg1 == "JSON: { \:arg2: \:arg3, \:arg4: :arg5 }" + */ + public function theFirstMessageInThePactFileWillContainTheMessageMetadataJson($arg1, $arg2, $arg3, $arg4, $arg5) + { + throw new PendingException(); + } + + /** + * @Given a provider state :arg1 for the message is specified + */ + public function aProviderStateForTheMessageIsSpecified($arg1) + { + throw new PendingException(); + } + + /** + * @Given a message is defined + */ + public function aMessageIsDefined() + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain :arg1 provider states + */ + public function theFirstMessageInThePactFileWillContainProviderStates($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first message in the Pact file will contain provider state :arg1 + */ + public function theFirstMessageInThePactFileWillContainProviderState($arg1) + { + throw new PendingException(); + } + + /** + * @Given a provider state :arg1 for the message is specified with the following data: + */ + public function aProviderStateForTheMessageIsSpecifiedWithTheFollowingData($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain :arg1 provider state + */ + public function theFirstMessageInThePactFileWillContainProviderState2($arg1) + { + throw new PendingException(); + } + + /** + * @Then the provider state :arg1 for the message will contain the following parameters: + */ + public function theProviderStateForTheMessageWillContainTheFollowingParameters($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given the message is configured with the following: + */ + public function theMessageIsConfiguredWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the message contents for :arg1 will have been replaced with an :arg2 + */ + public function theMessageContentsForWillHaveBeenReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message metadata will contain :arg1 replaced with an :arg2 + */ + public function theReceivedMessageMetadataWillContainReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a provider is started that can generate the :arg1 message with :arg2 + */ + public function aProviderIsStartedThatCanGenerateTheMessageWith($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1::arg2 is to be verified + */ + public function aPactFileForIsToBeVerified($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a provider is started that can generate the :arg1 message with "JSON: { \:arg2: \:arg3, \:arg4: \:arg5 }" + */ + public function aProviderIsStartedThatCanGenerateTheMessageWithJson($arg1, $arg2, $arg3, $arg4, $arg5) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1::arg2 is to be verified with provider state :arg3 + */ + public function aPactFileForIsToBeVerifiedWithProviderState($arg1, $arg2, $arg3) + { + throw new PendingException(); + } + + /** + * @Given a provider is started that can generate the :arg1 message with :arg2 and the following metadata: + */ + public function aProviderIsStartedThatCanGenerateTheMessageWithAndTheFollowingMetadata($arg1, $arg2, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1::arg2 is to be verified with the following metadata: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowingMetadata($arg1, $arg2, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1 is to be verified with the following: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowing($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the body value for :arg1 will have been replaced with :arg2 + */ + public function theBodyValueForWillHaveBeenReplacedWith($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given an HTTP interaction is being defined for a consumer test + */ + public function anHttpInteractionIsBeingDefinedForAConsumerTest() + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the Pact file will have a type of :arg1 + */ + public function theFirstInteractionInThePactFileWillHaveATypeOf($arg1) + { + throw new PendingException(); + } + + /** + * @Given a key of :arg1 is specified for the HTTP interaction + */ + public function aKeyOfIsSpecifiedForTheHttpInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the Pact file will have :arg1 = :arg3 + */ + public function theFirstInteractionInThePactFileWillHave($arg1, $arg2, $arg3) + { + throw new PendingException(); + } + + /** + * @Given the HTTP interaction is marked as pending + */ + public function theHttpInteractionIsMarkedAsPending() + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the Pact file will have :arg1 = :arg2 + */ + public function theFirstInteractionInThePactFileWillHave2($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a comment :arg1 is added to the HTTP interaction + */ + public function aCommentIsAddedToTheHttpInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the Pact file will have :arg1 = :arg4 + */ + public function theFirstInteractionInThePactFileWillHave3($arg1, $arg2, $arg3, $arg4) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for interaction :arg1 is to be verified, but is marked pending + */ + public function aPactFileForInteractionIsToBeVerifiedButIsMarkedPending($arg1) + { + throw new PendingException(); + } + + /** + * @Then there will be a pending :arg1 error + */ + public function thereWillBeAPendingError($arg1) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for interaction :arg1 is to be verified with the following comments: + */ + public function aPactFileForInteractionIsToBeVerifiedWithTheFollowingComments($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the comment :arg1 will have been printed to the console + */ + public function theCommentWillHaveBeenPrintedToTheConsole($arg1) + { + throw new PendingException(); + } + + /** + * @Then the :arg1 will displayed as the original test name + */ + public function theWillDisplayedAsTheOriginalTestName($arg1) + { + throw new PendingException(); + } + + /** + * @Given an expected response configured with the following: + */ + public function anExpectedResponseConfiguredWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given a status :arg1 response is received + */ + public function aStatusResponseIsReceived($arg1) + { + throw new PendingException(); + } + + /** + * @When the response is compared to the expected one + */ + public function theResponseIsComparedToTheExpectedOne() + { + throw new PendingException(); + } + + /** + * @Then the response comparison should be OK + */ + public function theResponseComparisonShouldBeOk() + { + throw new PendingException(); + } + + /** + * @Then the response comparison should NOT be OK + */ + public function theResponseComparisonShouldNotBeOk() + { + throw new PendingException(); + } + + /** + * @Then the response mismatches will contain a :arg1 mismatch with error :arg2 + */ + public function theResponseMismatchesWillContainAMismatchWithError($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a message interaction is being defined for a consumer test + */ + public function aMessageInteractionIsBeingDefinedForAConsumerTest() + { + throw new PendingException(); + } + + /** + * @Given a key of :arg1 is specified for the message interaction + */ + public function aKeyOfIsSpecifiedForTheMessageInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Given the message interaction is marked as pending + */ + public function theMessageInteractionIsMarkedAsPending() + { + throw new PendingException(); + } + + /** + * @Given a comment :arg1 is added to the message interaction + */ + public function aCommentIsAddedToTheMessageInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1::arg2 is to be verified, but is marked pending + */ + public function aPactFileForIsToBeVerifiedButIsMarkedPending($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Given a Pact file for :arg1::arg2 is to be verified with the following comments: + */ + public function aPactFileForIsToBeVerifiedWithTheFollowingComments($arg1, $arg2, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given a synchronous message interaction is being defined for a consumer test + */ + public function aSynchronousMessageInteractionIsBeingDefinedForAConsumerTest() + { + throw new PendingException(); + } + + /** + * @Given a key of :arg1 is specified for the synchronous message interaction + */ + public function aKeyOfIsSpecifiedForTheSynchronousMessageInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Given the synchronous message interaction is marked as pending + */ + public function theSynchronousMessageInteractionIsMarkedAsPending() + { + throw new PendingException(); + } + + /** + * @Given a comment :arg1 is added to the synchronous message interaction + */ + public function aCommentIsAddedToTheSynchronousMessageInteraction($arg1) + { + throw new PendingException(); + } + + /** + * @Given the message request payload contains the :arg1 JSON document + */ + public function theMessageRequestPayloadContainsTheJsonDocument($arg1) + { + throw new PendingException(); + } + + /** + * @Given the message response payload contains the :arg1 document + */ + public function theMessageResponsePayloadContainsTheDocument($arg1) + { + throw new PendingException(); + } + + /** + * @Then the received message payload will contain the :arg1 document + */ + public function theReceivedMessagePayloadWillContainTheDocument($arg1) + { + throw new PendingException(); + } + + /** + * @Then the pact file will contain :arg1 interaction + */ + public function thePactFileWillContainInteraction2($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file will contain the :arg1 document as the request + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheRequest($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file request content type will be :arg1 + */ + public function theFirstInteractionInThePactFileRequestContentTypeWillBe($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file will contain the :arg1 document as a response + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsAResponse($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file response content type will be :arg1 + */ + public function theFirstInteractionInThePactFileResponseContentTypeWillBe($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file will contain :arg1 response messages + */ + public function theFirstInteractionInThePactFileWillContainResponseMessages($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file will contain the :arg1 document as the first response message + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheFirstResponseMessage($arg1) + { + throw new PendingException(); + } + + /** + * @Then the first interaction in the pact file will contain the :arg1 document as the second response message + */ + public function theFirstInteractionInThePactFileWillContainTheDocumentAsTheSecondResponseMessage($arg1) + { + throw new PendingException(); + } + + /** + * @Given the message request contains the following metadata: + */ + public function theMessageRequestContainsTheFollowingMetadata(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the received message request metadata will contain :arg1 == :arg2 + */ + public function theReceivedMessageRequestMetadataWillContain($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message request metadata will contain :arg1 == "JSON: { \:arg2: \:arg3, \:arg4: :arg5 }" + */ + public function theReceivedMessageRequestMetadataWillContainJson($arg1, $arg2, $arg3, $arg4, $arg5) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain the request message metadata :arg1 == :arg2 + */ + public function theFirstMessageInThePactFileWillContainTheRequestMessageMetadata($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the first message in the pact file will contain the request message metadata :arg1 == "JSON: { \:arg2: \:arg3, \:arg4: :arg5 }" + */ + public function theFirstMessageInThePactFileWillContainTheRequestMessageMetadataJson($arg1, $arg2, $arg3, $arg4, $arg5) + { + throw new PendingException(); + } + + /** + * @Given a provider state :arg1 for the synchronous message is specified + */ + public function aProviderStateForTheSynchronousMessageIsSpecified($arg1) + { + throw new PendingException(); + } + + /** + * @Given a provider state :arg1 for the synchronous message is specified with the following data: + */ + public function aProviderStateForTheSynchronousMessageIsSpecifiedWithTheFollowingData($arg1, TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given the message request is configured with the following: + */ + public function theMessageRequestIsConfiguredWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Given the message response is configured with the following: + */ + public function theMessageResponseIsConfiguredWithTheFollowing(TableNode $table) + { + throw new PendingException(); + } + + /** + * @Then the message request contents for :arg1 will have been replaced with an :arg2 + */ + public function theMessageRequestContentsForWillHaveBeenReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the message response contents for :arg1 will have been replaced with an :arg2 + */ + public function theMessageResponseContentsForWillHaveBeenReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message request metadata will contain :arg1 replaced with an :arg2 + */ + public function theReceivedMessageRequestMetadataWillContainReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message response metadata will contain :arg1 == :arg2 + */ + public function theReceivedMessageResponseMetadataWillContain($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then the received message response metadata will contain :arg1 replaced with an :arg2 + */ + public function theReceivedMessageResponseMetadataWillContainReplacedWithAn($arg1, $arg2) + { + throw new PendingException(); + } + + /** + * @Then there will be an interaction in the Pact file with a type of :arg1 + */ + public function thereWillBeAnInteractionInThePactFileWithATypeOf($arg1) + { + throw new PendingException(); + } +} diff --git a/compatibility-suite/tests/Service/InteractionsStorage.php b/compatibility-suite/tests/Service/InteractionsStorage.php new file mode 100644 index 000000000..8b6ff4a67 --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsStorage.php @@ -0,0 +1,39 @@ + + */ + private array $interactions = []; + + /** + * @param array $interactions + */ + public function set(array $interactions): void + { + $this->interactions = []; + foreach ($interactions as $id => $interaction) { + $this->add($id, $interaction); + } + } + + public function add(int $id, Interaction $interaction): void + { + $this->interactions[$id] = $interaction; + } + + public function get(int $id): Interaction + { + if (!isset($this->interactions[$id])) { + throw new Exception(sprintf('Interaction %s is not defined', $id)); + } + + return $this->interactions[$id]; + } +} diff --git a/compatibility-suite/tests/Service/InteractionsStorageInterface.php b/compatibility-suite/tests/Service/InteractionsStorageInterface.php new file mode 100644 index 000000000..03d3ccd1f --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsStorageInterface.php @@ -0,0 +1,17 @@ + $interactions + */ + public function set(array $interactions): void; + + public function add(int $id, Interaction $interaction): void; + + public function get(int $id): Interaction; +} diff --git a/compatibility-suite/tests/Service/InteractionsTableTransformer.php b/compatibility-suite/tests/Service/InteractionsTableTransformer.php new file mode 100644 index 000000000..6911e1f7c --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsTableTransformer.php @@ -0,0 +1,182 @@ + + */ + public function transform(TableNode $table): array + { + $interactions = []; + foreach ($table->getHash() as $interactionHash) { + $id = (int) $interactionHash['No']; + $interaction = new Interaction(); + $interaction->setDescription("Interaction $id"); + $interaction->setRequest( + $this->getRequest( + $interactionHash['method'], + $interactionHash['path'], + $this->parseQueryString($interactionHash['query']), + $this->parseHeaders($interactionHash['headers']), + $this->parseBody($interactionHash['body']) + ) + ); + $interaction->setResponse( + $this->getResponse( + $interactionHash['response'], + $this->parseHeaders($interactionHash['response headers'] ?? ''), + $this->parseBody($interactionHash['response body']) + ) + ); + $interactions[$id] = $interaction; + } + + return $interactions; + } + + private function getRequest(string $method, string $path, array $query, array $headers, Text|Binary|Multipart|null $body): ConsumerRequest + { + $request = new ConsumerRequest(); + $request + ->setMethod($method) + ->setPath($path) + ->setQuery($query) + ->setHeaders($headers) + ->setBody($body); + + return $request; + } + + private function parseHeaders(string $headers): array + { + if (empty($headers)) { + return []; + } + return array_reduce( + explode(',', $headers), + function (array $values, string $header): array { + [$header, $value] = explode(':', rtrim(ltrim($header, "'"), "'"), 2); + + $header = trim($header); + $value = trim($value); + + if (in_array(strtolower($header), self::SINGLE_VALUE_HEADERS)) { + $values[$header] = [$value]; + } else { + $values[$header] = array_map(fn (string $value) => trim($value), explode(',', $value)); + } + + return $values; + }, + [] + ); + } + + private function parseBody(string $body, ?string $contentType = null): Text|Binary|Multipart|null + { + if (empty($body)) { + return null; + } + if (str_starts_with($body, 'JSON:')) { + return new Text(trim(substr($body, 5)), 'application/json'); + } + if (str_starts_with($body, 'XML:')) { + return new Text(trim(substr($body, 4)), 'application/xml'); + } + if (str_starts_with($body, 'file:')) { + $fileName = trim(substr($body, 5)); + $filePath = sprintf(__DIR__ . '/../../pact-compatibility-suite/fixtures/%s', $fileName); + if (!file_exists($filePath)) { + throw new Exception(sprintf("could not load fixture '%s'", $fileName)); + } + $contents = file_get_contents($filePath); + if (str_ends_with($fileName, '-body.xml')) { + $body = simplexml_load_string($contents); + if (!$body) { + throw new Exception(sprintf("could not read fixture '%s'", $fileName)); + } + $contentType = $body->contentType ?? 'text/plain'; + $contents = $body->contents ?? ''; + + return new Text($contents, $contentType); + } else { + $contentType ??= $this->determineContentType($fileName); + + return new Binary($contents, $contentType); + } + } + if ($body === 'EMPTY' && $contentType) { + return new Text('', $contentType); + } + return null; + } + + private function determineContentType(string $fileName): string + { + if (str_ends_with($fileName, '.json')) { + return 'application/json'; + } elseif (str_ends_with($fileName, '.xml')) { + return 'application/xml'; + } elseif (str_ends_with($fileName, '.jpg')) { + return 'image/jpeg'; + } elseif (str_ends_with($fileName, '.pdf')) { + return 'application/pdf'; + } else { + return 'text/plain'; + } + } + + private function parseQueryString(string $query): array + { + if (empty($query)) { + return []; + } + return array_reduce( + explode('&', $query), + function (array $values, string $kv): array { + if (str_contains($kv, '=')) { + [$key, $value] = explode('=', $kv, 2); + $values[$key][] = $value; + } else { + $values[$kv][] = ''; + } + + return $values; + }, + [] + ); + } + + private function getResponse(int $status, array $headers, Text|Binary|Multipart|null $body): ProviderResponse + { + $response = new ProviderResponse(); + $response + ->setStatus($status) + ->setHeaders($headers) + ->addHeader('Content-Type', 'application/json') + ->setBody($body); + + return $response; + } +} diff --git a/compatibility-suite/tests/Service/InteractionsTableTransformerInterface.php b/compatibility-suite/tests/Service/InteractionsTableTransformerInterface.php new file mode 100644 index 000000000..d564b3ae6 --- /dev/null +++ b/compatibility-suite/tests/Service/InteractionsTableTransformerInterface.php @@ -0,0 +1,7 @@ +config = new MockServerConfig(); + $this->config + ->setConsumer('v1-compatibility-suite-c') + ->setProvider('p') + ->setPactDir(__DIR__.'/../../pacts/v1') + ->setPactSpecificationVersion('1.0.0') + ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); + $this->driver = (new InteractionDriverFactory())->create($this->config); + } + + public function register(Interaction ...$interactions): void + { + $this->driver->registerInteractions(...$interactions); + } + + public function getBaseUri(): UriInterface + { + return $this->config->getBaseUri(); + } + + public function verify(): bool + { + return $this->driver->verifyInteractions(); + } + + public function getMismatches(): array + { + return $this->driver->getMismatches(); + } +} diff --git a/compatibility-suite/tests/Service/MockServerInterface.php b/compatibility-suite/tests/Service/MockServerInterface.php new file mode 100644 index 000000000..c21f020f5 --- /dev/null +++ b/compatibility-suite/tests/Service/MockServerInterface.php @@ -0,0 +1,17 @@ +=8.5.23 <10", - "guzzlehttp/guzzle": "^7.8" + "guzzlehttp/guzzle": "^7.8", + "behat/behat": "^3.13" }, "autoload": { "psr-4": { @@ -43,6 +44,7 @@ "autoload-dev": { "psr-4": { "PhpPactTest\\": "tests/PhpPact", + "PhpPactTest\\CompatibilitySuite\\": "compatibility-suite/tests", "JsonConsumer\\": "example/json/consumer/src", "JsonConsumer\\Tests\\": "example/json/consumer/tests", "JsonProvider\\": "example/json/provider/src", @@ -69,7 +71,8 @@ "test": [ "php -r \"array_map('unlink', glob('./example/*/pacts/*.json'));\"", "phpunit --debug" - ] + ], + "check-compatibility": "behat" }, "extra": { "downloads": {