Skip to content

Commit

Permalink
Merge pull request #428 from tienvx/v1-consumer
Browse files Browse the repository at this point in the history
test(compatibility-suite): Implement V1 Consumer scenarios
  • Loading branch information
tienvx committed Dec 22, 2023
2 parents ec1452a + b8a5234 commit a6fca91
Show file tree
Hide file tree
Showing 8 changed files with 510 additions and 13 deletions.
32 changes: 32 additions & 0 deletions compatibility-suite/tests/Constant/Mismatch.php
@@ -0,0 +1,32 @@
<?php

namespace PhpPactTest\CompatibilitySuite\Constant;

class Mismatch
{
public const VERIFIER_MISMATCH_TYPE_MAP = [
'MethodMismatch' => 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',
];
}
35 changes: 25 additions & 10 deletions compatibility-suite/tests/Context/Shared/InteractionsContext.php
Expand Up @@ -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;
Expand All @@ -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);
}
}
}
295 changes: 295 additions & 0 deletions compatibility-suite/tests/Context/V1/Http/ConsumerContext.php
@@ -0,0 +1,295 @@
<?php

namespace PhpPactTest\CompatibilitySuite\Context\V1\Http;

use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use PhpPactTest\CompatibilitySuite\Constant\Mismatch;
use PhpPactTest\CompatibilitySuite\Service\ClientInterface;
use PhpPactTest\CompatibilitySuite\Service\FixtureLoaderInterface;
use PhpPactTest\CompatibilitySuite\Service\InteractionsStorageInterface;
use PhpPactTest\CompatibilitySuite\Service\RequestBuilderInterface;
use PhpPactTest\CompatibilitySuite\Service\ServerInterface;
use PHPUnit\Framework\Assert;

final class ConsumerContext implements Context
{
private array $pact;

public function __construct(
private ServerInterface $server,
private RequestBuilderInterface $requestBuilder,
private ClientInterface $client,
private InteractionsStorageInterface $storage,
private FixtureLoaderInterface $fixtureLoader,
) {
}

/**
* @When the mock server is started with interaction :id
*/
public function theMockServerIsStartedWithInteraction(int $id): void
{
$this->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);
}
}

0 comments on commit a6fca91

Please sign in to comment.