diff --git a/compatibility-suite/tests/Constant/Mismatch.php b/compatibility-suite/tests/Constant/Mismatch.php new file mode 100644 index 00000000..2b5ab806 --- /dev/null +++ b/compatibility-suite/tests/Constant/Mismatch.php @@ -0,0 +1,32 @@ + false, + 'PathMismatch' => false, + 'StatusMismatch' => 'Response status did not match', + 'QueryMismatch' => false, + 'HeaderMismatch' => 'Headers had differences', + 'BodyTypeMismatch' => 'Body type had differences', + 'BodyMismatch' => 'Body had differences', + 'MetadataMismatch' => 'Metadata had differences', + ]; + + public const VERIFIER_MISMATCH_ERROR_MAP = [ + 'One or more of the setup state change handlers has failed' => 'State change request failed', + ]; + + public const MOCK_SERVER_MISMATCH_TYPE_MAP = [ + 'method' => 'MethodMismatch', + 'path' => 'PathMismatch', + 'status' => 'StatusMismatch', + 'query' => 'QueryMismatch', + 'header' => 'HeaderMismatch', + 'body-content-type' => 'BodyTypeMismatch', + 'body' => 'BodyMismatch', + 'metadata' => 'MetadataMismatch', + ]; +} diff --git a/compatibility-suite/tests/Context/Shared/InteractionsContext.php b/compatibility-suite/tests/Context/Shared/InteractionsContext.php index be01ca39..d7d911dd 100644 --- a/compatibility-suite/tests/Context/Shared/InteractionsContext.php +++ b/compatibility-suite/tests/Context/Shared/InteractionsContext.php @@ -3,6 +3,7 @@ namespace PhpPactTest\CompatibilitySuite\Context\Shared; use Behat\Behat\Context\Context; +use PhpPact\Consumer\Model\Interaction; use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface; use PhpPactTest\CompatibilitySuite\Service\MatchingRulesStorageInterface; use PhpPactTest\CompatibilitySuite\Service\RequestMatchingRuleBuilderInterface; @@ -24,16 +25,30 @@ public function __construct( public function theFollowingHttpInteractionsHaveBeenDefined(array $interactions): void { foreach ($interactions as $id => $interaction) { - $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_DOMAIN, $id, $interaction); - $this->storage->add(InteractionsStorageInterface::MOCK_SERVER_CLIENT_DOMAIN, $id, $interaction, true); - $this->storage->add(InteractionsStorageInterface::PROVIDER_DOMAIN, $id, $interaction, true); - $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id, $interaction); - if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { - $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $file); - } - if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id)) { - $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $file); - } + $this->storeInteractionWithoutMatchingRules($id, $interaction); + $this->storeInteractionWithMatchingRules($id, $interaction); + } + } + + private function storeInteractionWithoutMatchingRules(int $id, Interaction $interaction): void + { + $this->storage->add(InteractionsStorageInterface::CLIENT_DOMAIN, $id, $interaction, true); + } + + private function storeInteractionWithMatchingRules(int $id, Interaction $interaction): void + { + $this->buildMatchingRules($id, $interaction); + $this->storage->add(InteractionsStorageInterface::SERVER_DOMAIN, $id, $interaction, true); + $this->storage->add(InteractionsStorageInterface::PACT_WRITER_DOMAIN, $id, $interaction, true); + } + + private function buildMatchingRules(int $id, Interaction $interaction): void + { + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::REQUEST_DOMAIN, $id)) { + $this->requestMatchingRuleBuilder->build($interaction->getRequest(), $file); + } + if ($file = $this->matchingRulesStorage->get(MatchingRulesStorageInterface::RESPONSE_DOMAIN, $id)) { + $this->responseMatchingRuleBuilder->build($interaction->getResponse(), $file); } } } diff --git a/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php b/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php new file mode 100644 index 00000000..a721d0df --- /dev/null +++ b/compatibility-suite/tests/Context/V1/Http/ConsumerContext.php @@ -0,0 +1,295 @@ +server->register($id); + } + + /** + * @When request :id is made to the mock server + */ + public function requestIsMadeToTheMockServer(int $id): void + { + $this->client->sendRequestToServer($id); + } + + /** + * @Then a :code success response is returned + */ + public function aSuccessResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->client->getResponse()->getStatusCode()); + } + + /** + * @Then the payload will contain the :name JSON document + */ + public function thePayloadWillContainTheJsonDocument(string $name): void + { + Assert::assertJsonStringEqualsJsonString($this->fixtureLoader->load($name . '.json'), (string) $this->client->getResponse()->getBody()); + } + + /** + * @Then the content type will be set as :contentType + */ + public function theContentTypeWillBeSetAs(string $contentType): void + { + Assert::assertSame($contentType, $this->client->getResponse()->getHeaderLine('Content-Type')); + } + + /** + * @When the pact test is done + */ + public function thePactTestIsDone(): void + { + $this->server->verify(); + } + + /** + * @Then the mock server status will be OK + */ + public function theMockServerStatusWillBeOk(): void + { + Assert::assertTrue($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then the mock server will write out a Pact file for the interaction when done + */ + public function theMockServerWillWriteOutAPactFileForTheInteractionWhenDone(): void + { + Assert::assertTrue(file_exists($this->server->getPactPath())); + } + + /** + * @Then the pact file will contain {:num} interaction(s) + */ + public function thePactFileWillContainInteraction(int $num): void + { + $this->pact = json_decode(file_get_contents($this->server->getPactPath()), true); + Assert::assertEquals($num, count($this->pact['interactions'] ?? [])); + } + + /** + * @Then the {first} interaction request will be for a :method + */ + public function theFirstInteractionRequestWillBeForA(string $method): void + { + Assert::assertSame($method, $this->pact['interactions'][0]['request']['method'] ?? null); + } + + /** + * @Then the {first} interaction response will contain the :fixture document + */ + public function theFirstInteractionResponseWillContainTheDocument(string $fixture): void + { + Assert::assertEquals($this->fixtureLoader->loadJson($fixture), $this->pact['interactions'][0]['response']['body'] ?? null); + } + + /** + * @When the mock server is started with interactions :ids + */ + public function theMockServerIsStartedWithInteractions(string $ids): void + { + $ids = array_map(fn (string $id) => (int) trim($id), explode(',', $ids)); + $this->server->register(...$ids); + } + + /** + * @Then the mock server status will NOT be OK + */ + public function theMockServerStatusWillNotBeOk(): void + { + Assert::assertFalse($this->server->getVerifyResult()->isSuccess()); + } + + /** + * @Then the mock server will NOT write out a Pact file for the interactions when done + */ + public function theMockServerWillNotWriteOutAPactFileForTheInteractionsWhenDone(): void + { + Assert::assertFileDoesNotExist($this->server->getPactPath()); + } + + /** + * @Then the mock server status will be an expected but not received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnExpectedButNotReceivedErrorForInteraction(int $id): void + { + $request = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getRequest(); + $mismatches = $this->getMismatches(); + Assert::assertCount(1, $mismatches); + $mismatch = current($mismatches); + Assert::assertSame('missing-request', $mismatch['type']); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + Assert::assertSame($request->getQuery(), $mismatch['request']['query']); + // TODO assert headers, body + } + + /** + * @Then a :code error response is returned + */ + public function aErrorResponseIsReturned(int $code): void + { + Assert::assertSame($code, $this->client->getResponse()->getStatusCode()); + } + + /** + * @Then the mock server status will be an unexpected :method request received error for interaction {:id} + */ + public function theMockServerStatusWillBeAnUnexpectedRequestReceivedErrorForInteraction(string $method, int $id): void + { + $request = $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id)->getRequest(); + $mismatches = $this->getMismatches(); + Assert::assertCount(2, $mismatches); + $notFoundRequests = array_filter($mismatches, fn (array $mismatch) => $mismatch['type'] === 'request-not-found'); + $mismatch = current($notFoundRequests); + Assert::assertSame($request->getMethod(), $mismatch['request']['method']); + Assert::assertSame($request->getPath(), $mismatch['request']['path']); + // TODO assert query, headers, body + } + + /** + * @Then the {first} interaction request query parameters will be :query + */ + public function theFirstInteractionRequestQueryParametersWillBe(string $query) + { + Assert::assertEquals($query, $this->pact['interactions'][0]['request']['query']); + } + + /** + * @When request :id is made to the mock server with the following changes: + */ + public function requestIsMadeToTheMockServerWithTheFollowingChanges(int $id, TableNode $table) + { + $request = $this->storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $id)->getRequest(); + $this->requestBuilder->build($request, $table->getHash()[0]); + $this->requestIsMadeToTheMockServer($id); + } + + /** + * @Then the mock server status will be mismatches + */ + public function theMockServerStatusWillBeMismatches(): void + { + $mismatches = $this->getMismatches(); + Assert::assertNotEmpty($mismatches); + } + + /** + * @Then the mismatches will contain a :type mismatch with error :error + */ + public function theMismatchesWillContainAMismatchWithError(string $type, string $error): void + { + $mismatches = $this->getMismatches(); + $mismatch = current($mismatches); + Assert::assertSame('request-mismatch', $mismatch['type']); + $mismatches = array_filter( + $mismatch['mismatches'], + fn (array $mismatch) => $mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$type] + && str_contains($mismatch['mismatch'], $error) + ); + Assert::assertNotEmpty($mismatches); + } + + /** + * @Then the mock server will NOT write out a Pact file for the interaction when done + */ + public function theMockServerWillNotWriteOutAPactFileForTheInteractionWhenDone(): void + { + Assert::assertFileDoesNotExist($this->server->getPactPath()); + } + + /** + * @Then the mock server status will be an unexpected :method request received error for path :path + */ + public function theMockServerStatusWillBeAnUnexpectedRequestReceivedErrorForPath(string $method, string $path): void + { + $mismatches = $this->getMismatches(); + Assert::assertCount(2, $mismatches); + $notFoundRequests = array_filter($mismatches, fn (array $mismatch) => $mismatch['type'] === 'request-not-found'); + $mismatch = current($notFoundRequests); + Assert::assertSame($method, $mismatch['request']['method']); + Assert::assertSame($path, $mismatch['request']['path']); + } + + /** + * @Then the {first} interaction request will contain the header :header with value :value + */ + public function theFirstInteractionRequestWillContainTheHeaderWithValue(string $header, string $value): void + { + Assert::assertArrayHasKey($header, $this->pact['interactions'][0]['request']['headers']); + Assert::assertSame($value, $this->pact['interactions'][0]['request']['headers'][$header]); + } + + /** + * @Then the {first} interaction request content type will be :contentType + */ + public function theFirstInteractionRequestContentTypeWillBe(string $contentType): void + { + Assert::assertSame($contentType, $this->pact['interactions'][0]['request']['headers']['Content-Type']); + } + + /** + * @Then the {first} interaction request will contain the :fixture document + */ + public function theFirstInteractionRequestWillContainTheDocument(string $fixture): void + { + Assert::assertEquals($this->fixtureLoader->loadJson($fixture), $this->pact['interactions'][0]['request']['body'] ?? null); + } + + /** + * @Then the mismatches will contain a :type mismatch with path :path with error :error + */ + public function theMismatchesWillContainAMismatchWithPathWithError(string $type, string $path, string $error): void + { + $mismatches = $this->getMismatches(); + $mismatch = current($mismatches); + Assert::assertSame('request-mismatch', $mismatch['type']); + $mismatches = array_filter( + $mismatch['mismatches'], + fn (array $mismatch) => $mismatch['type'] === Mismatch::MOCK_SERVER_MISMATCH_TYPE_MAP[$type] + && $mismatch['path'] === $path + && str_contains($mismatch['mismatch'], $error) + ); + Assert::assertNotEmpty($mismatches); + } + + private function getMismatches(): array + { + if ($this->server->getVerifyResult()->isSuccess()) { + return []; + } + + return json_decode($this->server->getVerifyResult()->getOutput(), true); + } +} diff --git a/compatibility-suite/tests/Service/Client.php b/compatibility-suite/tests/Service/Client.php new file mode 100644 index 00000000..ae1e2930 --- /dev/null +++ b/compatibility-suite/tests/Service/Client.php @@ -0,0 +1,28 @@ +storage->get(InteractionsStorageInterface::CLIENT_DOMAIN, $id)->getRequest(); + $this->response = $this->httpClient->sendRequest($request, $this->server->getBaseUri()); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/compatibility-suite/tests/Service/ClientInterface.php b/compatibility-suite/tests/Service/ClientInterface.php new file mode 100644 index 00000000..75a7523b --- /dev/null +++ b/compatibility-suite/tests/Service/ClientInterface.php @@ -0,0 +1,12 @@ +pactPath = "$pactDir/$consumer-$provider.json"; + $this->config = new MockServerConfig(); + $this->config + ->setConsumer($consumer) + ->setProvider($provider) + ->setPactDir($pactDir) + ->setPactSpecificationVersion($specificationVersion) + ->setPactFileWriteMode(PactConfigInterface::MODE_OVERWRITE); + + $this->logger = new Logger(); + + $client = new Client(); + $pactRegistry = new PactRegistry($client); + $this->pactDriver = new PactDriver($client, $this->config, $pactRegistry); + $this->mockServer = new MockServer($client, $pactRegistry, $this->config, $this->logger); + $this->interactionRegistry = new InteractionRegistry($client, $pactRegistry); + } + + public function register(int ...$ids): void + { + $interactions = array_map(fn (int $id) => $this->storage->get(InteractionsStorageInterface::SERVER_DOMAIN, $id), $ids); + $this->pactDriver->setUp(); + foreach ($interactions as $interaction) { + $this->interactionRegistry->registerInteraction($interaction); + } + $this->mockServer->start(); + } + + public function getBaseUri(): UriInterface + { + return $this->config->getBaseUri(); + } + + public function verify(): void + { + $success = $this->mockServer->verify(); + $this->verifyResult = new VerifyResult($success, !$success ? $this->logger->getOutput() : ''); + } + + public function getVerifyResult(): VerifyResult + { + if (!isset($this->verifyResult)) { + $this->verify(); + } + + return $this->verifyResult; + } + + public function getPactPath(): string + { + return $this->pactPath; + } + + public function getPort(): int + { + return $this->config->getPort(); + } +} diff --git a/compatibility-suite/tests/Service/ServerInterface.php b/compatibility-suite/tests/Service/ServerInterface.php new file mode 100644 index 00000000..2015caf6 --- /dev/null +++ b/compatibility-suite/tests/Service/ServerInterface.php @@ -0,0 +1,21 @@ +