From c462b16d7ca74b60a63420008bd13b5b23c07e9e Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 14 Nov 2024 12:31:45 +0100 Subject: [PATCH] Added ability to restrict match count --- src/WiremockContext.php | 196 +++++++++++++++++++++---- tests/WiremockContextTest.php | 263 +++++++++++++++++++++++++++++++++- 2 files changed, 428 insertions(+), 31 deletions(-) diff --git a/src/WiremockContext.php b/src/WiremockContext.php index a5996b9..7329745 100644 --- a/src/WiremockContext.php +++ b/src/WiremockContext.php @@ -32,6 +32,11 @@ class WiremockContext implements Context private const BODY_RECORDINGS_START = '{"targetBaseUrl":"%s","requestBodyPattern":{"matcher":"equalToJson","ignoreArrayOrder":true,"ignoreExtraElements":true},"persist":true}'; + private const STUB_MATCH_COUNT_STRATEGY_EXACT = 'exact'; + private const STUB_MATCH_COUNT_STRATEGY_MAX = 'max'; + private const STUB_MATCH_COUNT_STRATEGY_MIN = 'min'; + private const STUB_MATCH_COUNT_STRATEGY_ANY = 'any'; + private HttpClientInterface $client; /** @@ -66,31 +71,34 @@ public function addWiremockStubStep(PyStringNode $body): void #[Given('/^wiremock stubs from "([^"]+)"$/')] public function addWiremockStubFromFileStep(string $path): void { - $absolutePath = $this->stubsDirectory . '/' . $path; + $this->addWiremockStubFromFile($path); + } - if (is_dir($absolutePath)) { - $files = scandir($absolutePath); + /** + * @throws WiremockContextException + */ + #[Given('/^wiremock stubs from "([^"]+)" and should be called exactly (?P\d+) times$/')] + public function addWiremockStubFromFileShouldBeCalledExactlyStep(string $path, int $expectedCallCount): void + { + $this->addWiremockStubFromFile($path, $expectedCallCount, self::STUB_MATCH_COUNT_STRATEGY_EXACT); + } - foreach ($files as $file) { - $filePath = $absolutePath . '/' . $file; - if (is_dir($filePath)) { - continue; - } + /** + * @throws WiremockContextException + */ + #[Given('/^wiremock stubs from "([^"]+)" and should be called minimal (?P\d+) times$/')] + public function addWiremockStubFromFileShouldBeCalledMinimalStep(string $path, int $expectedCallCount): void + { + $this->addWiremockStubFromFile($path, $expectedCallCount, self::STUB_MATCH_COUNT_STRATEGY_MIN); + } - try { - $this->loadStubFromFile($filePath); - } catch (Throwable $exception) { - throw new WiremockContextException( - sprintf( - 'Unable to load file "%s"', - $filePath - ) - , 0, $exception); - } - } - } else { - $this->loadStubFromFile($absolutePath); - } + /** + * @throws WiremockContextException + */ + #[Given('/^wiremock stubs from "([^"]+)" and should be called at most (?P\d+) times$/')] + public function addWiremockStubFromFileShouldBeCalledAtMostStep(string $path, int $expectedCallCount): void + { + $this->addWiremockStubFromFile($path, $expectedCallCount, self::STUB_MATCH_COUNT_STRATEGY_MAX); } #[Given('/^clean wiremock$/')] @@ -243,22 +251,32 @@ private function sendRequest(string $method, string $url, ?string $body = null): } } - public function addStub(string $body): void - { + public function addStub( + string $body, + ?int $expectedCallCount = null, + string $type = self::STUB_MATCH_COUNT_STRATEGY_ANY + ): void { $response = $this->sendRequest( 'POST', self::PATH_MAPPINGS, $body ); - $this->stubs[$response['id']] = $response; + $stubId = $response['id']; + + $this->stubs[$stubId]['response'] = $response; + $this->stubs[$stubId]['count'] = $expectedCallCount; + $this->stubs[$stubId]['type'] = $type; } - private function loadStubFromFile(string $filePath): void + private function loadStubFromFile(string $filePath, ?int $expectedCallCount, string $type): void { - $this->addStub(file_get_contents($filePath)); + $this->addStub(file_get_contents($filePath), $expectedCallCount, $type); } + /** + * @throws WiremockContextException + */ private function allStubsMatched(): void { $response = $this->sendRequest( @@ -285,7 +303,8 @@ private function allStubsMatched(): void )); } - $requestedStubsIds[] = $requestData['stubMapping']['id']; + $mappedRequestStubId = $requestData['stubMapping']['id']; + $requestedStubsIds[] = $mappedRequestStubId; } $requestedStubsIds = array_unique($requestedStubsIds); @@ -293,9 +312,128 @@ private function allStubsMatched(): void if ($diff = array_diff(array_keys($this->stubs), $requestedStubsIds)) { $unrequestedStubs = []; foreach ($diff as $stubId) { - $unrequestedStubs[] = $this->stubs[$stubId]; + $unrequestedStubs[] = $this->stubs[$stubId]['response']; } + throw new WiremockContextException('Unrequested stub(s) found: ' . json_encode($unrequestedStubs, JSON_PRETTY_PRINT)); } } + + /** + * @throws WiremockContextException + */ + #[AfterScenario] + public function allStubsMatchedAsExpectedForEachScenario(): void + { + $this->allStubsMatchedAsExpected(); + } + + /** + * @throws WiremockContextException + */ + private function addWiremockStubFromFile( + string $path, + ?int $expectedCallCount = null, + string $type = self::STUB_MATCH_COUNT_STRATEGY_ANY + ): void { + $absolutePath = $this->stubsDirectory . '/' . $path; + + if (is_dir($absolutePath)) { + $files = scandir($absolutePath); + + foreach ($files as $file) { + $filePath = $absolutePath . '/' . $file; + if (is_dir($filePath)) { + continue; + } + + try { + $this->loadStubFromFile($filePath, $expectedCallCount, $type); + } catch (Throwable $exception) { + throw new WiremockContextException( + sprintf( + 'Unable to load file "%s"', + $filePath + ) + , 0, $exception); + } + } + } else { + $this->loadStubFromFile($absolutePath, $expectedCallCount, $type); + } + } + + /** + * @param array $requestedStubsCallCounts + * @throws WiremockContextException + */ + private function allStubsMatchedAsExpected(): void + { + $response = $this->sendRequest( + 'GET', + self::PATH_REQUESTS + ); + + $requestedStubsCallCounts = []; + + foreach ($response['requests'] as $requestData) { + $mappedRequestStubId = $requestData['stubMapping']['id']; + + if (!isset($requestedStubsCallCounts[$mappedRequestStubId])) { + $requestedStubsCallCounts[$mappedRequestStubId] = 0; + } + + $requestedStubsCallCounts[$mappedRequestStubId]++; + } + + $errors = []; + + foreach ($requestedStubsCallCounts as $stubId => $actualCount) { + $expectedCount = $this->stubs[$stubId]['count']; + $expectedType = $this->stubs[$stubId]['type']; + + $url = $this->stubs[$stubId]['response']['request']['urlPath']; + + switch ($expectedType) { + case self::STUB_MATCH_COUNT_STRATEGY_EXACT: + if ($actualCount !== $expectedCount) { + $errors[] = sprintf( + 'Stub with URL "%s" was expected to be called exactly %d time(s), but was called %d time(s)', + $url, + $expectedCount, + $actualCount + ); + } + break; + case self::STUB_MATCH_COUNT_STRATEGY_MAX: + if ($actualCount > $expectedCount) { + $errors[] = sprintf( + 'Stub with URL "%s" was expected to be called at most %d time(s), but was called %d time(s)', + $url, + $expectedCount, + $actualCount + ); + } + break; + case self::STUB_MATCH_COUNT_STRATEGY_MIN: + if ($actualCount < $expectedCount) { + $errors[] = sprintf( + 'Stub with URL "%s" was expected to be called minimum %d time(s), but was called %d time(s)', + $url, + $expectedCount, + $actualCount + ); + } + break; + case self::STUB_MATCH_COUNT_STRATEGY_ANY: + break; + default: + throw new WiremockContextException(sprintf('Unknown expectation type %s for URL %s', $expectedType, $url)); + } + + if (!empty($errors)) { + throw new WiremockContextException(implode("\n", $errors)); + } + } + } } diff --git a/tests/WiremockContextTest.php b/tests/WiremockContextTest.php index 96b6e50..8832150 100644 --- a/tests/WiremockContextTest.php +++ b/tests/WiremockContextTest.php @@ -61,7 +61,9 @@ public function testAddStubWithHttpClientException(): void public function testAllStubsMatchedStepAfterAddingStub(): void { $addStubResponse = $this->createMock(ResponseInterface::class); - $addStubResponse->expects($this->once())->method('toArray')->willReturn(['id' => 'dummy-stub-id']); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); $matchedStubsResponse = $this->createMock(ResponseInterface::class); $matchedStubsResponse->expects($this->once())->method('toArray')->willReturn([ 'requests' => [ @@ -95,7 +97,9 @@ function ($method, $url, $options) use ($addStubResponse, $matchedStubsResponse) public function testAllStubsMatchedStepUnexpectedCall(): void { $addStubResponse = $this->createMock(ResponseInterface::class); - $addStubResponse->expects($this->once())->method('toArray')->willReturn(['id' => 'dummy-stub-id']); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); $matchedStubsResponse = $this->createMock(ResponseInterface::class); $matchedStubsResponse->expects($this->once())->method('toArray')->willReturn([ 'requests' => [ @@ -154,4 +158,259 @@ function ($method, $url, $options) use ($addStubResponse, $matchedStubsResponse) $this->expectExceptionMessage("Unrequested stub(s) found: [\n {\n \"id\": \"dummy-stub-id\"\n }\n]"); $this->wiremockContext->allStubsMatchedStep(); } + + public function testAllStubsMatchedStepWithExactMatchCountStrategyFailed() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 1, 'exact'); + $this->expectException(WiremockContextException::class); + $this->expectExceptionMessage('Stub with URL "/test" was expected to be called exactly 1 time(s), but was called 2 time(s)'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } + + public function testAllStubsMatchedStepWithExactMatchCountStrategy() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->expects($this->once())->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 1, 'exact'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } + + public function testAllStubsMatchedStepWithMaxMatchCountStrategyFailed() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 1, 'max'); + $this->expectException(WiremockContextException::class); + $this->expectExceptionMessage('Stub with URL "/test" was expected to be called at most 1 time(s), but was called 2 time(s)'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } + + public function testAllStubsMatchedStepWithMinMatchCountStrategyFailed() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 3, 'min'); + $this->expectException(WiremockContextException::class); + $this->expectExceptionMessage('Stub with URL "/test" was expected to be called minimum 3 time(s), but was called 2 time(s)'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } + + public function testAllStubsMatchedStepWithMaxMatchCountStrategy() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->expects($this->once())->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 1, 'max'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } + + public function testAllStubsMatchedStepWithMinMatchCountStrategy() + { + $addStubResponse = $this->createMock(ResponseInterface::class); + $addStubResponse->expects($this->once())->method('toArray')->willReturn( + ['id' => 'dummy-stub-id', 'request' => ['urlPath' => '/test']] + ); + $matchedStubsResponse = $this->createMock(ResponseInterface::class); + $matchedStubsResponse->expects($this->once())->method('toArray')->willReturn([ + 'requests' => [ + [ + 'stubMapping' => [ + 'id' => 'dummy-stub-id', + ], + 'request' => [ + 'method' => 'GET', + 'absoluteUrl' => 'http://wiremock:8080/test', + ], + ], + ] + ]); + $this->httpClient->method('request')->willReturnCallback( + function ($method, $url) use ($addStubResponse, $matchedStubsResponse) { + if ($method === 'POST' && $url === 'http://wiremock:8080/__admin/mappings') { + return $addStubResponse; + } elseif ($method === 'GET' && $url === 'http://wiremock:8080/__admin/requests') { + return $matchedStubsResponse; + } + + return null; + } + ); + + $stubBody = '{"request": {"method": "GET", "url": "/test"}, "response": {"status": 200, "body": "Success"}}'; + $this->wiremockContext->addStub($stubBody, 1, 'min'); + $this->wiremockContext->allStubsMatchedAsExpectedForEachScenario(); + } }