diff --git a/README.http-client-trait.md b/README.http-client-trait.md index fa37a97..8a764a5 100644 --- a/README.http-client-trait.md +++ b/README.http-client-trait.md @@ -7,14 +7,10 @@ To use the mock http client, configure the `MockRequestBuilderCollection` as `mo Example symfony config: ```yaml -# config/packages/http_client.yaml +# config/packages/mock_request_builder.yaml when@test: services: - Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection: - arguments: - - '@Brainbits\FunctionalTestHelpers\HttpClientMock\SymfonyMockResponseFactory' - - Brainbits\FunctionalTestHelpers\HttpClientMock\SymfonyMockResponseFactory: ~ + Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection: ~ ``` ```yaml diff --git a/TODO.md b/TODO.md index 82178ea..d6389f2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,2 @@ - Make firewall context configurable in RequestBuilder - Mark RequestBuilderFactory as Symfony specific -- Remove json()-deprecation from MockRequestBuilder -- Create better fail-description for missing MockRequestCollection in test case -- Make match immutable diff --git a/UPGRADE-7.0.md b/UPGRADE-7.0.md new file mode 100644 index 0000000..230a7b4 --- /dev/null +++ b/UPGRADE-7.0.md @@ -0,0 +1,7 @@ +UPGRADE FROM 6.x to 7.0 +======================= + +HttpClientMock +-------------- + + * Remove `SymfonyMockResponseFactory` definition and argument from `MockRequestBuilderCollection` in mock_request_builder.yaml. diff --git a/src/HttpClientMock/CallStack.php b/src/HttpClientMock/CallStack.php index 3a2dd43..96a8497 100644 --- a/src/HttpClientMock/CallStack.php +++ b/src/HttpClientMock/CallStack.php @@ -15,13 +15,13 @@ use const PHP_EOL; -/** @implements IteratorAggregate */ +/** @implements IteratorAggregate */ final class CallStack implements Countable, IteratorAggregate { - /** @var MockRequestBuilder[] */ + /** @var RealRequest[] */ private array $calls; - public function __construct(MockRequestBuilder ...$calls) + public function __construct(RealRequest ...$calls) { $this->calls = $calls; } @@ -33,7 +33,7 @@ public static function fromCallStacks(CallStack ...$callStacks): self return new self(...$requests); } - public function first(): MockRequestBuilder|null + public function first(): RealRequest|null { if (!count($this->calls)) { return null; @@ -52,7 +52,7 @@ public function count(): int return count($this->calls); } - /** @return Traversable|MockRequestBuilder[] */ + /** @return Traversable */ public function getIterator(): Traversable { yield from $this->calls; diff --git a/src/HttpClientMock/Compare.php b/src/HttpClientMock/Compare.php deleted file mode 100644 index 7457a14..0000000 --- a/src/HttpClientMock/Compare.php +++ /dev/null @@ -1,57 +0,0 @@ - $value) { - if (is_numeric($key)) { - $match = false; - foreach ($actual as $otherKey => $otherValue) { - if (is_numeric($otherKey) && $this($value, $otherValue)) { - $match = true; - break; - } - } - - if (!$match) { - return false; - } - } elseif (!array_key_exists($key, $actual) || !$this($actual[$key], $expected[$key])) { - return false; - } - } - - foreach ($actual as $key => $value) { - if (is_numeric($key)) { - $match = false; - foreach ($expected as $otherKey => $otherValue) { - if (is_numeric($otherKey) && $this($value, $otherValue)) { - $match = true; - break; - } - } - - if (!$match) { - return false; - } - } elseif (!array_key_exists($key, $expected) || !$this($actual[$key], $expected[$key])) { - return false; - } - } - - return true; - } -} diff --git a/src/HttpClientMock/Exception/NoMatchingMockRequest.php b/src/HttpClientMock/Exception/NoMatchingMockRequest.php index 01e7764..32d6431 100644 --- a/src/HttpClientMock/Exception/NoMatchingMockRequest.php +++ b/src/HttpClientMock/Exception/NoMatchingMockRequest.php @@ -4,25 +4,114 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock\Exception; -use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder; -use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatch; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Hit; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MatchResult; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Mismatch; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Missing; +use Brainbits\FunctionalTestHelpers\HttpClientMock\RealRequest; use RuntimeException; +use function array_map; +use function explode; +use function implode; use function sprintf; use const PHP_EOL; final class NoMatchingMockRequest extends RuntimeException implements HttpClientMockException { - /** @param MockRequestMatch[] $matches */ - public static function fromMockRequest(MockRequestBuilder $request, array $matches): self + public static function noBuilders(RealRequest $request): self + { + $message = sprintf('No mock request builders given for:%s%s%s', PHP_EOL, $request, PHP_EOL); + + return new self($message); + } + + /** @param MatchResult[] $matchResults */ + public static function fromResults(RealRequest $request, array $matchResults): self { $message = sprintf('No matching mock request builder found for:%s%s%s', PHP_EOL, $request, PHP_EOL); + $message .= sprintf('%sMock request builders:%s', PHP_EOL, PHP_EOL); + + $tick = '✔'; + $cross = '✘'; + + foreach ($matchResults as $key => $matchResult) { + $no = $key + 1; + $name = $matchResult->getName(); + + $message .= sprintf( + '#%s %s%s', + $no, + $name ?? '(unnamed)', + PHP_EOL, + ); + + foreach ($matchResult->getResults() as $result) { + if ($result instanceof Hit) { + if ($result->key) { + $line = sprintf( + '%s %s %s matches "%s"', + $tick, + $result->matcher, + $result->key, + $result->actual, + ); + } else { + $line = sprintf( + '%s %s matches "%s"', + $tick, + $result->matcher, + $result->actual, + ); + } + } elseif ($result instanceof Mismatch) { + if ($result->key) { + $line = sprintf( + '%s %s %s "%s" does not match "%s"', + $cross, + $result->matcher, + $result->key, + $result->actual ?? 'NULL', + $result->expected, + ); + } else { + $line = sprintf( + '%s %s "%s" does not match "%s"', + $cross, + $result->matcher, + $result->actual ?? 'NULL', + $result->expected, + ); + } + } elseif ($result instanceof Missing) { + $line = sprintf( + '%s %s %s missing', + $cross, + $result->matcher, + $result->key, + ); + } else { + continue; + } + + $message .= sprintf( + ' %s (%s)%s', + $line, + $result->score, + PHP_EOL, + ); - if ($matches) { - $message .= sprintf('%sReasons:%s', PHP_EOL, PHP_EOL); - foreach ($matches as $match) { - $message .= sprintf('- %s%s', $match->getReason(), PHP_EOL); + if ($result instanceof Mismatch && $result->diff) { // phpcs:ignore + $diff = implode( + PHP_EOL, + array_map( + static fn ($line) => ' ' . $line, + explode(PHP_EOL, $result->diff), + ), + ); + $message .= $diff . PHP_EOL; + } } } diff --git a/src/HttpClientMock/Exception/NoResponseMock.php b/src/HttpClientMock/Exception/NoResponseMock.php index 66ce997..dcb73a7 100644 --- a/src/HttpClientMock/Exception/NoResponseMock.php +++ b/src/HttpClientMock/Exception/NoResponseMock.php @@ -23,9 +23,15 @@ public static function allResponsesProcessed(): self return new self('All responses have already been processed'); } - public static function withRequest(self $decorated, MockRequestBuilder $request): self + public static function withRequest(self $decorated, MockRequestBuilder $requestBuilder): self { - $message = sprintf('%s for:%s%s%s', $decorated->getMessage(), PHP_EOL, $request, PHP_EOL); + $message = sprintf( + '%s for:%s%s%s', + $decorated->getMessage(), + PHP_EOL, + $requestBuilder, + PHP_EOL, + ); return new self($message, $decorated->getCode(), $decorated->getPrevious()); } diff --git a/src/HttpClientMock/Exception/NoUriConfigured.php b/src/HttpClientMock/Exception/NoUriConfigured.php deleted file mode 100644 index 172fa93..0000000 --- a/src/HttpClientMock/Exception/NoUriConfigured.php +++ /dev/null @@ -1,17 +0,0 @@ - $storage */ $storage = new ArrayObject(); $callbackHandlerFn = static function ($record) use (&$storage): void { @@ -46,13 +53,7 @@ protected function registerNoMatchingMockRequestAsserts( $storage['exception'] = $exception; }; - if (class_exists(LogRecord::class)) { - // Used when Monolog >= 3.0 is installed - $callbackHandler = new CallbackHandler($callbackHandlerFn); - } else { - // Used when Monolog < 3.0 is installed - $callbackHandler = new LegacyCallbackHandler($callbackHandlerFn); - } + $callbackHandler = new CallbackHandler($callbackHandlerFn); foreach ($loggers as $logger) { $logger->pushHandler($callbackHandler); @@ -111,7 +112,7 @@ static function (ConsoleTerminateEvent $event) use ($storage): void { protected function mockRequest(string|null $method = null, string|callable|null $uri = null): MockRequestBuilder // phpcs:ignore Generic.Files.LineLength.TooLong,SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter { - if (!self::getContainer()) { + if (!self::getContainer()) { // @phpstan-ignore-line static::fail(sprintf( 'A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', static::class, @@ -129,8 +130,39 @@ protected function mockRequest(string|null $method = null, string|callable|null assert($stack instanceof MockRequestBuilderCollection); $builder = (new MockRequestBuilder()) - ->method($method) - ->uri($uri); + ->method($method); + + // legacy start - remove in 8.0.0 + + if (is_string($uri) && str_contains($uri, '?')) { + trigger_deprecation( + 'functional-test-helpers', + '7.0.0', + 'Query parameters in uri is deprecated. Use %s instead', + 'queryParam()', + ); + $uriParts = parse_url($uri); + $uri = ($uriParts['scheme'] ?? false ? $uriParts['scheme'] . '://' : '') . + ($uriParts['host'] ?? false ? $uriParts['host'] : '') . + ($uriParts['path'] ?? ''); + + if ($uriParts['query'] ?? false) { + $queryParts = explode('&', $uriParts['query']); + $queryParts = array_map(static fn ($query) => explode('=', $query), $queryParts); + + foreach ($queryParts as $queryPart) { + if (!$queryPart[0]) { + continue; + } + + $builder->queryParam($queryPart[0], urldecode($queryPart[1] ?? '')); + } + } + } + + // legacy end - remove in 8.0.0 + + $builder->uri($uri); $stack->addMockRequestBuilder($builder); @@ -144,7 +176,7 @@ protected function mockResponse(): MockResponseBuilder protected function callStack(): CallStack { - if (!self::getContainer()) { + if (!self::getContainer()) { // @phpstan-ignore-line static::fail(sprintf( 'A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', static::class, @@ -268,7 +300,6 @@ protected static function assertRequestMockIsCalledWithQueryParameter( } } - /** @param mixed[] $expected */ protected static function assertRequestMockIsCalledWithContent( string $expected, MockRequestBuilder $actualRequest, @@ -286,7 +317,6 @@ protected static function assertRequestMockIsCalledWithContent( } } - /** @param mixed[] $expected */ protected static function assertRequestMockIsCalledWithFile( string $expectedKey, string $expectedFilename, @@ -321,17 +351,6 @@ protected static function assertRequestMockIsCalledWithFile( (string) $call, ), ); - - self::assertSame( - $expectedSize, - $multiparts[$expectedKey]['size'], - self::mockRequestMessage( - $message, - 'Request not called with expected file size "%s": %s', - $expectedSize, - (string) $call, - ), - ); } } @@ -425,7 +444,7 @@ protected static function assertRequestMockIsCalledNTimes( protected static function assertAllRequestMocksAreCalled(string $message = ''): void { - if (!self::getContainer()) { + if (!self::getContainer()) { // @phpstan-ignore-line static::fail(self::mockRequestMessage( $message, 'A client must be set to make assertions on it. Did you forget to call "%s::createClient()"?', diff --git a/src/HttpClientMock/LegacyCallbackHandler.php b/src/HttpClientMock/LegacyCallbackHandler.php deleted file mode 100644 index f9a281a..0000000 --- a/src/HttpClientMock/LegacyCallbackHandler.php +++ /dev/null @@ -1,48 +0,0 @@ -fn = $fn; - } - - public function clear(): void - { - } - - public function reset(): void - { - } - - /** @param mixed[] $record */ - protected function write(array $record): void - { - ($this->fn)($record); - } - } -} diff --git a/src/HttpClientMock/Matcher/CatchAllMatcher.php b/src/HttpClientMock/Matcher/CatchAllMatcher.php new file mode 100644 index 0000000..fbfa3a6 --- /dev/null +++ b/src/HttpClientMock/Matcher/CatchAllMatcher.php @@ -0,0 +1,20 @@ +content = $content; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + $expectedContent = $this->content; + $realContent = $realRequest->getContent(); + + if (is_callable($expectedContent)) { + if ($expectedContent($realContent) === false) { + return Mismatch::mismatchingContent('', $realContent); + } + } elseif ($expectedContent !== $realContent) { + return Mismatch::mismatchingContent($expectedContent, $realContent); + } + + return Hit::matchesContent($realContent); + } + + public function __toString(): string + { + return is_callable($this->content) + ? 'callback(request.content) !== false' + : sprintf('request.content === "%s"', $this->content); + } +} diff --git a/src/HttpClientMock/Matcher/HeaderMatcher.php b/src/HttpClientMock/Matcher/HeaderMatcher.php new file mode 100644 index 0000000..53c3429 --- /dev/null +++ b/src/HttpClientMock/Matcher/HeaderMatcher.php @@ -0,0 +1,41 @@ +header = strtolower($header); + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + if (!$realRequest->hasHeader($this->header)) { + return Missing::missingHeader($this->header, $this->value); + } + + $expectedValue = $this->value; + $realValue = $realRequest->getHeader($this->header); + + if ($expectedValue !== $realValue) { + return Mismatch::mismatchingHeader($this->header, $expectedValue, $realValue); + } + + return Hit::matchesHeader($this->header, $realValue); + } + + public function __toString(): string + { + return sprintf('request.header["%s"] === "%s"', $this->value, $this->header); + } +} diff --git a/src/HttpClientMock/Matcher/Hit.php b/src/HttpClientMock/Matcher/Hit.php new file mode 100644 index 0000000..3a018c4 --- /dev/null +++ b/src/HttpClientMock/Matcher/Hit.php @@ -0,0 +1,72 @@ +json = $json; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + $expectedJson = $this->json; + $realJson = $realRequest->getJson(); + + if (is_callable($expectedJson)) { + if ($expectedJson($realJson) === false) { + return Mismatch::mismatchingJsonCallback('', json_encode($realJson)); + } + } else { + $expectedValue = Json::canonicalize(json_encode($expectedJson)); + $realValue = $realJson ? Json::canonicalize(json_encode($realJson)) : null; + + if ($expectedValue !== $realValue) { + return Mismatch::mismatchingJson( + json_encode($expectedJson), + $realJson ? json_encode($realJson) : null, + ); + } + } + + return Hit::matchesJson(json_encode($realJson)); + } + + public function __toString(): string + { + return is_callable($this->json) + ? 'callback(request.content) !== false' + : sprintf('request.content === "%s"', json_encode($this->json)); + } +} diff --git a/src/HttpClientMock/Matcher/MatchResult.php b/src/HttpClientMock/Matcher/MatchResult.php new file mode 100644 index 0000000..01a6ae5 --- /dev/null +++ b/src/HttpClientMock/Matcher/MatchResult.php @@ -0,0 +1,75 @@ + $results */ + private function __construct(private string|null $name, private array $results) + { + } + + public static function create(string|null $name): self + { + return new self($name, []); + } + + public function getName(): string|null + { + return $this->name; + } + + /** @return list */ + public function getResults(): array + { + return $this->results; + } + + public function getScore(): int + { + if (!count($this->results)) { + return 0; + } + + $score = 0; + foreach ($this->results as $result) { + if ($result instanceof Mismatch || $result instanceof Missing) { + return 0; + } + + $score += $result->score; + } + + return $score; + } + + public function isEmpty(): bool + { + return !count($this->results); + } + + public function isMismatch(): bool + { + if ($this->isEmpty()) { + return false; + } + + foreach ($this->results as $result) { + if ($result instanceof Mismatch || $result instanceof Missing) { + return true; + } + } + + return false; + } + + public function withResult(Hit|Mismatch|Missing $hit): static + { + return new self($this->name, array_merge($this->results, [$hit])); + } +} diff --git a/src/HttpClientMock/Matcher/Matcher.php b/src/HttpClientMock/Matcher/Matcher.php new file mode 100644 index 0000000..9ad0b8e --- /dev/null +++ b/src/HttpClientMock/Matcher/Matcher.php @@ -0,0 +1,14 @@ +method = $method; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + $expectedMethod = $this->method; + $realMethod = $realRequest->getMethod(); + + if (is_callable($expectedMethod)) { + if ($expectedMethod($realMethod) === false) { + return Mismatch::mismatchingMethod('', $realMethod); + } + } elseif ($expectedMethod !== $realMethod) { + return Mismatch::mismatchingMethod($expectedMethod, $realMethod); + } + + return Hit::matchesMethod($realMethod); + } + + public function __toString(): string + { + return is_callable($this->method) + ? 'callback(request.method) !== false' + : sprintf('request.method === "%s"', $this->method); + } +} diff --git a/src/HttpClientMock/Matcher/Mismatch.php b/src/HttpClientMock/Matcher/Mismatch.php new file mode 100644 index 0000000..1964b37 --- /dev/null +++ b/src/HttpClientMock/Matcher/Mismatch.php @@ -0,0 +1,153 @@ +score = 0; + } + + public static function mismatchingMethod(string $method, string|null $otherMethod): self + { + return new self( + 'method', + null, + $method, + $otherMethod, + ); + } + + public static function mismatchingUri(string $uri, string|null $otherUri): self + { + return new self( + 'uri', + null, + $uri, + $otherUri, + ); + } + + public static function mismatchingContent(string $content, string|null $otherContent): self + { + return new self( + 'content', + null, + $content, + $otherContent, + ); + } + + public static function mismatchingJsonCallback(string $content, string|null $otherContent): self + { + return new self( + 'json', + null, + $content, + $otherContent, + ); + } + + public static function mismatchingJson(string $content, string|null $otherContent): self + { + $diff = null; + if ($otherContent) { + $differ = new Differ( + new UnifiedDiffOutputBuilder( + '--- Expected' . PHP_EOL . '+++ Actual' . PHP_EOL, + ), + ); + $diff = $differ->diff(Json::prettify($content), Json::prettify($otherContent)); + } + + return new self( + 'json', + null, + $content, + $otherContent, + $diff, + ); + } + + public static function mismatchingXml(string $content, string|null $otherContent): self + { + return new self( + 'xml', + null, + $content, + $otherContent, + ); + } + + public static function mismatchingRequestParam(string $key, string $content, string|null $otherContent): self + { + return new self( + 'requestParam', + $key, + $content, + $otherContent, + ); + } + + /** + * @param mixed[] $multipart + * @param mixed[] $otherMultipart + */ + public static function mismatchingMultipart(string $name, array $multipart, array|null $otherMultipart): self + { + return new self( + 'multipart', + $name, + json_encode($multipart), + json_encode($otherMultipart), + ); + } + + public static function mismatchingHeader(string $key, mixed $value, mixed $otherValue): self + { + return new self( + 'header', + $key, + $value, + $otherValue, + ); + } + + public static function mismatchingQueryParam(string $key, string $value, string $otherValue): self + { + return new self( + 'queryParam', + $key, + $value, + $otherValue, + ); + } + + public static function mismatchingThat(string $reason): self + { + return new self( + 'that', + null, + '', + $reason, + ); + } +} diff --git a/src/HttpClientMock/Matcher/Missing.php b/src/HttpClientMock/Matcher/Missing.php new file mode 100644 index 0000000..31dd299 --- /dev/null +++ b/src/HttpClientMock/Matcher/Missing.php @@ -0,0 +1,51 @@ +score = 0; + } + + public static function missingHeader(string $key, string $expected): self + { + return new self( + 'header', + $key, + $expected, + ); + } + + public static function missingQueryParam(string $key, string $expected): self + { + return new self( + 'queryParam', + $key, + $expected, + ); + } + + public static function missingRequestParam(string $key, string $expected): self + { + return new self( + 'requestParam', + $key, + $expected, + ); + } + + public static function missingMultipart(string $name, string $expected): self + { + return new self( + 'multipart', + $name, + $expected, + ); + } +} diff --git a/src/HttpClientMock/Matcher/MultipartMatcher.php b/src/HttpClientMock/Matcher/MultipartMatcher.php new file mode 100644 index 0000000..5e5cf0d --- /dev/null +++ b/src/HttpClientMock/Matcher/MultipartMatcher.php @@ -0,0 +1,107 @@ +hasMultipart($this->name)) { + return Missing::missingMultipart($this->name, json_encode($this->createExpectedMultipartArray())); + } + + $expectedMultipart = $this->createExpectedMultipartArray(); + $reducedMultipart = $this->reduceMultipart($expectedMultipart, $realRequest->getMultipart($this->name)); + + if (count(array_diff($expectedMultipart, $reducedMultipart))) { + return Mismatch::mismatchingMultipart($this->name, $expectedMultipart, $reducedMultipart); + } + + return Hit::matchesMultipart($reducedMultipart); + } + + /** @return array{name: string, mimetype?: string, filename?: string, content?: string} */ + private function createExpectedMultipartArray(): array + { + $expectedMultiparts = ['name' => $this->name]; + + if ($this->mimetype) { + $expectedMultiparts['mimetype'] = $this->mimetype; + } + + if ($this->filename) { + $expectedMultiparts['filename'] = $this->filename; + } + + if ($this->content) { + $expectedMultiparts['content'] = $this->content; + } + + return $expectedMultiparts; + } + + /** + * phpcs:disable Generic.Files.LineLength.TooLong + * + * @param array{name: string, mimetype?: string|null, filename?: string|null, content?: string|null} $expectedMultipart + * @param array{name: string, mimetype?: string|null, filename?: string|null, content?: string|null} $realMultipart + * + * @return array{name: string, mimetype?: string|null, filename?: string|null, content?: string|null} + * + * phpcs:enable Generic.Files.LineLength.TooLong + */ + private function reduceMultipart(array $expectedMultipart, array $realMultipart): array + { + $reducedMultipart = []; + foreach ($expectedMultipart as $key => $value) { + $reducedMultipart[$key] = $value !== null && ($realMultipart[$key] ?? false) ? $realMultipart[$key] : null; + } + + return $reducedMultipart; + } + + public function __toString(): string + { + $parts = []; + + if ($this->filename) { + $parts[] = 'filename=' . $this->filename; + } + + if ($this->mimetype) { + $parts[] = 'mimetype=' . $this->mimetype; + } + + if ($this->content) { + $parts[] = 'content=' . $this->content; + } + + if ($parts) { + return sprintf( + '[%s] === request.request[%s]', + implode(', ', $parts), + $this->name, + ); + } + + return sprintf('request.request[%s] is set', $this->name); + } +} diff --git a/src/HttpClientMock/Matcher/QueryParamMatcher.php b/src/HttpClientMock/Matcher/QueryParamMatcher.php new file mode 100644 index 0000000..af591d4 --- /dev/null +++ b/src/HttpClientMock/Matcher/QueryParamMatcher.php @@ -0,0 +1,41 @@ + $placeholders */ + public function __construct(private string $key, string $value, array $placeholders) + { + $this->value = sprintf($value, ...$placeholders); + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + if (!$realRequest->hasQueryParam($this->key)) { + return Missing::missingQueryParam($this->key, $this->value); + } + + $expectedValue = $this->value; + $realValue = $realRequest->getQueryParam($this->key); + + if ($expectedValue !== $realValue) { + return Mismatch::mismatchingQueryParam($this->key, $expectedValue, $realValue); + } + + return Hit::matchesQueryParam($this->key, $realValue); + } + + public function __toString(): string + { + return sprintf('request.queryParams["%s"] === "%s"', $this->key, $this->value); + } +} diff --git a/src/HttpClientMock/Matcher/RequestParamMatcher.php b/src/HttpClientMock/Matcher/RequestParamMatcher.php new file mode 100644 index 0000000..e54388a --- /dev/null +++ b/src/HttpClientMock/Matcher/RequestParamMatcher.php @@ -0,0 +1,37 @@ +hasRequestParam($this->key)) { + return Missing::missingRequestParam($this->key, $this->value); + } + + $expectedValue = $this->value; + $realValue = $realRequest->getRequestParam($this->key); + + if ($expectedValue !== $realValue) { + return Mismatch::mismatchingRequestParam($this->key, $expectedValue, $realValue); + } + + return Hit::matchesRequestParam($this->key, $realValue); + } + + public function __toString(): string + { + return sprintf('request.requestParams["%s"] === "%s"', $this->key, $this->value); + } +} diff --git a/src/HttpClientMock/Matcher/ThatMatcher.php b/src/HttpClientMock/Matcher/ThatMatcher.php new file mode 100644 index 0000000..f9b062d --- /dev/null +++ b/src/HttpClientMock/Matcher/ThatMatcher.php @@ -0,0 +1,31 @@ +that = $that; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + if (($this->that)($realRequest) === false) { + return Mismatch::mismatchingThat('returned false'); + } + + return Hit::matchesThat(); + } + + public function __toString(): string + { + return 'callback(request)'; + } +} diff --git a/src/HttpClientMock/Matcher/UriMatcher.php b/src/HttpClientMock/Matcher/UriMatcher.php new file mode 100644 index 0000000..c63978e --- /dev/null +++ b/src/HttpClientMock/Matcher/UriMatcher.php @@ -0,0 +1,56 @@ +uri = $uri; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + $realUri = $realRequest->getUri(); + + if (is_callable($this->uri)) { + if (($this->uri)($realUri, $this->uriParams->toArray()) === false) { + $params = $this->uriParams->toJson(); + + return Mismatch::mismatchingUri('', $realUri); + } + + return Hit::matchesUri($realUri); + } + + $expectedUri = $this->uriParams->replace($this->uri); + + if ($expectedUri !== $realUri) { + return Mismatch::mismatchingUri($expectedUri, $realUri); + } + + return Hit::matchesUri($realUri); + } + + public function __toString(): string + { + return is_callable($this->uri) + ? 'callback(request.uri, ' . $this->uriParams->toJson() . ') !== false' + : sprintf('request.uri === "%s"', $this->uriParams->replace($this->uri)); + } +} diff --git a/src/HttpClientMock/Matcher/UriParams.php b/src/HttpClientMock/Matcher/UriParams.php new file mode 100644 index 0000000..46e84ba --- /dev/null +++ b/src/HttpClientMock/Matcher/UriParams.php @@ -0,0 +1,63 @@ + $params */ + public function __construct(private array $params = []) + { + } + + public function count(): int + { + return count($this->params); + } + + public function set(string $key, string $value): void + { + $this->params[$key] = $value; + } + + public function has(string $key): bool + { + return (bool) ($this->params[$key] ?? false); + } + + public function get(string $key): string|null + { + return $this->params[$key] ?? null; + } + + public function replace(string $uri): string + { + $keys = array_keys($this->params); + $values = array_values($this->params); + $placeholders = array_map(static fn ($key) => sprintf('{%s}', $key), $keys); + + return str_replace($placeholders, $values, $uri); + } + + /** @return array */ + public function toArray(): array + { + return $this->params; + } + + public function toJson(): string + { + return $this->params ? json_encode($this->params) : '{}'; + } +} diff --git a/src/HttpClientMock/Matcher/XmlMatcher.php b/src/HttpClientMock/Matcher/XmlMatcher.php new file mode 100644 index 0000000..92fba97 --- /dev/null +++ b/src/HttpClientMock/Matcher/XmlMatcher.php @@ -0,0 +1,91 @@ +isXmlString($xml)) { + throw InvalidMockRequest::notXml($xml); + } + + $this->xml = $xml; + } + + public function __invoke(RealRequest $realRequest): Hit|Mismatch|Missing + { + $expectedXml = $this->xml; + $realXml = $realRequest->getContent(); + + if (is_callable($expectedXml)) { + if (!$this->isXmlString($realXml)) { + return Mismatch::mismatchingXml('', $realXml); + } + + if ($expectedXml($realXml) === false) { + return Mismatch::mismatchingXml('', $realXml); + } + } else { + if (!$realXml || !$this->isXmlString($realXml)) { + return Mismatch::mismatchingXml($expectedXml, $realXml); + } + + $expectedDom = new DOMDocument(); + $expectedDom->preserveWhiteSpace = false; + $expectedDom->formatOutput = true; + $expectedDom->loadXML($expectedXml); + + $realDom = new DOMDocument(); + $realDom->preserveWhiteSpace = false; + $realDom->formatOutput = true; + $realDom->loadXML($realXml); + + if ($expectedDom->saveXML() !== $realDom->saveXML()) { + return Mismatch::mismatchingXml($expectedXml, $realXml); + } + } + + return Hit::matchesXml($realXml); + } + + public function __toString(): string + { + return is_callable($this->xml) + ? 'callback(request.content) !== false' + : sprintf('request.content === "%s"', $this->xml); + } + + private function isXmlString(string $data): bool + { + $document = new DOMDocument(); + $internal = libxml_use_internal_errors(true); + $reporting = error_reporting(0); + + try { + $document->loadXML($data); + + $errors = libxml_get_errors(); + } finally { + libxml_use_internal_errors($internal); + error_reporting($reporting); + } + + return count($errors) === 0; + } +} diff --git a/src/HttpClientMock/MockRequestBuilder.php b/src/HttpClientMock/MockRequestBuilder.php index ba1f9ee..9f6730c 100644 --- a/src/HttpClientMock/MockRequestBuilder.php +++ b/src/HttpClientMock/MockRequestBuilder.php @@ -5,142 +5,115 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\AddMockResponseFailed; -use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\InvalidMockRequest; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\NoResponseMock; -use DOMDocument; -use Safe\Exceptions\JsonException; -use SimpleXMLElement; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\CatchAllMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\ContentMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\HeaderMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\JsonMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Matcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MethodMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MultipartMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\QueryParamMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\RequestParamMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\ThatMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\UriMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\UriParams; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\XmlMatcher; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\File\File; use Throwable; -use function array_key_exists; -use function array_keys; -use function array_map; -use function array_values; use function base64_encode; use function count; -use function error_reporting; -use function explode; +use function implode; use function is_array; -use function is_callable; -use function is_string; -use function libxml_get_errors; -use function libxml_use_internal_errors; -use function Safe\json_decode; -use function Safe\json_encode; -use function Safe\preg_match; -use function Safe\simplexml_load_string; use function sprintf; -use function str_contains; -use function str_repeat; -use function str_replace; -use function strpos; use function strtolower; -use function substr; use function trim; -use function ucwords; -use function urldecode; -use function urlencode; - -use const PHP_EOL; final class MockRequestBuilder { - private string|null $method = null; - - /** @var string|callable|null */ - private mixed $uri = null; - - /** @var array */ - private array $uriParams = []; - - /** @var mixed[]|null */ - private array|null $headers = null; - - /** @var mixed[]|null */ - private array|null $queryParams = null; + public string|null $name = null; - private string|null $content = null; - - /** @var array|null */ - private array|null $multiparts = null; + private MockResponseCollection $responses; - /** @var callable(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): ?string|null */ - private mixed $that = null; + /** @var Matcher[]|array */ + private array $matchers = []; - private MockResponseCollection $responses; + /** @var array */ + private array $assertions = []; - /** @var self[] */ + /** @var RealRequest[] */ private array $calls = []; + private UriParams $uriParams; + /** @var callable|null */ public mixed $onMatch = null; public function __construct() { $this->responses = new MockResponseCollection(); + $this->uriParams = new UriParams(); } - public function method(string|null $method): self + public function getMatcher(): MockRequestMatcher { - $this->method = $method; + $matchers = []; + foreach ($this->matchers as $matcher) { + if (is_array($matcher)) { + foreach ($matcher as $nestedMatcher) { + $matchers[] = $nestedMatcher; + } + } else { + $matchers[] = $matcher; + } + } - return $this; + if (!count($this->matchers)) { + $matchers[] = new CatchAllMatcher(); + } + + return new MockRequestMatcher($this->name, $matchers); } - public function uri(string|callable|null $uri): self // phpcs:ignore Generic.Files.LineLength.TooLong,SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter + public function getResponse(RealRequest $realRequest): MockResponse { - if ($uri === null) { - $this->uri = null; - - return $this; - } + $responseBuilder = $this->nextResponse(); - if (is_callable($uri)) { - $this->uri = $uri; - - return $this; + if ($responseBuilder instanceof Throwable) { + throw $responseBuilder; } - $queryParamStart = strpos($uri, '?'); + return $responseBuilder->getResponse($realRequest); + } - if ($queryParamStart === false) { - $this->uri = $uri; - } else { - $this->uri = substr($uri, 0, $queryParamStart); - $this->applyEncodedQueryParams(substr($uri, $queryParamStart + 1)); - } + public function name(string $name): self + { + $this->name = $name; return $this; } - public function header(string $key, string $value): self + public function method(string|callable $method): self { - $this->headers ??= []; - $this->headers[strtolower($key)] = $value; + $this->matchers['method'] = new MethodMatcher($method); return $this; } - public function hasHeaders(): bool + public function uri(string|callable $uri): self { - return $this->headers !== null; - } + $this->matchers['uri'] = new UriMatcher($uri, $this->uriParams); - /** @return mixed[]|null */ - public function getHeaders(): array|null - { - return $this->headers; + return $this; } - public function hasHeader(string $key): bool + public function header(string $key, string $value): self { - return array_key_exists($key, $this->headers); - } + $this->matchers['headers'][strtolower($key)] = new HeaderMatcher($key, $value); - public function getHeader(string $key): mixed - { - return $this->headers[strtolower($key)] ?? null; + return $this; } public function basicAuthentication(string $username, string $password): self @@ -150,153 +123,102 @@ public function basicAuthentication(string $username, string $password): self return $this->header('Authorization', sprintf('Basic %s', $token)); } - public function content(string $content): self + public function content(string|callable $content): self { - $this->content = $content; + $this->matchers['content'] = new ContentMatcher($content); return $this; } - public function getContent(): string|null - { - return $this->content; - } - - public function hasContent(): bool + /** @param callable(string $method, self $requestBuilder): void $assert */ + public function assertMethod(callable $assert): self { - return $this->content !== null; - } - - /** @param mixed[] $data */ - public function json(array $data): self - { - $this->content(json_encode($data)); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->assertions[] = static fn (RealRequest $realRequest, self $requestBuilder) => $assert($realRequest->getMethod(), $requestBuilder); return $this; } - public function isJson(): bool + /** @param callable(string $uri, self $requestBuilder): void $assert */ + public function assertUri(callable $assert): self { - if (!$this->hasContent()) { - return false; - } - - try { - json_decode($this->content, true); - } catch (JsonException) { - return false; - } + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->assertions[] = static fn (RealRequest $realRequest, self $requestBuilder) => $assert($realRequest->getUri(), $requestBuilder); - return true; + return $this; } - /** @return mixed[] */ - public function getJson(): array|null + /** @param callable(string $content, self $requestBuilder): void $assert */ + public function assertContent(callable $assert): self { - if (!$this->isJson()) { - return null; - } + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->assertions[] = static fn (RealRequest $realRequest, self $requestBuilder) => $assert($realRequest->getContent(), $requestBuilder); - return json_decode($this->content, true); + return $this; } - public function xml(string $data): self + /** @param callable(RealRequest $realRequest, self $requestBuilder): void $assert */ + public function assertThat(callable $assert): self { - if (!$this->isXmlString($data)) { - throw InvalidMockRequest::notXml($data); - } - - $this->content($data); + $this->assertions[] = $assert; return $this; } - public function isXml(): bool + public function assert(RealRequest $realRequest): void { - if (!$this->hasContent()) { - return false; + foreach ($this->assertions as $assertion) { + $assertion($realRequest, $this); } - - return $this->isXmlString($this->getContent()); } - /** @param array $namespaces */ - public function getXml(array $namespaces = []): SimpleXMLElement|null + /** @param mixed[]|callable $data */ + public function json(array|callable $data): self { - if (!$this->isXml()) { - return null; - } - - $xml = simplexml_load_string($this->content); + $this->matchers['json'] = new JsonMatcher($data); - foreach ($namespaces as $prefix => $namespace) { - $xml->registerXPathNamespace($prefix, $namespace); - } - - return $xml; + return $this; } - public function queryParam(string $key, string $value, string ...$placeholders): self + public function xml(string|callable $xml): self { - $this->queryParams ??= []; - $this->queryParams[$key] = sprintf($value, ...$placeholders); + $this->matchers['xml'] = new XmlMatcher($xml); return $this; } - /** @return string[] */ - public function getQueryParams(): array|null + public function queryParam(string $key, string $value, string ...$placeholders): self { - return $this->queryParams; - } + $this->matchers['queryParams'] ??= []; + $this->matchers['queryParams'][$key] = new QueryParamMatcher($key, $value, $placeholders); - public function hasQueryParams(): bool - { - return $this->queryParams !== null; + return $this; } public function requestParam(string $key, string $value): self { + $this->matchers['requestParams'] ??= []; + $this->matchers['requestParams'][$key] = new RequestParamMatcher($key, $value); + + /* if ((string) $this->content !== '') { $this->content .= '&'; } $this->content .= sprintf('%s=%s', urlencode($key), urlencode($value)); + */ return $this; } - /** @return string[] */ - public function getRequestParams(): array - { - return $this->parseEncodedParams((string) $this->content); - } - - public function hasRequestParams(): bool - { - return (bool) preg_match('/[^=]+=[^=]*(&[^=]+=[^=]*)*/', (string) $this->content) && !$this->isJson(); - } - public function multipart( string $name, string|null $mimetype = null, string|null $filename = null, string|null $content = null, ): self { - $this->multiparts ??= []; - $this->multiparts[$name] = []; - - if ($mimetype !== null) { - $this->multiparts[$name]['mimetype'] = $mimetype; - } - - if ($filename !== null) { - $this->multiparts[$name]['filename'] = $filename; - } - - if ($content !== null) { - $this->multiparts[$name]['content'] = $content; - } + $this->matchers['multiparts'] ??= []; + $this->matchers['multiparts'][$name] = new MultipartMatcher($name, $mimetype, $filename, $content); return $this; } @@ -313,70 +235,19 @@ public function multipartFromFile(string $name, File $file, string|null $mimetyp return $this; } - /** @return array|null */ - public function getMultiparts(): array|null - { - return $this->multiparts; - } - - public function hasMultiparts(): bool - { - return is_array($this->multiparts) && count($this->multiparts) > 0; - } - - public function uriParam(string $key, mixed $value): self - { - $this->uriParams[$key] = (string) $value; - - return $this; - } - - public function getMethod(): string|null - { - return $this->method; - } - - public function hasUri(): bool - { - return $this->uri !== null; - } - - public function getUri(): string|callable|null - { - if (is_string($this->uri)) { - return $this->replaceUriParams($this->uri, $this->uriParams); - } - - return $this->uri; - } - - public function hasUriParams(): bool - { - return (bool) $this->uriParams; - } - - /** @return array */ - public function getUriParams(): array - { - return $this->uriParams; - } - - /** @param callable(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): ?string $that */ + /** @param callable(RealRequest $realRequest): ?bool $that */ public function that(callable $that): self { - $this->that = $that; + $this->matchers['that'] = new ThatMatcher($that); return $this; } - public function hasThat(): bool + public function uriParam(string $key, string $value): self { - return $this->that !== null; - } + $this->uriParams->set($key, $value); - public function getThat(): callable|null - { - return $this->that; + return $this; } public function onMatch(callable $fn): self @@ -458,39 +329,52 @@ public function resetResponses(): self public function __toString(): string { - $string = ''; + $parts = []; - if ($this->method) { - $string .= $this->method . ' '; + if ($this->matchers['method'] ?? false) { + $parts[] = $this->matchers['method']; } - if ($this->uri) { - if (is_callable($this->uri)) { - $string .= ' '; - } else { - $string .= $this->uri . ' '; + if ($this->matchers['uri'] ?? false) { + $parts[] = $this->matchers['uri']; + } + + if ($this->matchers['headers'] ?? false) { + foreach ($this->matchers['headers'] as $header) { + $parts[] = (string) $header; } } - if ($this->headers) { - foreach ($this->headers as $key => $value) { - $key = str_replace('-', ' ', $key); - $key = ucwords($key); - $key = str_replace(' ', '-', $key); + if ($this->matchers['queryParams'] ?? false) { + foreach ($this->matchers['queryParams'] as $queryParam) { + $parts[] = (string) $queryParam; + } + } - $string .= sprintf('%s%s: %s', PHP_EOL, $key, $value); + if ($this->matchers['requestParams'] ?? false) { + foreach ($this->matchers['requestParams'] as $requestParam) { + $parts[] = (string) $requestParam; } } - if ($this->hasContent()) { - $string .= ($string ? str_repeat(PHP_EOL, 2) : ''); - $string .= $this->content; + if ($this->matchers['multiparts'] ?? false) { + foreach ($this->matchers['multiparts'] as $multipart) { + $parts[] = (string) $multipart; + } } - return trim($string); + if ($this->matchers['json'] ?? false) { + $parts[] = (string) $this->matchers['json']; + } elseif ($this->matchers['xml'] ?? false) { + $parts[] = (string) $this->matchers['xml']; + } elseif ($this->matchers['content'] ?? false) { + $parts[] = (string) $this->matchers['content']; + } + + return trim(implode(' && ', $parts)); } - public function called(self $request): self + public function called(RealRequest $request): self { $this->calls[] = $request; @@ -504,68 +388,6 @@ public function getCallStack(): CallStack public function isEmpty(): bool { - return $this->method === null && - $this->uri === null && - $this->headers === null && - $this->content === null && - $this->multiparts === null; - } - - /** @param array $uriParams */ - private static function replaceUriParams(string $uri, array $uriParams): mixed - { - $keys = array_keys($uriParams); - $values = array_values($uriParams); - $placeholders = array_map(static fn ($key) => sprintf('{%s}', $key), $keys); - - return str_replace($placeholders, $values, $uri); - } - - private function applyEncodedQueryParams(string $encodedParams): void - { - foreach ($this->parseEncodedParams($encodedParams) as $key => $value) { - $this->queryParam($key, $value); - } - } - - /** @return string[] */ - private function parseEncodedParams(string $encodedParams): array - { - if ($encodedParams === '') { - return []; - } - - $params = []; - - foreach (explode('&', $encodedParams) as $keyValue) { - if (str_contains($keyValue, '=')) { - [$key, $value] = explode('=', (string) $keyValue); - } else { - $key = $keyValue; - $value = ''; - } - - $params[urldecode((string) $key)] = urldecode((string) $value); - } - - return $params; - } - - private function isXmlString(string $data): bool - { - $document = new DOMDocument(); - $internal = libxml_use_internal_errors(true); - $reporting = error_reporting(0); - - try { - $document->loadXML($data); - - $errors = libxml_get_errors(); - } finally { - libxml_use_internal_errors($internal); - error_reporting($reporting); - } - - return count($errors) === 0; + return !$this->matchers; } } diff --git a/src/HttpClientMock/MockRequestBuilderCollection.php b/src/HttpClientMock/MockRequestBuilderCollection.php index e0ea585..a539d5c 100644 --- a/src/HttpClientMock/MockRequestBuilderCollection.php +++ b/src/HttpClientMock/MockRequestBuilderCollection.php @@ -4,24 +4,26 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock; +use Countable; use IteratorAggregate; use Symfony\Contracts\HttpClient\ResponseInterface; use Traversable; use function array_map; +use function count; use function is_callable; /** @implements IteratorAggregate */ -final class MockRequestBuilderCollection implements IteratorAggregate +final class MockRequestBuilderCollection implements IteratorAggregate, Countable { - private MockRequestBuilderFactory $requestFactory; + private RealRequestFactory $requestFactory; private MockRequestResolver $requestResolver; /** @var MockRequestBuilder[] */ private array $requestBuilders = []; - public function __construct(private MockResponseFactory $responseFactory) + public function __construct() { - $this->requestFactory = new MockRequestBuilderFactory(); + $this->requestFactory = new RealRequestFactory(); $this->requestResolver = new MockRequestResolver(); } @@ -31,13 +33,14 @@ public function __invoke(string $method, string $url, array $options): ResponseI $realRequest = ($this->requestFactory)($method, $url, $options); $requestBuilder = ($this->requestResolver)($this, $realRequest); + $requestBuilder->assert($realRequest); $requestBuilder->called($realRequest); if ($requestBuilder->onMatch && is_callable($requestBuilder->onMatch)) { // @phpstan-ignore-line ($requestBuilder->onMatch)($realRequest); } - return $this->responseFactory->fromRequestBuilder($requestBuilder); + return $requestBuilder->getResponse($realRequest); } public function addMockRequestBuilder(MockRequestBuilder $mockRequestBuilder): void @@ -60,4 +63,9 @@ public function getIterator(): Traversable { yield from $this->requestBuilders; } + + public function count(): int + { + return count($this->requestBuilders); + } } diff --git a/src/HttpClientMock/MockRequestBuilderFactory.php b/src/HttpClientMock/MockRequestBuilderFactory.php deleted file mode 100644 index c9afeeb..0000000 --- a/src/HttpClientMock/MockRequestBuilderFactory.php +++ /dev/null @@ -1,142 +0,0 @@ -method($method) - ->uri($url); - - foreach ($options['headers'] ?? [] as $header) { - [$key, $value] = explode(': ', (string) $header); - - $mockRequestBuilder->header((string) $key, (string) $value); - } - - if (array_key_exists('json', $options)) { - $mockRequestBuilder->json($options['json']); - } - - if (array_key_exists('body', $options)) { - $this->processBody($mockRequestBuilder, $options['body'], $options['headers'] ?? []); - } - - return $mockRequestBuilder; - } - - /** - * @param mixed[]|string|callable|null $body - * @param mixed[] $headers - */ - private function processBody( - MockRequestBuilder $mockRequestBuilder, - array|string|callable|null $body, - array $headers, - ): void { - $contentType = (string) $mockRequestBuilder->getHeader('Content-Type'); - $contentLength = $mockRequestBuilder->getHeader('Content-Length') ?? 0; - - // application/json; charset=utf-8 - if (strpos($contentType, 'application/json') === 0) { - if (is_string($body)) { - $mockRequestBuilder->json(json_decode($body, true)); - } elseif (is_callable($body)) { - $mockRequestBuilder->json(json_decode((string) $body((int) $contentLength), true)); - } elseif (is_array($body)) { - $mockRequestBuilder->json($body); - } else { - throw UnprocessableBody::create(); - } - - return; - } - - if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { - assert(is_string($body)); - - if (preg_match('/[^=]+=[^=]*(&[^=]+=[^=]*)*/', $body)) { - foreach (explode('&', $body) as $keyValue) { - [$key, $value] = explode('=', $keyValue); - - $mockRequestBuilder->requestParam(urldecode($key), urldecode($value)); - } - - return; - } - } - - // multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__ - if (strpos($contentType, 'multipart/form-data') === 0) { - $stream = fopen('php://temp', 'rw'); - - foreach ($headers as $header) { - fwrite($stream, $header . "\r\n"); - } - - fwrite($stream, "\r\n"); - - if (is_string($body)) { - fwrite($stream, $body); - } elseif (is_callable($body)) { - while ($chunk = ($body)(1000)) { - fwrite($stream, $chunk); - } - } else { - throw UnprocessableBody::create(); - } - - rewind($stream); - - $mp = new StreamedPart($stream); - foreach ($mp->getParts() as $part) { - assert($part instanceof StreamedPart); - - $mockRequestBuilder->multipart( - $part->getName(), - mimetype: $part->getMimeType(), - filename: $part->getFileName(), - content: $part->getBody(), - ); - } - - return; - } - - if (is_string($body)) { - $mockRequestBuilder->content($body); - - return; - } - - if (is_callable($body)) { - $mockRequestBuilder->content((string) $body((int) $contentLength)); - - return; - } - - throw UnprocessableBody::create(); - } -} diff --git a/src/HttpClientMock/MockRequestMatch.php b/src/HttpClientMock/MockRequestMatch.php deleted file mode 100644 index d89d5ba..0000000 --- a/src/HttpClientMock/MockRequestMatch.php +++ /dev/null @@ -1,181 +0,0 @@ -score; - } - - public function isMismatch(): bool - { - return $this->score === 0; - } - - public function getReason(): string|null - { - return $this->reason; - } - - /** @return mixed[] */ - public function getMatches(): array - { - return $this->matches; - } - - public function matchesMethod(string $method): void - { - $this->score += 10; - $this->matches['method'] = $method; - } - - public function matchesUri(string $uri): void - { - $this->score += 20; - $this->matches['uri'] = $uri; - } - - /** @param mixed[] $queryParams */ - public function matchesQueryParams(array $queryParams): void - { - $this->score += 5; - $this->matches['queryParams'] = $queryParams; - } - - public function matchesContent(string $content): void - { - $this->score += 5; - $this->matches['content'] = $content; - } - - /** @param mixed[] $multiparts */ - public function matchesMultiparts(array $multiparts): void - { - $this->score += 5; - $this->matches['multiparts'] = $multiparts; - } - - public static function create(): self - { - $match = new self(); - $match->score = 0; - - return $match; - } - - public static function empty(): self - { - return new self(1); - } - - public static function mismatchingMethod(string $method, string|null $otherMethod): self - { - return new self(0, sprintf('Mismatching method, expected %s, got %s', $method, $otherMethod ?? 'NULL')); - } - - public static function mismatchingUri(string $uri, string|null $otherUri): self - { - return new self(0, sprintf('Mismatching uri, expected %s, got %s', $uri, $otherUri ?? 'NULL')); - } - - public static function mismatchingContent(string $content, string|null $otherContent): self - { - return new self(0, sprintf('Mismatching content, expected %s, got %s', $content, $otherContent ?? 'NULL')); - } - - public static function mismatchingJsonContent(string $content, string|null $otherContent): self - { - return new self(0, sprintf('Mismatching json content, expected %s, got %s', $content, $otherContent ?? 'NULL')); - } - - public static function mismatchingXmlContent(string $content, string|null $otherContent): self - { - return new self(0, sprintf('Mismatching xml content, expected %s, got %s', $content, $otherContent ?? 'NULL')); - } - - public static function mismatchingRequestParameterContent(string $content, string|null $otherContent): self - { - return new self(0, sprintf('Mismatching request parameters, expected %s, got %s', $content, $otherContent ?? 'NULL')); // phpcs:ignore Generic.Files.LineLength.TooLong - } - - /** - * @param mixed[] $multiparts - * @param mixed[] $otherMultiparts - */ - public static function mismatchingMultiparts(array $multiparts, array|null $otherMultiparts): self - { - return new self( - 0, - sprintf( - 'Mismatching multiparts, expected %s, got %s', - json_encode($multiparts), - json_encode($otherMultiparts), - ), - ); - } - - public static function missingHeader(string $key, string $value): self - { - return new self( - 0, - sprintf( - 'Missing header, expected %s: %s', - $key, - json_encode($value), - ), - ); - } - - public static function mismatchingHeader(string $key, mixed $value, mixed $otherValue): self - { - return new self( - 0, - sprintf( - 'Mismatching header %s, expected %s, got %s', - $key, - json_encode($value), - json_encode($otherValue), - ), - ); - } - - /** - * @param mixed[] $queryParams - * @param mixed[] $otherQueryParams - */ - public static function mismatchingQueryParams(array $queryParams, array|null $otherQueryParams): self - { - return new self( - 0, - sprintf( - 'Mismatching query params, expected %s, got %s', - json_encode($queryParams), - json_encode($otherQueryParams), - ), - ); - } - - public static function mismatchingThat(string|Stringable $reason): self - { - return new self( - 0, - sprintf('Mismatching that, reason: %s', (string) $reason), - ); - } -} diff --git a/src/HttpClientMock/MockRequestMatcher.php b/src/HttpClientMock/MockRequestMatcher.php index cbdb679..1793d4b 100644 --- a/src/HttpClientMock/MockRequestMatcher.php +++ b/src/HttpClientMock/MockRequestMatcher.php @@ -4,264 +4,24 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock; -use DOMDocument; -use PHPUnit\Framework\AssertionFailedError; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Matcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MatchResult; -use function is_callable; -use function is_string; - -final class MockRequestMatcher +final readonly class MockRequestMatcher { - private Compare $compare; - - public function __construct() - { - $this->compare = new Compare(); - } - - public function __invoke(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): MockRequestMatch - { - if ($expectation->isEmpty()) { - return MockRequestMatch::empty(); - } - - if ($expectation->getMethod() !== null && $expectation->getMethod() !== $realRequest->getMethod()) { - return MockRequestMatch::mismatchingMethod($expectation->getMethod(), $realRequest->getMethod()); - } - - if ($expectation->getUri() !== null) { - $expectedUri = $expectation->getUri(); - $realUri = $realRequest->getUri(); - - if (is_callable($expectedUri) && !$expectedUri($realUri, $expectation->getUriParams())) { - return MockRequestMatch::mismatchingUri( - self::uriAsString($expectedUri), - self::uriAsString($realUri), - ); - } - - if (is_string($expectedUri) && $expectedUri !== $realUri) { - return MockRequestMatch::mismatchingUri( - self::uriAsString($expectedUri), - self::uriAsString($realUri), - ); - } - } - - // phpcs:disable Generic.Files.LineLength.TooLong - if ($expectation->hasQueryParams() && !($this->compare)($expectation->getQueryParams(), $realRequest->getQueryParams())) { - return MockRequestMatch::mismatchingQueryParams($expectation->getQueryParams(), $realRequest->getQueryParams()); - } - - // phpcs:disable Generic.Files.LineLength.TooLong - if ($expectation->hasHeaders()) { - foreach ($expectation->getHeaders() as $key => $value) { - if (!$realRequest->hasHeader($key)) { - return MockRequestMatch::missingHeader($key, $value); - } - - if (!($this->compare)($value, $realRequest->getHeader($key))) { - return MockRequestMatch::mismatchingHeader($key, $value, $realRequest->getHeader($key)); - } - } - } - - // phpcs:enable Generic.Files.LineLength.TooLong - - if ($expectation->isJson()) { - if (!$this->isJsonContentMatching($expectation, $realRequest)) { - return MockRequestMatch::mismatchingJsonContent( - $expectation->getContent(), - $realRequest->getContent(), - ); - } - } elseif ($expectation->isXml()) { - if (!$this->isXmlContentMatching($expectation, $realRequest)) { - return MockRequestMatch::mismatchingXmlContent( - $expectation->getContent(), - $realRequest->getContent(), - ); - } - } elseif ($expectation->hasRequestParams()) { - if (!$this->isRequestParamsMatching($expectation, $realRequest)) { - return MockRequestMatch::mismatchingRequestParameterContent( - $expectation->getContent(), - $realRequest->getContent(), - ); - } - } elseif ($expectation->hasContent()) { - if (!$this->isPlainContentMatching($expectation, $realRequest)) { - return MockRequestMatch::mismatchingContent( - $expectation->getContent(), - $realRequest->getContent(), - ); - } - } - - if ($expectation->getMultiparts() !== null) { - if (!$this->isMultipartMatching($expectation, $realRequest)) { - return MockRequestMatch::mismatchingMultiparts( - $expectation->getMultiparts(), - $this->reduceMultiparts($expectation, $realRequest), - ); - } - } - - if ($expectation->getThat() !== null) { - try { - $reason = ($expectation->getThat())($expectation, $realRequest); - } catch (AssertionFailedError $e) { - return MockRequestMatch::mismatchingThat($e->getMessage()); - } - - if ($reason) { - return MockRequestMatch::mismatchingThat($reason); - } - } - - $match = MockRequestMatch::create(); - - if ($expectation->getMethod() !== null && $expectation->getMethod() === $realRequest->getMethod()) { - $match->matchesMethod($expectation->getMethod()); - } - - if ($expectation->getUri() !== null) { - $expectedUri = $expectation->getUri(); - $realUri = $realRequest->getUri(); - - if (is_callable($expectedUri)) { - if ($expectedUri($realUri, $expectation->getUriParams())) { - $match->matchesUri(''); - } - } else { - if ($expectation->getUri() === $realRequest->getUri()) { - $match->matchesUri($expectation->getUri()); - } - } - } - - if ( - $expectation->hasQueryParams() - && ($this->compare)($expectation->getQueryParams(), $realRequest->getQueryParams()) - ) { - $match->matchesQueryParams($expectation->getQueryParams()); - } - - if ( - $this->isPlainContentMatching($expectation, $realRequest) - || $this->isJsonContentMatching($expectation, $realRequest) - || $this->isRequestParamsMatching($expectation, $realRequest) - ) { - $match->matchesContent($expectation->getContent()); - } - - if ($expectation->getMultiparts() !== null) { - $match->matchesMultiparts($expectation->getMultiparts()); - } - - return $match; - } - - private function isPlainContentMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool - { - if (!$expectation->hasContent() || !$realRequest->hasContent()) { - return false; - } - - return $expectation->getContent() === $realRequest->getContent(); - } - - /** @return array */ - private function reduceMultiparts(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): array - { - $realMultiparts = $realRequest->getMultiparts(); - $expectationMultiparts = $expectation->getMultiparts(); - - $reducedMultiparts = $realMultiparts; - foreach ($expectationMultiparts as $key => $data) { - if (!($realMultiparts[$key] ?? false)) { - continue; - } - - $reducedMultiparts[$key] = []; - foreach ($data as $name => $value) { - $reducedMultiparts[$key][$name] = $value !== null ? $realMultiparts[$key][$name] : null; - } - } - - return $reducedMultiparts; - } - - private function isMultipartMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool - { - if (!$expectation->hasMultiparts() && !$realRequest->hasMultiparts()) { - return true; - } - - if (!$expectation->hasMultiparts() || !$realRequest->hasMultiparts()) { - return false; - } - - $realMultiparts = $this->reduceMultiparts($expectation, $realRequest); - - return ($this->compare)($expectation->getMultiparts(), $realMultiparts); - } - - private function isJsonContentMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool - { - if (!$expectation->hasContent() || !$expectation->isJson()) { - return false; - } - - if (!$realRequest->hasContent() || !$realRequest->isJson()) { - return false; - } - - return ($this->compare)($expectation->getJson(), $realRequest->getJson()); - } - - private function isXmlContentMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool + /** @param Matcher[] $matchers */ + public function __construct(private string|null $name, private array $matchers) { - if (!$expectation->hasContent() || !$expectation->isXml()) { - return false; - } - - if (!$realRequest->hasContent() || !$realRequest->isXml()) { - return false; - } - - $expectedDom = new DOMDocument(); - $expectedDom->preserveWhiteSpace = false; - $expectedDom->formatOutput = true; - $expectedDom->loadXML($expectation->getContent()); - - $realDom = new DOMDocument(); - $realDom->preserveWhiteSpace = false; - $realDom->formatOutput = true; - $realDom->loadXML($realRequest->getContent()); - - return $expectedDom->saveXML() === $realDom->saveXML(); } - private function isRequestParamsMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool + public function __invoke(RealRequest $realRequest): MatchResult { - if (!$expectation->hasContent() || !$expectation->hasRequestParams()) { - return false; - } + $result = MatchResult::create($this->name); - if (!$realRequest->hasContent() || !$realRequest->hasRequestParams()) { - return false; - } - - return ($this->compare)($expectation->getRequestParams(), $realRequest->getRequestParams()); - } - - private static function uriAsString(string|callable|null $uri): string // phpcs:ignore Generic.Files.LineLength.TooLong,SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter - { - if (is_callable($uri)) { - return ''; + foreach ($this->matchers as $matcher) { + $result = $result->withResult($matcher($realRequest)); } - return (string) $uri; + return $result; } } diff --git a/src/HttpClientMock/MockRequestResolver.php b/src/HttpClientMock/MockRequestResolver.php index b1bc2d7..7fd51f3 100644 --- a/src/HttpClientMock/MockRequestResolver.php +++ b/src/HttpClientMock/MockRequestResolver.php @@ -5,52 +5,51 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\NoMatchingMockRequest; +use SplObjectStorage; -use function array_pop; use function count; +use function current; +use function krsort; final class MockRequestResolver { - private MockRequestMatcher $matcher; - - public function __construct() - { - $this->matcher = new MockRequestMatcher(); - } - public function __invoke( MockRequestBuilderCollection $requestBuilders, - MockRequestBuilder $realRequest, + RealRequest $realRequest, ): MockRequestBuilder { - $matches = []; - $bestScore = null; - $bestMatchingRequestBuilders = []; + $scoredMatchResults = []; - foreach ($requestBuilders as $requestBuilder) { - $matches[] = $match = ($this->matcher)($requestBuilder, $realRequest); + if (!count($requestBuilders)) { + throw NoMatchingMockRequest::noBuilders($realRequest); + } - if ($match->isMismatch() || ($bestScore !== null && $match->getScore() < $bestScore)) { - continue; - } + $builders = new SplObjectStorage(); + foreach ($requestBuilders as $requestBuilder) { + $matchResult = ($requestBuilder->getMatcher())($realRequest); + $scoredMatchResults[$matchResult->getScore()][] = $matchResult; + $builders[$matchResult] = $requestBuilder; + } - if ($bestScore === null || $match->getScore() > $bestScore) { - $bestMatchingRequestBuilders = []; - $bestScore = $match->getScore(); - } + krsort($scoredMatchResults); - $bestMatchingRequestBuilders[] = $requestBuilder; + $missedMatchResults = []; + if ($scoredMatchResults[0] ?? false) { + $missedMatchResults = $scoredMatchResults[0]; + unset($scoredMatchResults[0]); } - if (count($bestMatchingRequestBuilders) === 0) { - throw NoMatchingMockRequest::fromMockRequest($realRequest, $matches); + if (count($scoredMatchResults) === 0) { + throw NoMatchingMockRequest::fromResults($realRequest, $missedMatchResults); } - foreach ($bestMatchingRequestBuilders as $requestBuilder) { - if ($requestBuilder->hasNextResponse()) { - return $requestBuilder; + foreach ($scoredMatchResults as $matchResults) { + foreach ($matchResults as $matchResult) { + if ($builders[$matchResult]->hasNextResponse()) { + return $builders[$matchResult]; + } } } - return array_pop($bestMatchingRequestBuilders); + return $builders[current(current($scoredMatchResults))]; } } diff --git a/src/HttpClientMock/MockResponseBuilder.php b/src/HttpClientMock/MockResponseBuilder.php index 30f7c91..1ec3e39 100644 --- a/src/HttpClientMock/MockResponseBuilder.php +++ b/src/HttpClientMock/MockResponseBuilder.php @@ -4,9 +4,9 @@ namespace Brainbits\FunctionalTestHelpers\HttpClientMock; -use function array_key_exists; +use Symfony\Component\HttpClient\Response\MockResponse; + use function count; -use function Safe\json_decode; use function Safe\json_encode; use function sprintf; use function str_repeat; @@ -23,6 +23,8 @@ final class MockResponseBuilder private array $headers = []; private string|null $content = null; private int|null $code = null; + /** @var callable|null */ + private mixed $callback = null; public function content(string|null $content): self { @@ -83,70 +85,48 @@ public function code(int|null $code): self return $this; } - /** @return mixed[] */ - public function getHeaders(): array + /** @param callable(RealRequest $realRequest):MockResponse $callback */ + public function fromCallback(callable $callback): self { - return $this->headers; - } + $this->callback = $callback; - public function hasHeaders(): bool - { - return count($this->headers) > 0; + return $this; } - public function getHeader(string $key): string|null + public function getResponse(RealRequest $realRequest): MockResponse { - if ($this->hasHeader($key)) { - return $this->headers[strtolower($key)]; + if ($this->callback) { + return ($this->callback)($realRequest); } - return null; - } - - public function hasHeader(string $key): bool - { - return array_key_exists(strtolower($key), $this->headers); - } - - public function getContent(): string|null - { - return $this->content; - } - - public function hasContent(): bool - { - return $this->content !== null; - } + $info = []; - /** @return mixed[]|null */ - public function getJson(): array|null - { - if ($this->content === null) { - return null; + if ($this->code) { + $info['http_code'] = $this->code; } - return json_decode($this->content, true); - } + if (count($this->headers)) { + $info['response_headers'] = $this->headers; + } - public function getCode(): int|null - { - return $this->code; - } + $body = (string) $this->content; - public function hasCode(): bool - { - return $this->code !== null; + return new MockResponse($body, $info); } public function __toString(): string { + if ($this->callback) { + return 'callable(realRequest)'; + } + $string = ''; - if ($this->hasCode()) { - $string .= sprintf('HTTP Code: %d', $this->getCode()); + if ($this->code) { + $string .= sprintf('HTTP Code: %d', $this->code); } - if ($this->hasHeaders()) { + if (count($this->headers)) { foreach ($this->headers as $key => $value) { $key = str_replace('-', ' ', $key); $key = ucwords($key); @@ -156,7 +136,7 @@ public function __toString(): string } } - if ($this->hasContent()) { + if ($this->content) { $string .= ($string ? str_repeat(PHP_EOL, 2) : ''); $string .= $this->content; } diff --git a/src/HttpClientMock/MockResponseFactory.php b/src/HttpClientMock/MockResponseFactory.php deleted file mode 100644 index eb34d7f..0000000 --- a/src/HttpClientMock/MockResponseFactory.php +++ /dev/null @@ -1,12 +0,0 @@ - $headers + * @param mixed[] $json + * @param array $queryParams + * @param array $requestParams + * @param array $multiparts + */ + public function __construct( + private string $method, + private string $uri, + private array $headers, + private string|null $content, + private array|null $json, + private array $queryParams, + private array $requestParams, + private array $multiparts, + ) { + $this->headers = array_combine( + array_map(strtolower(...), array_keys($headers)), + array_values($headers), + ); + } + + public function getMethod(): string + { + return $this->method; + } + + public function getUri(): string + { + return $this->uri; + } + + public function hasHeader(string $key): bool + { + return array_key_exists($key, $this->headers); + } + + public function getHeader(string $key): string|null + { + return $this->headers[strtolower($key)] ?? null; + } + + /** @return array */ + public function getHeaders(): array + { + return $this->headers; + } + + public function getContent(): string|null + { + return $this->content; + } + + /** @return mixed[]|null */ + public function getJson(): array|null + { + return $this->json; + } + + public function hasQueryParam(string $key): bool + { + return array_key_exists($key, $this->queryParams); + } + + public function getQueryParam(string $key): string|null + { + return $this->queryParams[$key] ?? null; + } + + /** @return array */ + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function hasRequestParam(string $key): bool + { + return array_key_exists($key, $this->requestParams); + } + + public function getRequestParam(string $key): string|null + { + return $this->requestParams[$key] ?? null; + } + + /** @return array */ + public function getRequestParams(): array + { + return $this->requestParams; + } + + public function hasMultipart(string $name): bool + { + return (bool) ($this->multiparts[$name] ?? false); + } + + /** @return array{name: string, filename?: string, mimetype?: string, content?: string}|null */ + public function getMultipart(string $name): array|null + { + return $this->multiparts[$name] ?? null; + } + + /** @return array */ + public function getMultiparts(): array + { + return $this->multiparts; + } + + public function __toString(): string + { + $string = $this->method . ' ' . $this->uri; + + if ($this->queryParams) { + $string .= '?' . http_build_query($this->queryParams); + } + + if ($this->headers) { + $string .= PHP_EOL; + foreach ($this->headers as $key => $header) { + $string .= sprintf('%s: %s%s', $key, $header, PHP_EOL); + } + } + + if ($this->requestParams) { + foreach ($this->requestParams as $key => $value) { + $string .= sprintf('&%s=%s', $key, $value); + } + + $string .= PHP_EOL; + } elseif ($this->multiparts) { + foreach ($this->multiparts as $key => $multipart) { + $parts = ($multipart['filename'] ?? false ? sprintf(', filename=%s', $multipart['filename']) : '') . + ($multipart['mimetype'] ?? false ? sprintf(', mimetype=%s', $multipart['mimetype']) : '') . + ($multipart['content'] ?? false ? sprintf(', content=%s', $multipart['content']) : ''); + $string .= sprintf('%s: name=%s, %s%s', $key, $multipart['name'], $parts, PHP_EOL); + } + } elseif ($this->json !== null) { + $string .= json_encode($this->json); + } elseif ($this->content !== null) { + $string .= $this->content; + } + + return trim($string); + } +} diff --git a/src/HttpClientMock/RealRequestFactory.php b/src/HttpClientMock/RealRequestFactory.php new file mode 100644 index 0000000..0180944 --- /dev/null +++ b/src/HttpClientMock/RealRequestFactory.php @@ -0,0 +1,165 @@ +parseEncodedParams($encodedParams) as $key => $value) { + $queryParams[$key] = $value; + } + } + + foreach ($options['headers'] ?? [] as $header) { + [$key, $value] = explode(': ', (string) $header); + + $headers[strtolower($key)] = $value; + } + + if (array_key_exists('json', $options)) { + $json = $options['json']; + } + + if (array_key_exists('body', $options)) { + $body = $options['body']; + $contentType = $headers['content-type'] ?? ''; + $contentLength = $headers['content-length'] ?? 0; + + // application/json; charset=utf-8 + if (strpos($contentType, 'application/json') === 0) { + if (is_string($body)) { + $json = json_decode($body, true); + } elseif (is_callable($body)) { + $json = json_decode((string) $body((int) $contentLength), true); + } elseif (is_array($body)) { + $json = $body; + } else { + throw UnprocessableBody::create(); + } + } + + if (strpos($contentType, 'application/x-www-form-urlencoded') === 0) { + assert(is_string($body)); + + if (preg_match('/[^=]+=[^=]*(&[^=]+=[^=]*)*/', $body)) { + foreach (explode('&', $body) as $keyValue) { + [$key, $value] = explode('=', $keyValue); + + $requestParams[urldecode($key)] = urldecode($value); + } + } + } + + // multipart/form-data; charset=utf-8; boundary=__X_PAW_BOUNDARY__ + if (strpos($contentType, 'multipart/form-data') === 0) { + $stream = fopen('php://temp', 'rw'); + + foreach ($headers as $key => $value) { + fwrite($stream, $key . ': ' . $value . "\r\n"); + } + + fwrite($stream, "\r\n"); + + if (is_string($body)) { + fwrite($stream, $body); + } elseif (is_callable($body)) { + while ($chunk = ($body)(1000)) { + fwrite($stream, $chunk); + } + } else { + throw UnprocessableBody::create(); + } + + rewind($stream); + + $mp = new StreamedPart($stream); + foreach ($mp->getParts() as $part) { + assert($part instanceof StreamedPart); + + $multiparts[$part->getName()] = [ + 'name' => $part->getName(), + 'mimetype' => $part->getMimeType(), + 'filename' => $part->getFileName(), + 'content' => $part->getBody(), + ]; + } + } + + if (is_string($body)) { + $content = $body; + } elseif (is_callable($body)) { + $content = (string) $body((int) $contentLength); + } + } + + return new RealRequest( + $method, + $url, + $headers, + $content, + $json, + $queryParams, + $requestParams, + $multiparts, + ); + } + + /** @return string[] */ + private function parseEncodedParams(string $encodedParams): array + { + if ($encodedParams === '') { + return []; + } + + $params = []; + + foreach (explode('&', $encodedParams) as $keyValue) { + if (str_contains($keyValue, '=')) { + [$key, $value] = explode('=', (string) $keyValue); + } else { + $key = $keyValue; + $value = ''; + } + + $params[urldecode((string) $key)] = urldecode((string) $value); + } + + return $params; + } +} diff --git a/src/HttpClientMock/SymfonyMockResponseFactory.php b/src/HttpClientMock/SymfonyMockResponseFactory.php deleted file mode 100644 index ce275a6..0000000 --- a/src/HttpClientMock/SymfonyMockResponseFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -nextResponse(); - - if ($responseBuilder instanceof Throwable) { - throw $responseBuilder; - } - - $info = []; - - if ($responseBuilder->hasCode()) { - $info['http_code'] = $responseBuilder->getCode(); - } - - if ($responseBuilder->hasHeaders()) { - $info['response_headers'] = $responseBuilder->getHeaders(); - } - - $body = (string) $responseBuilder->getContent(); - - return new MockResponse($body, $info); - } -} diff --git a/tests/HttpClientMock/CompareTest.php b/tests/HttpClientMock/CompareTest.php deleted file mode 100644 index 95231d4..0000000 --- a/tests/HttpClientMock/CompareTest.php +++ /dev/null @@ -1,88 +0,0 @@ -assertTrue($compare(1, 1)); - $this->assertTrue($compare('foo', 'foo')); - $this->assertTrue($compare(5.8, 5.8)); - - $this->assertFalse($compare(1, 2)); - $this->assertFalse($compare(1, 1.0)); - $this->assertFalse($compare(1, '1')); - $this->assertFalse($compare('foo', 'bar')); - } - - public function testCompareSimpleArray(): void - { - $compare = new Compare(); - - $this->assertTrue($compare(null, null)); - $this->assertTrue($compare([1, 2, 3], [1, 2, 3])); - $this->assertTrue($compare(['foo', 'bar', 'baz'], ['foo', 'bar', 'baz'])); - $this->assertTrue($compare([], [])); - - $this->assertFalse($compare([1, 2, 3], [1, 2, 4])); - $this->assertFalse($compare([1, 2, 3], ['1', 2, 3])); - $this->assertFalse($compare(['foo', 'bar', 'baz'], ['a', 'b', 'c'])); - $this->assertFalse($compare([], [1])); - $this->assertFalse($compare([], null)); - } - - public function testCompareSimpleAssociativeArray(): void - { - $compare = new Compare(); - - $this->assertTrue($compare(['foo' => 1], ['foo' => 1])); - $this->assertTrue($compare(['foo' => 'bar'], ['foo' => 'bar'])); - - $this->assertFalse($compare(['foo' => 1], ['baz' => 1])); - $this->assertFalse($compare(['foo' => 'bar'], ['baz' => 'bar'])); - } - - public function testCompareNestedArray(): void - { - $compare = new Compare(); - - $this->assertTrue($compare( - [[1, 2, 3], [4, 5, 6]], - [[1, 2, 3], [4, 5, 6]], - )); - $this->assertTrue($compare( - [['foo', 'bar', 'baz'], ['a', 'b', 'c']], - [['foo', 'bar', 'baz'], ['a', 'b', 'c']], - )); - $this->assertTrue($compare( - [['foo', 'bar', 'baz'], ['a', 'b', 'c']], - [['a', 'b', 'c'], ['foo', 'bar', 'baz']], - )); - $this->assertFalse($compare( - [[1, 2, 3], [4, 5, 6]], - [[1, 2, 3]], - )); - $this->assertFalse($compare( - [[1, 2, 3], [4, 5, 6]], - [[1, 2, 3], [4, 5, 7]], - )); - $this->assertFalse($compare( - ['a' => [1, 2, 3], 'b' => [4, 5, 6]], - ['b' => [1, 2, 3], 'a' => [4, 5, 7]], - )); - $this->assertFalse($compare( - ['a' => [1, 2, 3], 'b' => [4, 5, 6]], - ['a' => [1, 2, 3], 'b' => [4, 5, 7], 'c' => []], - )); - } -} diff --git a/tests/HttpClientMock/HttpClientMockTraitTest.php b/tests/HttpClientMock/HttpClientMockTraitTest.php new file mode 100644 index 0000000..45b5973 --- /dev/null +++ b/tests/HttpClientMock/HttpClientMockTraitTest.php @@ -0,0 +1,95 @@ +set(MockRequestBuilderCollection::class, self::$collection); + + return $container; + } + + public function testMockRequest(): void + { + $this->mockRequest('GET', '/query') + ->name('test') + ->queryParam('firstname', 'peter') + ->queryParam('lastname', '%s', 'peterson') + ->queryParam('email', '') + ->header('content-type', 'text/plain') + ->content('this is text') + ->willRespond( + $this->mockResponse() + ->header('content-type', 'text/plain') + ->content('test'), + ); + + // simulate http request + (self::$collection)( + 'GET', + '/query?firstname=peter&lastname=peterson&email=', + [ + 'headers' => ['Content-Type: text/plain'], + 'body' => 'this is text', + ], + ); + + $this->assertAllRequestMocksAreCalled(); + } + + public function testMockRequestParsesLegacyQueryParamsFromUri(): void + { + $builder = $this->mockRequest('GET', '/query?&firstname=peter&lastname={lastname}&email='); + + $this->assertEquals( + $builder->getMatcher(), + new MockRequestMatcher(null, [ + new MethodMatcher('GET'), + new QueryParamMatcher('firstname', 'peter', []), + new QueryParamMatcher('lastname', '{lastname}', []), + new QueryParamMatcher('email', '', []), + new UriMatcher('/query', new UriParams()), + ]), + ); + } + + public function testMockRequestParsesLegacyQueryParamsFromUriWithHost(): void + { + $builder = $this->mockRequest('GET', 'https://example.com/query?&firstname=peter&lastname={lastname}&email='); + + $this->assertEquals( + $builder->getMatcher(), + new MockRequestMatcher(null, [ + new MethodMatcher('GET'), + new QueryParamMatcher('firstname', 'peter', []), + new QueryParamMatcher('lastname', '{lastname}', []), + new QueryParamMatcher('email', '', []), + new UriMatcher('https://example.com/query', new UriParams()), + ]), + ); + } +} diff --git a/tests/HttpClientMock/Matcher/CatchAllMatcherTest.php b/tests/HttpClientMock/Matcher/CatchAllMatcherTest.php new file mode 100644 index 0000000..c45bcf7 --- /dev/null +++ b/tests/HttpClientMock/Matcher/CatchAllMatcherTest.php @@ -0,0 +1,38 @@ +createRealRequest(content: 'this is text'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(1, $result); + self::assertMatcher('catchAll', $result); + } + + public function testToString(): void + { + $matcher = new CatchAllMatcher(); + + self::assertSame('*', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/ContentMatcherTest.php b/tests/HttpClientMock/Matcher/ContentMatcherTest.php new file mode 100644 index 0000000..86e5cee --- /dev/null +++ b/tests/HttpClientMock/Matcher/ContentMatcherTest.php @@ -0,0 +1,87 @@ +createRealRequest(content: 'this is text'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('content', $result); + } + + public function testMatchContentWithCallback(): void + { + $matcher = new ContentMatcher(static fn ($content) => $content === 'this is text'); + + $realRequest = $this->createRealRequest(content: 'this is text'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('content', $result); + } + + public function testMismatchContent(): void + { + $matcher = new ContentMatcher('this is text'); + + $realRequest = $this->createRealRequest(content: 'does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('content', $result); + } + + public function testMismatchContentWithCallback(): void + { + $matcher = new ContentMatcher(static fn ($content) => $content === 'this is text'); + + $realRequest = $this->createRealRequest(content: 'does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('content', $result); + } + + public function testToString(): void + { + $matcher = new ContentMatcher('this is text'); + + self::assertSame('request.content === "this is text"', (string) $matcher); + } + + public function testToStringWithCallback(): void + { + $matcher = new ContentMatcher(static fn ($content) => $content === 'this is text'); + + self::assertSame('callback(request.content) !== false', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/HeaderMatcherTest.php b/tests/HttpClientMock/Matcher/HeaderMatcherTest.php new file mode 100644 index 0000000..ed13233 --- /dev/null +++ b/tests/HttpClientMock/Matcher/HeaderMatcherTest.php @@ -0,0 +1,69 @@ +createRealRequest(headers: ['Accept' => 'text/plain']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('header', $result); + } + + public function testMissesHeader(): void + { + $matcher = new HeaderMatcher('Accept', 'text/plain'); + + $realRequest = $this->createRealRequest(headers: ['Content-Type' => 'text/plain']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Missing::class, $result); + self::assertScore(0, $result); + self::assertMatcher('header', $result); + } + + public function testMismatchesHeader(): void + { + $matcher = new HeaderMatcher('Accept', 'text/plain'); + + $realRequest = $this->createRealRequest(headers: ['Accept' => 'text/does-not-match']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('header', $result); + } + + public function testToStringWithCallback(): void + { + $matcher = new HeaderMatcher('Accept', 'text/plain'); + + self::assertSame('request.header["text/plain"] === "accept"', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/JsonMatcherTest.php b/tests/HttpClientMock/Matcher/JsonMatcherTest.php new file mode 100644 index 0000000..1ff00ac --- /dev/null +++ b/tests/HttpClientMock/Matcher/JsonMatcherTest.php @@ -0,0 +1,117 @@ + 'peter', 'lastname' => 'peterson']); + + $realRequest = $this->createRealRequest(json: ['firstname' => 'peter', 'lastname' => 'peterson']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('json', $result); + } + + public function testMatchJsonWithCallback(): void + { + $matcher = new JsonMatcher( + static fn ($content) => json_encode($content) === '{"firstname":"peter","lastname":"peterson"}', + ); + + $realRequest = $this->createRealRequest(json: ['firstname' => 'peter', 'lastname' => 'peterson']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('json', $result); + } + + public function testMismatchJson(): void + { + $matcher = new JsonMatcher(['firstname' => 'peter', 'lastname' => 'peterson']); + + $realRequest = $this->createRealRequest(json: ['firstname' => 'peter', 'lastname' => 'does-not-match']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('json', $result); + self::assertSame('--- Expected ++++ Actual +@@ @@ + { + "firstname": "peter", +- "lastname": "peterson" ++ "lastname": "does-not-match" + } +', $result->diff); + } + + public function testMismatchJsonWithCallback(): void + { + $matcher = new JsonMatcher( + static fn ($content) => json_encode($content) === '{"firstname":"peter","lastname":"peterson"}', + ); + + $realRequest = $this->createRealRequest(json: ['firstname' => 'peter', 'lastname' => 'does-not-match']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('json', $result); + self::assertNull($result->diff); + } + + public function testMismatchJsonWithoutDiff(): void + { + $matcher = new JsonMatcher(['firstname' => 'peter', 'lastname' => 'peterson']); + + $realRequest = $this->createRealRequest(json: null); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('json', $result); + self::assertNull($result->diff); + } + + public function testToString(): void + { + $matcher = new JsonMatcher(['firstname' => 'peter', 'lastname' => 'peterson']); + + self::assertSame('request.content === "{"firstname":"peter","lastname":"peterson"}"', (string) $matcher); + } + + public function testToStringWithCallback(): void + { + $matcher = new JsonMatcher(static fn ($content) => $content === 'this is text'); + + self::assertSame('callback(request.content) !== false', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/MatchResultTest.php b/tests/HttpClientMock/Matcher/MatchResultTest.php new file mode 100644 index 0000000..875d7ed --- /dev/null +++ b/tests/HttpClientMock/Matcher/MatchResultTest.php @@ -0,0 +1,102 @@ +assertNull($matchResult1->getName()); + $this->assertSame('test', $matchResult2->getName()); + } + + public function testResultsCanBeEmpty(): void + { + $matchResult = MatchResult::create('test'); + + $this->assertSame([], $matchResult->getResults()); + } + + public function testResultsCanBeSet(): void + { + $hit = Hit::matchesMethod('GET'); + $mismatch = Mismatch::mismatchingUri('/query', '/does-not-match'); + $missing = Missing::missingHeader('Content-Type', 'application/json'); + + $matchResult = MatchResult::create('test') + ->withResult($hit) + ->withResult($mismatch) + ->withResult($missing); + + $this->assertSame([$hit, $mismatch, $missing], $matchResult->getResults()); + } + + public function testEmptyHasZeroScore(): void + { + $matchResult = MatchResult::create('test'); + + $this->assertSame(0, $matchResult->getScore()); + } + + public function testEmptyIsNoMismatch(): void + { + $matchResult = MatchResult::create('test'); + + $this->assertFalse($matchResult->isMismatch()); + } + + public function testOnlyHitsHaveScore(): void + { + $matchResult = MatchResult::create('test') + ->withResult(Hit::matchesMethod('GET')) + ->withResult(Hit::matchesUri('/query')); + + $this->assertSame(30, $matchResult->getScore()); + } + + public function testOnlyHitsIsNoMismatch(): void + { + $matchResult = MatchResult::create('test') + ->withResult(Hit::matchesMethod('GET')) + ->withResult(Hit::matchesUri('/query')); + + $this->assertFalse($matchResult->isMismatch()); + } + + public function testGetMixedResultsHaveZeroScore(): void + { + $matchResult = MatchResult::create('test') + ->withResult(Hit::matchesMethod('GET')) + ->withResult(Mismatch::mismatchingUri('/query', '/does-not-match')) + ->withResult(Missing::missingHeader('Content-Type', 'application/json')); + + $this->assertSame(0, $matchResult->getScore()); + } + + public function testGetMixedResultsIsMismatch(): void + { + $matchResult = MatchResult::create('test') + ->withResult(Hit::matchesMethod('GET')) + ->withResult(Mismatch::mismatchingUri('/query', '/does-not-match')) + ->withResult(Missing::missingHeader('Content-Type', 'application/json')); + + $this->assertTrue($matchResult->isMismatch()); + } +} diff --git a/tests/HttpClientMock/Matcher/MatcherTrait.php b/tests/HttpClientMock/Matcher/MatcherTrait.php new file mode 100644 index 0000000..aad1e4c --- /dev/null +++ b/tests/HttpClientMock/Matcher/MatcherTrait.php @@ -0,0 +1,22 @@ +score); + } + + private static function assertMatcher(string $matcher, Hit|Mismatch|Missing $result): void + { + self::assertSame($matcher, $result->matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/MethodMatcherTest.php b/tests/HttpClientMock/Matcher/MethodMatcherTest.php new file mode 100644 index 0000000..b3fa406 --- /dev/null +++ b/tests/HttpClientMock/Matcher/MethodMatcherTest.php @@ -0,0 +1,87 @@ +createRealRequest(method: 'GET'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(10, $result); + self::assertMatcher('method', $result); + } + + public function testMatchMethodWithCallback(): void + { + $matcher = new MethodMatcher(static fn ($method) => $method === 'GET'); + + $realRequest = $this->createRealRequest(method: 'GET'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(10, $result); + self::assertMatcher('method', $result); + } + + public function testMismatchMethod(): void + { + $matcher = new MethodMatcher('GET'); + + $realRequest = $this->createRealRequest(method: 'POST'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('method', $result); + } + + public function testMismatchMethodWithCallback(): void + { + $matcher = new MethodMatcher(static fn ($method) => $method === 'GET'); + + $realRequest = $this->createRealRequest(method: 'POST'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('method', $result); + } + + public function testToString(): void + { + $matcher = new MethodMatcher('GET'); + + self::assertSame('request.method === "GET"', (string) $matcher); + } + + public function testToStringWithCallback(): void + { + $matcher = new MethodMatcher(static fn ($method) => $method === 'GET'); + + self::assertSame('callback(request.method) !== false', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/MultipartMatcherTest.php b/tests/HttpClientMock/Matcher/MultipartMatcherTest.php new file mode 100644 index 0000000..62e9c22 --- /dev/null +++ b/tests/HttpClientMock/Matcher/MultipartMatcherTest.php @@ -0,0 +1,193 @@ +createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('multipart', $result); + } + + public function testMissingMultipart(): void + { + $matcher = new MultipartMatcher('file', 'application/pdf', 'file.pdf', 'pdf'); + + $realRequest = $this->createRealRequest(multiparts: [ + 'does-not-match' => [ + 'name' => 'does-not-match', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Missing::class, $result); + self::assertScore(0, $result); + self::assertMatcher('multipart', $result); + } + + public function testMismatchingMultipart(): void + { + $matcher = new MultipartMatcher('file', 'application/pdf', 'file.pdf', 'pdf'); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'does-not-match', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('multipart', $result); + } + + public function testMatchesWithoutMimetype(): void + { + $matcher = new MultipartMatcher('file', null, 'file.pdf', 'pdf'); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('multipart', $result); + } + + public function testMatchesWithoutFilename(): void + { + $matcher = new MultipartMatcher('file', 'application/pdf', null, 'pdf'); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('multipart', $result); + } + + public function testMatchesWithoutContent(): void + { + $matcher = new MultipartMatcher('file', 'application/pdf', 'file.pdf', null); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('multipart', $result); + } + + public function testMatchesWithOnlyName(): void + { + $matcher = new MultipartMatcher('file', null, null, null); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/pdf', + 'filename' => 'file.pdf', + 'content' => 'pdf', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('multipart', $result); + } + + public function testDoesNotMatchWithIncompleteMultiparts(): void + { + $matcher = new MultipartMatcher('file', null, null, 'pdf'); + + $realRequest = $this->createRealRequest(multiparts: [ + 'file' => [ + 'name' => 'file', + 'mimetype' => 'application/does-not-match', + 'filename' => 'does-not-match.pdf', + 'content' => 'does-not-match', + ], + ]); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('multipart', $result); + } + + public function testToString(): void + { + $matcher = new MultipartMatcher('file', 'application/pdf', 'file.pdf', 'pdf'); + + self::assertSame( + '[filename=file.pdf, mimetype=application/pdf, content=pdf] === request.request[file]', + (string) $matcher, + ); + } +} diff --git a/tests/HttpClientMock/Matcher/QueryParamMatcherTest.php b/tests/HttpClientMock/Matcher/QueryParamMatcherTest.php new file mode 100644 index 0000000..c08ae4d --- /dev/null +++ b/tests/HttpClientMock/Matcher/QueryParamMatcherTest.php @@ -0,0 +1,95 @@ +createRealRequest(queryParams: ['filter' => 'lastname']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('queryParam', $result); + } + + public function testMatchQueryParamWithZeroValue(): void + { + $matcher = new QueryParamMatcher('filter', '0', []); + + $realRequest = $this->createRealRequest(queryParams: ['filter' => '0']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('queryParam', $result); + } + + public function testMatchQueryParamWithPlaceholder(): void + { + $matcher = new QueryParamMatcher('filter', '%s%s', ['last', 'name']); + + $realRequest = $this->createRealRequest(queryParams: ['filter' => 'lastname']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('queryParam', $result); + } + + public function testMissesQueryParam(): void + { + $matcher = new QueryParamMatcher('filter', 'lastname', []); + + $realRequest = $this->createRealRequest(); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Missing::class, $result); + self::assertScore(0, $result); + self::assertMatcher('queryParam', $result); + } + + public function testMismatchQueryParam(): void + { + $matcher = new QueryParamMatcher('filter', 'lastname', []); + + $realRequest = $this->createRealRequest(queryParams: ['filter' => 'does-not-match']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('queryParam', $result); + } + + public function testToString(): void + { + $matcher = new QueryParamMatcher('filter', 'lastname', []); + + self::assertSame('request.queryParams["filter"] === "lastname"', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/RequestParamMatcherTest.php b/tests/HttpClientMock/Matcher/RequestParamMatcherTest.php new file mode 100644 index 0000000..d5fe225 --- /dev/null +++ b/tests/HttpClientMock/Matcher/RequestParamMatcherTest.php @@ -0,0 +1,69 @@ +createRealRequest(requestParams: ['firstname' => 'tester']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('requestParam', $result); + } + + public function testMissesRequestParam(): void + { + $matcher = new RequestParamMatcher('firstname', 'tester'); + + $realRequest = $this->createRealRequest(); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Missing::class, $result); + self::assertScore(0, $result); + self::assertMatcher('requestParam', $result); + } + + public function testMismatchRequestParam(): void + { + $matcher = new RequestParamMatcher('firstname', 'tester'); + + $realRequest = $this->createRealRequest(requestParams: ['firstname' => 'does-not-match']); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('requestParam', $result); + } + + public function testToString(): void + { + $matcher = new RequestParamMatcher('firstname', 'tester'); + + self::assertSame('request.requestParams["firstname"] === "tester"', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/ThatMatcherTest.php b/tests/HttpClientMock/Matcher/ThatMatcherTest.php new file mode 100644 index 0000000..94acba3 --- /dev/null +++ b/tests/HttpClientMock/Matcher/ThatMatcherTest.php @@ -0,0 +1,68 @@ + $realRequest->getContent() === 'test'); + + $realRequest = $this->createRealRequest(content: 'test'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('that', $result); + } + + public function testMatchThatWithNoReturn(): void + { + $matcher = new ThatMatcher(static function ($realRequest): void { + }); + + $realRequest = $this->createRealRequest(content: 'test'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('that', $result); + } + + public function testMismatchThatWithExplicitReturnFalse(): void + { + $matcher = new ThatMatcher(static fn ($realRequest) => $realRequest->getContent() === 'test'); + + $realRequest = $this->createRealRequest(content: 'does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('that', $result); + } + + public function testToString(): void + { + $matcher = new ThatMatcher(static fn ($realRequest) => $realRequest->getContent() === 'test'); + + self::assertSame('callback(request)', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/UriMatcherTest.php b/tests/HttpClientMock/Matcher/UriMatcherTest.php new file mode 100644 index 0000000..f003e48 --- /dev/null +++ b/tests/HttpClientMock/Matcher/UriMatcherTest.php @@ -0,0 +1,164 @@ +expectException(UriContainsQueryParameters::class); + $this->expectExceptionMessage( + 'Given uri /query?foo=bar conts query parameters, use queryParam() calls instead.', + ); + + new UriMatcher('/query?foo=bar', new UriParams()); + } + + public function testMatchUri(): void + { + $matcher = new UriMatcher('/query', new UriParams()); + + $realRequest = $this->createRealRequest(uri: '/query'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(20, $result); + } + + public function testUriDoesNotMatch(): void + { + $matcher = new UriMatcher('/query', new UriParams()); + + $realRequest = $this->createRealRequest(uri: '/does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(0, $result); + } + + public function testMatchUriWithUriParams(): void + { + $matcher = new UriMatcher('/query/{tpl}', new UriParams(['tpl' => 'test'])); + + $realRequest = $this->createRealRequest(uri: '/query/test'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(20, $result); + } + + public function testMatchUriWithCallback(): void + { + $matcher = new UriMatcher(static fn ($uri) => $uri === '/query', new UriParams()); + + $realRequest = $this->createRealRequest(uri: '/query'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(20, $result); + } + + public function testMatchUriWithCallbackAndUriParams(): void + { + $matcher = new UriMatcher( + static fn ($uri, $params) => $uri === '/query/' . $params['id'], + new UriParams(['id' => 'test']), + ); + + $realRequest = $this->createRealRequest(uri: '/query/test'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(20, $result); + } + + public function testUriDoesNotMatchWithUriParams(): void + { + $matcher = new UriMatcher('/query/{tpl}', new UriParams(['tpl' => 'test'])); + + $realRequest = $this->createRealRequest(uri: '/query/does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(0, $result); + } + + public function testUriDoesNotMatchWithCallback(): void + { + $matcher = new UriMatcher(static fn ($uri) => $uri === '/query', new UriParams()); + + $realRequest = $this->createRealRequest(uri: '/does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(0, $result); + } + + public function testUriDoesNotMatchWithCallbackAndUriParams(): void + { + $matcher = new UriMatcher( + static fn ($uri, $params) => $uri === '/query/' . $params['id'], + new UriParams(['id' => 'test']), + ); + + $realRequest = $this->createRealRequest(uri: '/query/does-not-match'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertMatcher('uri', $result); + self::assertScore(0, $result); + } + + public function testToString(): void + { + $matcher = new UriMatcher('/query', new UriParams()); + + self::assertSame('request.uri === "/query"', (string) $matcher); + } + + public function testToStringWithCallback(): void + { + $matcher = new UriMatcher(static fn ($uri) => $uri === '/query', new UriParams()); + + self::assertSame('callback(request.uri, {}) !== false', (string) $matcher); + } + + public function testToStringWithCallbackAndParams(): void + { + $matcher = new UriMatcher(static fn ($uri) => $uri === '/query/{id}', new UriParams(['{id}' => 'abc'])); + + self::assertSame('callback(request.uri, {"{id}":"abc"}) !== false', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/Matcher/UriParamsTest.php b/tests/HttpClientMock/Matcher/UriParamsTest.php new file mode 100644 index 0000000..0ac88ad --- /dev/null +++ b/tests/HttpClientMock/Matcher/UriParamsTest.php @@ -0,0 +1,77 @@ + '1', 'b' => '2']); + + self::assertCount(2, $uriParams); + } + + public function testParamsCanBeChecked(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertTrue($uriParams->has('a')); + self::assertFalse($uriParams->has('c')); + } + + public function testParamsCanBeRetrieved(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertSame('1', $uriParams->get('a')); + self::assertNull($uriParams->get('c')); + } + + public function testParamsCanBeSet(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertFalse($uriParams->has('c')); + self::assertNull($uriParams->get('c')); + + $uriParams->set('c', '3'); + + self::assertTrue($uriParams->has('c')); + self::assertSame('3', $uriParams->get('c')); + } + + public function testParamsCanBeRetrievedAsArray(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertSame(['a' => '1', 'b' => '2'], $uriParams->toArray()); + } + + public function testParamsCanBeRetrievedAsJson(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertJsonStringEqualsJsonString('{"a":"1","b":"2"}', $uriParams->toJson()); + } + + public function testParamsCanBeReplacedOnString(): void + { + $uriParams = new UriParams(['a' => '1', 'b' => '2']); + + self::assertSame('foo 1 2', $uriParams->replace('foo {a} {b}')); + } +} diff --git a/tests/HttpClientMock/Matcher/XmlMatcherTest.php b/tests/HttpClientMock/Matcher/XmlMatcherTest.php new file mode 100644 index 0000000..f9d9c85 --- /dev/null +++ b/tests/HttpClientMock/Matcher/XmlMatcherTest.php @@ -0,0 +1,132 @@ +expectException(InvalidMockRequest::class); + + new XmlMatcher('abc'); + } + + public function testMatchXml(): void + { + $matcher = new XmlMatcher('abc'); + + $realRequest = $this->createRealRequest(content: 'abc'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('xml', $result); + } + + public function testMatchXmlWithCallback(): void + { + $matcher = new XmlMatcher(static fn ($xml) => $xml === 'abc'); + + $realRequest = $this->createRealRequest(content: 'abc'); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Hit::class, $result); + self::assertScore(5, $result); + self::assertMatcher('xml', $result); + } + + public function testMismatchXml(): void + { + $matcher = new XmlMatcher('abc'); + + $realRequest = $this->createRealRequest( + content: 'does-not-match', + ); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('xml', $result); + } + + public function testMismatchXmlWithCallback(): void + { + $matcher = new XmlMatcher(static fn ($xml) => $xml === 'abc'); + + $realRequest = $this->createRealRequest( + content: 'does-not-match', + ); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('xml', $result); + } + + public function testMismatchInvalidXml(): void + { + $matcher = new XmlMatcher('abc'); + + $realRequest = $this->createRealRequest( + content: 'does-not-match', + ); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('xml', $result); + } + + public function testMismatchInvalidXmlWithCallback(): void + { + $matcher = new XmlMatcher(static fn ($xml) => $xml === 'abc'); + + $realRequest = $this->createRealRequest( + content: 'does-not-match', + ); + + $result = $matcher($realRequest); + + self::assertInstanceOf(Mismatch::class, $result); + self::assertScore(0, $result); + self::assertMatcher('xml', $result); + } + + public function testToString(): void + { + $matcher = new XmlMatcher('abc'); + + self::assertSame( + 'request.content === "abc"', + (string) $matcher, + ); + } + + public function testToStringWithCallback(): void + { + $matcher = new XmlMatcher(static fn ($xml) => $xml === 'abc'); + + self::assertSame('callback(request.content) !== false', (string) $matcher); + } +} diff --git a/tests/HttpClientMock/MockRequestBuilderCollectionTest.php b/tests/HttpClientMock/MockRequestBuilderCollectionTest.php index f8790ca..c4f407b 100644 --- a/tests/HttpClientMock/MockRequestBuilderCollectionTest.php +++ b/tests/HttpClientMock/MockRequestBuilderCollectionTest.php @@ -6,17 +6,16 @@ use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection; -use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatch; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockResponseBuilder; -use Brainbits\FunctionalTestHelpers\HttpClientMock\SymfonyMockResponseFactory; +use Brainbits\FunctionalTestHelpers\HttpClientMock\RealRequest; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversClass(MockRequestBuilder::class)] #[CoversClass(MockRequestBuilderCollection::class)] -#[CoversClass(MockRequestMatch::class)] #[CoversClass(MockRequestMatcher::class)] final class MockRequestBuilderCollectionTest extends TestCase { @@ -28,70 +27,97 @@ public function setUp(): void { $this->builders = [ 'fallback' => (new MockRequestBuilder()) + ->name('fallback') ->willRespond(new MockResponseBuilder()), 'get' => (new MockRequestBuilder()) + ->name('get') ->method('GET') ->willRespond(new MockResponseBuilder()), 'post' => (new MockRequestBuilder()) + ->name('post') ->method('POST') ->willRespond(new MockResponseBuilder()), - 'foo' => (new MockRequestBuilder()) - ->uri('/foo') + 'get_uri_header' => (new MockRequestBuilder()) + ->name('header') + ->method('GET') + ->uri('/uri') + ->header('Accept', 'application/zip') + ->willRespond(new MockResponseBuilder()), + + 'only_uri' => (new MockRequestBuilder()) + ->name('uri') + ->uri('/only-uri') ->willRespond(new MockResponseBuilder()), - 'getBar' => (new MockRequestBuilder()) + 'get_uri' => (new MockRequestBuilder()) + ->name('get_uri') ->method('GET') - ->uri('/bar') + ->uri('/uri') ->willRespond(new MockResponseBuilder()), - 'getBarWithOneParam' => (new MockRequestBuilder()) + 'get_uri_param' => (new MockRequestBuilder()) + ->name('get_uri_param') ->method('GET') - ->uri('/bar?one=1') + ->uri('/uri') + ->queryParam('one', '1') ->willRespond(new MockResponseBuilder()), - 'getBarWithTwoParams' => (new MockRequestBuilder()) + 'get_uri_params' => (new MockRequestBuilder()) + ->name('get_uri_params') ->method('GET') - ->uri('/bar') + ->uri('/uri') ->queryParam('one', '1') ->queryParam('two', '2') ->willRespond(new MockResponseBuilder()), - 'postBarJson' => (new MockRequestBuilder()) + 'post_uri_json' => (new MockRequestBuilder()) + ->name('post_uri_json') ->method('POST') - ->uri('/bar') + ->uri('/uri') ->json(['json' => 'data']) ->willRespond(new MockResponseBuilder()), - 'postBarWithOneParam' => (new MockRequestBuilder()) + 'post_uri_xml' => (new MockRequestBuilder()) + ->name('post_uri_xml') ->method('POST') - ->uri('/bar') + ->uri('/uri') + ->xml('test') + ->willRespond(new MockResponseBuilder()), + + 'post_uri_param' => (new MockRequestBuilder()) + ->name('post_uri_param') + ->method('POST') + ->uri('/uri') ->requestParam('one', '1') ->willRespond(new MockResponseBuilder()), - 'postBarWithTwoParams' => (new MockRequestBuilder()) + 'post_uri_params' => (new MockRequestBuilder()) + ->name('post_uri_params') ->method('POST') - ->uri('/bar') + ->uri('/uri') ->requestParam('one', '1') ->requestParam('two', '2') ->willRespond(new MockResponseBuilder()), - 'postBarWithContent' => (new MockRequestBuilder()) + 'post_uri_content' => (new MockRequestBuilder()) + ->name('post_uri_content') ->method('POST') - ->uri('/bar') + ->uri('/uri') ->content('content') ->willRespond(new MockResponseBuilder()), - 'postBarWithMultipart' => (new MockRequestBuilder()) + 'post_uri_multipart' => (new MockRequestBuilder()) + ->name('post_uri_multipart') ->method('POST') - ->uri('/barx') + ->uri('/uri') ->multipart('key', 'application/octet-stream', null, 'content') ->willRespond(new MockResponseBuilder()), ]; - $this->collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $this->collection = new MockRequestBuilderCollection(); foreach ($this->builders as $builder) { $this->collection->addMockRequestBuilder($builder); } @@ -101,50 +127,144 @@ public function setUp(): void #[DataProvider('requests')] public function testRequestMatching(string $method, string $uri, array $options, string $index): void { - ($this->collection)($method, $uri, $options); + $x = ($this->collection)($method, $uri, $options); $expectedMockRequestBuilder = $this->builders[$index]; self::assertFalse($expectedMockRequestBuilder->getCallStack()->isEmpty()); } + public function testOnMatch(): void + { + $called = false; + + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder( + (new MockRequestBuilder()) + ->method('GET') + ->uri('/query') + ->onMatch(static function () use (&$called): void { + $called = true; + }) + ->willRespond(new MockResponseBuilder()), + ); + + $collection('GET', '/query', []); + + self::assertTrue($called, 'onMatch() was not called'); + } + + public function testAssertContent(): void + { + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder( + (new MockRequestBuilder()) + ->assertContent(function (string $content): void { + $this->assertSame('this is content', $content); + }) + ->willRespond(new MockResponseBuilder()), + ); + + $collection('GET', '/query', ['body' => 'this is content']); + } + + public function testAssertContentFails(): void + { + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder( + (new MockRequestBuilder()) + ->assertContent(function (string $content): void { + $this->assertSame('this is content', $content); + }) + ->willRespond(new MockResponseBuilder()), + ); + + try { + $collection('GET', '/query', ['body' => 'does-not-match']); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion was not thrown'); + } + + public function testAssertThat(): void + { + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder( + (new MockRequestBuilder()) + ->assertThat(function (RealRequest $realRequest): void { + $this->assertSame('this is content', $realRequest->getContent()); + }) + ->willRespond(new MockResponseBuilder()), + ); + + $collection('GET', '/query', ['body' => 'this is content']); + } + + public function testAssertThatFails(): void + { + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder( + (new MockRequestBuilder()) + ->assertThat(function (RealRequest $realRequest): void { + $this->assertSame('this is content', $realRequest->getContent()); + }) + ->willRespond(new MockResponseBuilder()), + ); + + try { + $collection('GET', '/query', ['body' => 'does-not-match']); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion was not thrown'); + } + /** @return mixed[] */ public static function requests(): array { return [ - ['DELETE', '/baz', [], 'fallback'], - ['GET', '/baz', [], 'get'], - ['POST', '/baz', [], 'post'], - ['GET', '/foo', [], 'foo'], - ['POST', '/foo', [], 'foo'], - ['DELETE', '/foo', [], 'foo'], - ['GET', '/bar', [], 'getBar'], - ['GET', '/bar?one=1', [], 'getBarWithOneParam'], - ['GET', '/bar?one=1&two=2', [], 'getBarWithTwoParams'], - ['GET', '/bar', [], 'getBar'], - ['POST', '/bar', [], 'post'], - ['POST', '/bar', ['json' => ['json' => 'data']], 'postBarJson'], - 'postBarWithOneParam' => [ + 'delete' => ['DELETE', '/not-matched', [], 'fallback'], + 'get' => ['GET', '/not-matched', [], 'get'], + 'post' => ['POST', '/not-matched', [], 'post'], + 'getUriHeader' => ['GET', '/uri', ['headers' => ['Accept: application/zip']], 'get_uri_header'], + 'getOnlyUri' => ['GET', '/only-uri', [], 'only_uri'], + 'postOnlyUri' => ['POST', '/only-uri', [], 'only_uri'], + 'deleteOnlyUri' => ['DELETE', '/only-uri', [], 'only_uri'], + 'getUri' => ['GET', '/uri', [], 'get_uri'], + 'getUriWithOneParam' => ['GET', '/uri?one=1', [], 'get_uri_param'], + 'getUriWithTwoParams' => ['GET', '/uri?one=1&two=2', [], 'get_uri_params'], + 'postUri' => ['POST', '/uri', [], 'post'], + 'postUriJson' => ['POST', '/uri', ['json' => ['json' => 'data']], 'post_uri_json'], + 'postUriXml' => [ + 'POST', + '/uri', + ['body' => 'test', 'headers' => ['Content-Type: text/xml']], + 'post_uri_xml', + ], + 'postUriWithOneParam' => [ 'POST', - '/bar', + '/uri', ['body' => 'one=1', 'headers' => ['Content-Type: application/x-www-form-urlencoded']], - 'postBarWithOneParam', + 'post_uri_param', ], - 'postBarWithTwoParams' => [ + 'postUriWithTwoParams' => [ 'POST', - '/bar', + '/uri', ['body' => 'one=1&two=2', 'headers' => ['Content-Type: application/x-www-form-urlencoded']], - 'postBarWithTwoParams', + 'post_uri_params', ], - 'postBarWithContent' => [ + 'postUriWithContent' => [ 'POST', - '/bar', + '/uri', ['body' => 'content', 'headers' => ['Content-Type: application/x-www-form-urlencoded']], - 'postBarWithContent', + 'post_uri_content', ], - 'postBarWithMultipart' => [ + 'postUriWithMultipart' => [ 'POST', - '/barx', + '/uri', [ 'body' => <<<'BODY' --12345 @@ -155,7 +275,7 @@ public static function requests(): array BODY, 'headers' => ['Content-Type: multipart/form-data; boundary=12345'], ], - 'postBarWithMultipart', + 'post_uri_multipart', ], ]; } diff --git a/tests/HttpClientMock/MockRequestBuilderFactoryTest.php b/tests/HttpClientMock/MockRequestBuilderFactoryTest.php deleted file mode 100644 index 3d0b5d7..0000000 --- a/tests/HttpClientMock/MockRequestBuilderFactoryTest.php +++ /dev/null @@ -1,80 +0,0 @@ -mockRequestBuilderFactory = new MockRequestBuilderFactory(); - } - - public function testBuildsRequestWithoutBody(): void - { - $options = [ - 'headers' => ['Content-Type: application/json'], - 'json' => ['foo' => 'bar'], - ]; - $request = ($this->mockRequestBuilderFactory)('POST', 'https://service.com', $options); - - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://service.com', $request->getUri()); - self::assertSame(['foo' => 'bar'], $request->getJson()); - } - - public function testBuildsRequestWithJsonInBody(): void - { - $options = [ - 'headers' => [ - 'Content-Length: 1', - 'Content-Type: application/json', - ], - 'body' => '{"foo": "bar"}', - ]; - - $request = ($this->mockRequestBuilderFactory)('POST', 'https://service.com', $options); - - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://service.com', $request->getUri()); - self::assertSame(['foo' => 'bar'], $request->getJson()); - self::assertTrue($request->isJson()); - } - - public function testBuildsRequestWithCallableInBody(): void - { - $size = 1; - $body = $this->getMockBuilder(StreamInterface::class)->getMock(); - $body - ->expects(self::once()) - ->method('read') - ->with($size) - ->willReturn('{"foo": "bar"}'); - - $options = [ - 'headers' => [ - sprintf('Content-Length: %d', $size), - 'Content-Type: application/json', - ], - 'body' => static fn (int $size) => $body->read($size), - ]; - - $request = ($this->mockRequestBuilderFactory)('POST', 'https://service.com', $options); - - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://service.com', $request->getUri()); - self::assertSame(['foo' => 'bar'], $request->getJson()); - self::assertTrue($request->isJson()); - } -} diff --git a/tests/HttpClientMock/MockRequestBuilderTest.php b/tests/HttpClientMock/MockRequestBuilderTest.php index b6e7921..b3857b0 100644 --- a/tests/HttpClientMock/MockRequestBuilderTest.php +++ b/tests/HttpClientMock/MockRequestBuilderTest.php @@ -7,18 +7,32 @@ use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\AddMockResponseFailed; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\InvalidMockRequest; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\NoResponseMock; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\HeaderMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\JsonMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MethodMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MultipartMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\QueryParamMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\ThatMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\UriMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\UriParams; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\XmlMatcher; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder; +use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockResponseBuilder; +use Brainbits\FunctionalTestHelpers\HttpClientMock\RealRequest; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use RuntimeException; use Symfony\Component\HttpFoundation\File\File; -use function Safe\file_get_contents; +use const PHP_EOL; #[CoversClass(MockRequestBuilder::class)] final class MockRequestBuilderTest extends TestCase { + use RealRequestTrait; + public function testWithoutAnythingSpecifiedARequestIsEmpty(): void { $mockRequestBuilder = new MockRequestBuilder(); @@ -26,118 +40,275 @@ public function testWithoutAnythingSpecifiedARequestIsEmpty(): void self::assertTrue($mockRequestBuilder->isEmpty()); } - public function testWithRequestParametersARequestIsNotEmpty(): void + public function testWithMatcherIsNotEmpty(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->requestParam('one', '1'); + $mockRequestBuilder->method('GET'); self::assertFalse($mockRequestBuilder->isEmpty()); } - public function testWithoutContentNoRequestParametersExists(): void + public function testMethod(): void { $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->method('GET'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertFalse($mockRequestBuilder->hasRequestParams()); + self::assertEquals(new MockRequestMatcher(null, [new MethodMatcher('GET')]), $matcher); } - public function testWithNotDecodableContentNoRequestParametersExists(): void + public function testUri(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->content('no form data'); + $mockRequestBuilder->uri('/query'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertFalse($mockRequestBuilder->hasRequestParams()); + self::assertEquals( + new MockRequestMatcher(null, [new UriMatcher('/query', new UriParams())]), + $matcher, + ); } - public function testWithJsonContentNoRequestParametersExists(): void + public function testUriWithUriParam(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->json(['value' => 'key=value']); + $mockRequestBuilder->uri('/query/{tpl}'); + $mockRequestBuilder->uriParam('tpl', 'value'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertFalse($mockRequestBuilder->hasRequestParams()); + self::assertEquals( + new MockRequestMatcher(null, [new UriMatcher('/query/{tpl}', new UriParams(['tpl' => 'value']))]), + $matcher, + ); } - public function testRequestMayHaveOneRequestParameter(): void + public function testQueryParam(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->requestParam('one', '1'); + $mockRequestBuilder->queryParam('filter', 'firstname'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertTrue($mockRequestBuilder->hasRequestParams()); - self::assertSame(['one' => '1'], $mockRequestBuilder->getRequestParams()); + self::assertEquals( + new MockRequestMatcher(null, [new QueryParamMatcher('filter', 'firstname', [])]), + $matcher, + ); } - public function testRequestMayHaveMultipleRequestParameters(): void + public function testMultipleQueryParams(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->requestParam('one', '1'); - $mockRequestBuilder->requestParam('two', '2'); - $mockRequestBuilder->requestParam('three', '3'); + $mockRequestBuilder->queryParam('filter', 'firstname'); + $mockRequestBuilder->queryParam('orderBy', 'lastname'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertTrue($mockRequestBuilder->hasRequestParams()); - self::assertSame(['one' => '1', 'two' => '2', 'three' => '3'], $mockRequestBuilder->getRequestParams()); + self::assertEquals( + new MockRequestMatcher( + null, + [ + new QueryParamMatcher('filter', 'firstname', []), + new QueryParamMatcher('orderBy', 'lastname', []), + ], + ), + $matcher, + ); } - public function testRequestParameterValuesMayBeEmpty(): void + public function testXml(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->requestParam('one', '1'); - $mockRequestBuilder->requestParam('empty', ''); - $mockRequestBuilder->requestParam('three', '3'); + $mockRequestBuilder->xml('abc'); + + $matcher = $mockRequestBuilder->getMatcher(); + + self::assertEquals( + new MockRequestMatcher(null, [new XmlMatcher('abc')]), + $matcher, + ); + } + + public function testXmlWithInvalidXmlThrowsException(): void + { + $this->expectException(InvalidMockRequest::class); + $this->expectExceptionMessage('No valid xml: foo'); - self::assertTrue($mockRequestBuilder->hasRequestParams()); - self::assertSame(['one' => '1', 'empty' => '', 'three' => '3'], $mockRequestBuilder->getRequestParams()); + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->xml('foo'); } - public function testUriIsOptional(): void + public function testJson(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->uri(null); + $mockRequestBuilder->json(['firstname' => 'peter']); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertFalse($mockRequestBuilder->hasUri(), 'no uri'); - self::assertFalse($mockRequestBuilder->hasQueryParams(), 'no query parameters'); + self::assertEquals( + new MockRequestMatcher(null, [new JsonMatcher(['firstname' => 'peter'])]), + $matcher, + ); } - public function testParsesQueryParamsInUri(): void + public function testWithBasicAuthentication(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->uri('http://example.org?one=1&two=2'); + $mockRequestBuilder->basicAuthentication('username', 'password'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertSame(['one' => '1', 'two' => '2'], $mockRequestBuilder->getQueryParams()); + self::assertEquals( + new MockRequestMatcher(null, [new HeaderMatcher('Authorization', 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')]), + $matcher, + ); } - public function testSupportsEncodesQueryParamsInUri(): void + public function testWithMultipart(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->uri('http://example.org?space=%20"e=%22'); + $mockRequestBuilder->multipart('key', 'mimetype', 'filename', 'content'); + + $matcher = $mockRequestBuilder->getMatcher(); - self::assertSame(['space' => ' ', 'quote' => '"'], $mockRequestBuilder->getQueryParams()); + self::assertEquals( + new MockRequestMatcher(null, [new MultipartMatcher('key', 'mimetype', 'filename', 'content')]), + $matcher, + ); } - public function testSupportsQueryParamWithoutValue(): void + public function testWithMultipartFromFile(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->uri('http://example.org?key'); + $mockRequestBuilder->multipartFromFile('key', new File(__DIR__ . '/../files/test.txt')); - self::assertSame(['key' => ''], $mockRequestBuilder->getQueryParams()); + $matcher = $mockRequestBuilder->getMatcher(); + + self::assertEquals( + // phpcs:ignore Generic.Files.LineLength.TooLong + new MockRequestMatcher(null, [new MultipartMatcher('key', 'text/plain', 'test.txt', 'this is a txt file' . PHP_EOL)]), + $matcher, + ); } - public function testSupportsQueryParams(): void + public function testThat(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->queryParam('one', '1'); - $mockRequestBuilder->queryParam('two', '2'); - $mockRequestBuilder->queryParam('three', '%s %s %s', 'a', 'b', 'c'); + $mockRequestBuilder->that(static fn ($request) => true); - self::assertSame(['one' => '1', 'two' => '2', 'three' => 'a b c'], $mockRequestBuilder->getQueryParams()); + $matcher = $mockRequestBuilder->getMatcher(); + + self::assertEquals( + // phpcs:ignore Generic.Files.LineLength.TooLong + new MockRequestMatcher(null, [new ThatMatcher(static fn ($request) => true)]), + $matcher, + ); } - public function testIgnoresEmptyQueryString(): void + public function testAssertMethod(): void { $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->uri('http://example.org?'); + $mockRequestBuilder->assertMethod(function (string $method): void { + $this->assertSame('POST', $method); + }); - self::assertNull($mockRequestBuilder->getQueryParams()); - self::assertSame('http://example.org', $mockRequestBuilder->getUri()); + $mockRequestBuilder->assert($this->createRealRequest(method: 'POST')); + } + + public function testAssertMethodFails(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertUri(function (string $method): void { + $this->assertSame('does-not-match', $method); + }); + + try { + $mockRequestBuilder->assert($this->createRealRequest(method: 'POST')); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion error was not thrown'); + } + + public function testAssertUri(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertUri(function (string $uri): void { + $this->assertSame('/query', $uri); + }); + + $mockRequestBuilder->assert($this->createRealRequest(uri: '/query')); + } + + public function testAssertUriFails(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertUri(function (string $content): void { + $this->assertSame('does-not-match', $content); + }); + + try { + $mockRequestBuilder->assert($this->createRealRequest(uri: '/query')); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion error was not thrown'); + } + + public function testAssertContent(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertContent(function (string $content): void { + $this->assertSame('this is content', $content); + }); + + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + } + + public function testAssertContentFails(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertContent(function (string $content): void { + $this->assertSame('does-not-match', $content); + }); + + try { + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion error was not thrown'); + } + + public function testAssertThat(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertThat(function (RealRequest $realRequest): void { + $this->assertSame('this is content', $realRequest->getContent()); + }); + + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + } + + public function testAssertThatFails(): void + { + $mockRequestBuilder = new MockRequestBuilder(); + $mockRequestBuilder->assertThat(function (RealRequest $realRequest): void { + $this->assertSame('does-not-match', $realRequest->getContent()); + }); + + try { + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion error was not thrown'); } public function testEmptyResponsesThrowsException(): void @@ -228,102 +399,4 @@ public function testResponseBuilderIsResettable(): void self::assertFalse($mockRequestBuilder->hasResponse()); } - - public function testXmlStringsAreDetectedAsXml(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->content('abc'); - - self::assertTrue($mockRequestBuilder->isXml()); - self::assertFalse($mockRequestBuilder->isEmpty()); - self::assertFalse($mockRequestBuilder->isJson()); - } - - public function testXmlStringsAreAccessibleAsSimpleXmlObjects(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->content('abc'); - - self::assertSame('abc', (string) $mockRequestBuilder->getXml()->first); - } - - public function testXmlStringsWithNamespacesAreAccessibleAsSimpleXmlObjects(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->content('abc'); - - $xml = $mockRequestBuilder->getXml(['x' => 'http://example.org/xml']); - - self::assertSame('abc', (string) $xml->xpath('//x:first')[0]); - } - - public function testWithJsonContent(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->json(['value' => 'key=value']); - - self::assertTrue($mockRequestBuilder->isJson()); - self::assertSame(['value' => 'key=value'], $mockRequestBuilder->getJson()); - } - - public function testWithInvalidXmlThrowsException(): void - { - $this->expectException(InvalidMockRequest::class); - $this->expectExceptionMessage('No valid xml: foo'); - - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->xml('foo'); - } - - public function testWithXmlContent(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->xml(''); - - self::assertTrue($mockRequestBuilder->isXml()); - self::assertXmlStringEqualsXmlString('', $mockRequestBuilder->getXml()->saveXML()); - } - - public function testWithBasicAuthentication(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->basicAuthentication('username', 'password'); - - $this->assertSame('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $mockRequestBuilder->getHeader('Authorization')); - } - - public function testWithMultipart(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->multipart('key', 'mimetype', 'filename', 'content'); - - $this->assertTrue($mockRequestBuilder->hasMultiparts()); - $this->assertSame( - [ - 'key' => [ - 'mimetype' => 'mimetype', - 'filename' => 'filename', - 'content' => 'content', - ], - ], - $mockRequestBuilder->getMultiparts(), - ); - } - - public function testWithMultipartFromFile(): void - { - $mockRequestBuilder = new MockRequestBuilder(); - $mockRequestBuilder->multipartFromFile('key', new File(__DIR__ . '/../files/test.zip')); - - $this->assertSame( - [ - 'key' => [ - 'mimetype' => 'application/zip', - 'filename' => 'test.zip', - 'content' => file_get_contents(__DIR__ . '/../files/test.zip'), - ], - ], - $mockRequestBuilder->getMultiparts(), - ); - } } diff --git a/tests/HttpClientMock/MockRequestMatcherTest.php b/tests/HttpClientMock/MockRequestMatcherTest.php index 929ec5b..e1ac45f 100644 --- a/tests/HttpClientMock/MockRequestMatcherTest.php +++ b/tests/HttpClientMock/MockRequestMatcherTest.php @@ -4,8 +4,6 @@ namespace Brainbits\FunctionalTestHelpers\Tests\HttpClientMock; -use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder; -use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatch; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -13,142 +11,13 @@ #[CoversClass(MockRequestMatcher::class)] final class MockRequestMatcherTest extends TestCase { - private MockRequestMatcher $matcher; - private MockRequestBuilder $expectation; - private MockRequestBuilder $realRequest; + use RealRequestTrait; - protected function setUp(): void + public function testIt(): void { - $this->matcher = new MockRequestMatcher(); + $matcher = new MockRequestMatcher('test', []); + $result = ($matcher)($this->createRealRequest()); - $this->expectation = new MockRequestBuilder(); - $this->realRequest = new MockRequestBuilder(); - } - - public function testDetectMatchingRequestParameters(): void - { - $this->expectation->requestParam('one', '1'); - $this->expectation->requestParam('two', '2'); - - $this->realRequest->requestParam('two', '2'); - $this->realRequest->requestParam('one', '1'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(5, $match); - } - - public function testDetectUriWithString(): void - { - $this->expectation->uri('/host'); - $this->realRequest->uri('/host'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(20, $match); - } - - public function testUriDoesNotMatchWithString(): void - { - $this->expectation->uri('/host'); - $this->realRequest->uri('/does-not-match'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(0, $match); - } - - public function testDetectUriWithCallback(): void - { - $this->expectation->uri(static fn ($uri) => $uri === '/host'); - $this->realRequest->uri('/host'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(20, $match); - } - - public function testUriDoesNotMatchWithCallback(): void - { - $this->expectation->uri(static fn ($uri) => $uri === '/host'); - $this->realRequest->uri('/does-not-match'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(0, $match); - } - - public function testMatchesWithSameMultiparts(): void - { - $this->expectation->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - $this->realRequest->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(5, $match); - } - - public function testDoesNotMatchWithDifferentMultiparts(): void - { - $this->expectation->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - $this->realRequest->multipart('wrong_file', 'application/pdf', 'file.pdf', 'pdf'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(0, $match); - // phpcs:ignore Generic.Files.LineLength.TooLong - self::assertReason('Mismatching multiparts, expected {"file":{"mimetype":"application\/pdf","filename":"file.pdf","content":"pdf"}}, got {"wrong_file":{"mimetype":"application\/pdf","filename":"file.pdf","content":"pdf"}}', $match); - } - - public function testMatchesWithIncompleteMultiparts(): void - { - $this->expectation->multipart('file'); - $this->realRequest->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(5, $match); - } - - public function testMatchesWithoutMimetype(): void - { - $this->expectation->multipart('file', null, 'file.pdf', 'pdf'); - $this->realRequest->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(5, $match); - } - - public function testMatchesWithoutFilename(): void - { - $this->expectation->multipart('file', null, null, 'pdf'); - $this->realRequest->multipart('file', 'application/pdf', 'file.pdf', 'pdf'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(5, $match); - } - - public function testDoesNotMatchWithIncompleteMultiparts(): void - { - $this->expectation->multipart('file', null, null, 'pdf'); - $this->realRequest->multipart('file', 'application/pdf', 'file.pdf', 'foo'); - - $match = ($this->matcher)($this->expectation, $this->realRequest); - - self::assertMatchScoreIs(0, $match); - // phpcs:ignore Generic.Files.LineLength.TooLong - self::assertReason('Mismatching multiparts, expected {"file":{"content":"pdf"}}, got {"file":{"content":"foo"}}', $match); - } - - private static function assertMatchScoreIs(int $expected, MockRequestMatch $match): void - { - self::assertSame($expected, $match->getScore()); - } - - private static function assertReason(string $reason, MockRequestMatch $match): void - { - self::assertSame($reason, $match->getReason()); + $this->assertSame('test', $result->getName()); } } diff --git a/tests/HttpClientMock/MockRequestResolverTest.php b/tests/HttpClientMock/MockRequestResolverTest.php index dde5ee6..f1fe9f4 100644 --- a/tests/HttpClientMock/MockRequestResolverTest.php +++ b/tests/HttpClientMock/MockRequestResolverTest.php @@ -9,39 +9,216 @@ use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestResolver; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockResponseBuilder; -use Brainbits\FunctionalTestHelpers\HttpClientMock\SymfonyMockResponseFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; #[CoversClass(MockRequestResolver::class)] final class MockRequestResolverTest extends TestCase { + use RealRequestTrait; + public function testEmptyCollection(): void { $this->expectException(NoMatchingMockRequest::class); + $this->expectExceptionMessage('No mock request builders given for: +GET /query'); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar'); + $realRequest = $this->createRealRequest('GET', '/query'); + + $collection = new MockRequestBuilderCollection(); + + (new MockRequestResolver())($collection, $realRequest); + } - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + public function testNoMatchWithMissingKeys(): void + { + $this->expectException(NoMatchingMockRequest::class); + $this->expectExceptionMessage(<<<'MSG' + No matching mock request builder found for: + GET /does-not-match + + Mock request builders: + #1 test-with-missing + ✘ method "GET" does not match "POST" (0) + ✘ uri "/does-not-match" does not match "/query" (0) + ✘ header accept missing (0) + ✘ queryParam filter missing (0) + ✘ requestParam firstname missing (0) + ✘ multipart file missing (0) + ✘ content "NULL" does not match "this is plain text" (0) + MSG); + + $requestBuilder1 = (new MockRequestBuilder()) + ->name('test-with-missing') + ->method('POST') + ->uri('/query') + ->header('Accept', 'text/plain') + ->queryParam('filter', 'lastname') + ->requestParam('firstname', 'tester') + ->multipart('file') + ->content('this is plain text'); + + $realRequest = $this->createRealRequest('GET', '/does-not-match'); + + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder($requestBuilder1); (new MockRequestResolver())($collection, $realRequest); } - public function testNoMatch(): void + public function testNoMatchWithMismatchingKeys(): void { + // phpcs:disable Generic.Files.LineLength.TooLong $this->expectException(NoMatchingMockRequest::class); + $this->expectExceptionMessage(<<<'MSG' + No matching mock request builder found for: + GET /query?filter=firstname + accept: text/csv + &firstname=peter + + Mock request builders: + #1 test-with-mismatch + ✔ method matches "GET" (10) + ✔ uri matches "/query" (20) + ✘ header accept "text/csv" does not match "text/plain" (0) + ✘ queryParam filter "firstname" does not match "lastname" (0) + ✘ requestParam firstname "peter" does not match "tester" (0) + ✘ multipart file "{"name":"file","mimetype":null}" does not match "{"name":"file","mimetype":"picture.jpg"}" (0) + ✘ content "text" does not match "this is plain text" (0) + MSG); + // phpcs:enable Generic.Files.LineLength.TooLong $requestBuilder1 = (new MockRequestBuilder()) + ->name('test-with-mismatch') ->method('GET') - ->uri('/foo'); + ->uri('/query') + ->header('Accept', 'text/plain') + ->queryParam('filter', 'lastname') + ->requestParam('firstname', 'tester') + ->multipart('file', 'picture.jpg') + ->content('this is plain text'); + + $realRequest = $this->createRealRequest( + 'GET', + '/query', + headers: ['Accept' => 'text/csv'], + content: 'text', + queryParams: ['filter' => 'firstname'], + requestParams: ['firstname' => 'peter'], + multiparts: ['file' => ['name' => 'file', 'filename' => 'wrong.jpg']], + ); + + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder($requestBuilder1); + + (new MockRequestResolver())($collection, $realRequest); + } - $realRequest = (new MockRequestBuilder()) + public function testNoMatchWithContent(): void + { + $this->expectException(NoMatchingMockRequest::class); + $this->expectExceptionMessage(<<<'MSG' + No matching mock request builder found for: + GET /query?filter=lastname + accept: text/plain + this is non-matching-text + + Mock request builders: + #1 test-with-content + ✔ method matches "GET" (10) + ✔ uri matches "/query" (20) + ✔ header accept matches "text/plain" (5) + ✔ queryParam filter matches "lastname" (5) + ✘ content "this is non-matching-text" does not match "this is plain text" (0) + MSG); + + $requestBuilder1 = (new MockRequestBuilder()) + ->name('test-with-content') ->method('GET') - ->uri('/bar'); + ->uri('/query') + ->header('Accept', 'text/plain') + ->queryParam('filter', 'lastname') + ->content('this is plain text'); + + $realRequest = $this->createRealRequest( + 'GET', + '/query', + headers: ['Accept' => 'text/plain'], + content: 'this is non-matching-text', + queryParams: ['filter' => 'lastname'], + ); + + $collection = new MockRequestBuilderCollection(); + $collection->addMockRequestBuilder($requestBuilder1); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + (new MockRequestResolver())($collection, $realRequest); + } + + public function testNoMatchWithJson(): void + { + // phpcs:disable Generic.Files.LineLength.TooLong + $this->expectException(NoMatchingMockRequest::class); + $this->expectExceptionMessage(<<<'MSG' + No matching mock request builder found for: + GET /query?filter=lastname + accept: text/plain + {"firstname":"peter","lastname":"peterson","address":{"street":"bobstreet 1","zip":"12345","city":"peterstown"}} + + Mock request builders: + #1 test-with-content + ✔ method matches "GET" (10) + ✔ uri matches "/query" (20) + ✔ header accept matches "text/plain" (5) + ✔ queryParam filter matches "lastname" (5) + ✘ json "{"firstname":"peter","lastname":"peterson","address":{"street":"bobstreet 1","zip":"12345","city":"peterstown"}}" does not match "{"firstname":"peter","lastname":"peterson","address":{"street":"peterstreet 1","zip":"12345","city":"peterstown"}}" (0) + --- Expected + +++ Actual + @@ @@ + "firstname": "peter", + "lastname": "peterson", + "address": { + - "street": "peterstreet 1", + + "street": "bobstreet 1", + "zip": "12345", + "city": "peterstown" + } + } + MSG); + // phpcs:enable Generic.Files.LineLength.TooLong + + $requestBuilder1 = (new MockRequestBuilder()) + ->name('test-with-content') + ->method('GET') + ->uri('/query') + ->header('Accept', 'text/plain') + ->queryParam('filter', 'lastname') + ->json([ + 'firstname' => 'peter', + 'lastname' => 'peterson', + 'address' => [ + 'street' => 'peterstreet 1', + 'zip' => '12345', + 'city' => 'peterstown', + ], + ]); + + $realRequest = $this->createRealRequest( + 'GET', + '/query', + headers: ['Accept' => 'text/plain'], + json: [ + 'firstname' => 'peter', + 'lastname' => 'peterson', + 'address' => [ + 'street' => 'bobstreet 1', + 'zip' => '12345', + 'city' => 'peterstown', + ], + ], + queryParams: ['filter' => 'lastname'], + ); + + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); (new MockRequestResolver())($collection, $realRequest); @@ -51,13 +228,11 @@ public function testMatch(): void { $requestBuilder1 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar'); + ->uri('/query'); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar'); + $realRequest = $this->createRealRequest('GET', '/query'); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $resultRequestBuilder = (new MockRequestResolver())($collection, $realRequest); @@ -65,23 +240,21 @@ public function testMatch(): void self::assertSame($requestBuilder1, $resultRequestBuilder); } - public function testMultipleMatchWithResponses(): void + public function testMultipleMatchesWithResponses(): void { $requestBuilder1 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar') + ->uri('/query') ->willRespond(new MockResponseBuilder()); $requestBuilder2 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar') + ->uri('/query') ->willRespond(new MockResponseBuilder()); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar'); + $realRequest = $this->createRealRequest('GET', '/query'); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $collection->addMockRequestBuilder($requestBuilder2); @@ -94,14 +267,11 @@ public function testMatchWithSoftMatching(): void { $requestBuilder1 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar'); + ->uri('/query'); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar') - ->queryParam('foo', '1337'); + $realRequest = $this->createRealRequest('GET', '/query', queryParams: ['foo' => '1337']); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $resultRequestBuilder = (new MockRequestResolver())($collection, $realRequest); @@ -113,19 +283,16 @@ public function testBestMatch(): void { $requestBuilder1 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar'); + ->uri('/query'); $requestBuilder2 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar') + ->uri('/query') ->queryParam('foo', '1337'); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar') - ->queryParam('foo', '1337'); + $realRequest = $this->createRealRequest('GET', '/query', queryParams: ['foo' => '1337']); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $collection->addMockRequestBuilder($requestBuilder2); @@ -138,19 +305,17 @@ public function testMatchWithProcessedRequest(): void { $requestBuilder1 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar') + ->uri('/query') ->willRespond(new MockResponseBuilder()); $requestBuilder2 = (new MockRequestBuilder()) ->method('GET') - ->uri('/bar') + ->uri('/query') ->willRespond(new MockResponseBuilder()); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar'); + $realRequest = $this->createRealRequest('GET', '/query'); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $collection->addMockRequestBuilder($requestBuilder2); @@ -164,22 +329,21 @@ public function testMatchWithProcessedRequest(): void public function testBestMatchWithProcessedRequest(): void { $requestBuilder1 = (new MockRequestBuilder()) + ->name('one') ->method('GET') - ->uri('/bar') + ->uri('/query') ->queryParam('foo', '1337') ->willRespond(new MockResponseBuilder()); $requestBuilder2 = (new MockRequestBuilder()) + ->name('two') ->method('GET') - ->uri('/bar') + ->uri('/query') ->willRespond(new MockResponseBuilder()); - $realRequest = (new MockRequestBuilder()) - ->method('GET') - ->uri('/bar') - ->queryParam('foo', '1337'); + $realRequest = $this->createRealRequest('GET', '/query', queryParams: ['foo' => '1337']); - $collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory()); + $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder($requestBuilder1); $collection->addMockRequestBuilder($requestBuilder2); @@ -187,6 +351,6 @@ public function testBestMatchWithProcessedRequest(): void $resultRequestBuilder = (new MockRequestResolver())($collection, $realRequest); - self::assertSame($requestBuilder1, $resultRequestBuilder); + self::assertSame($requestBuilder2, $resultRequestBuilder); } } diff --git a/tests/HttpClientMock/MockResponseBuilderTest.php b/tests/HttpClientMock/MockResponseBuilderTest.php index 770324f..0afe6ae 100644 --- a/tests/HttpClientMock/MockResponseBuilderTest.php +++ b/tests/HttpClientMock/MockResponseBuilderTest.php @@ -5,8 +5,11 @@ namespace Brainbits\FunctionalTestHelpers\Tests\HttpClientMock; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockResponseBuilder; +use Brainbits\FunctionalTestHelpers\HttpClientMock\RealRequest; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use function implode; @@ -15,6 +18,111 @@ #[CoversClass(MockResponseBuilder::class)] final class MockResponseBuilderTest extends TestCase { + use RealRequestTrait; + + public function testBuildContentResponse(): void + { + $builder = (new MockResponseBuilder()) + ->code(202) + ->contentType('text/plain') + ->contentLength(12) + ->etag('plain-content') + ->content('this is text'); + + $client = new MockHttpClient($builder->getResponse($this->createRealRequest())); + $response = $client->request('GET', '/query'); + + $this->assertSame(202, $response->getStatusCode()); + $this->assertSame('this is text', $response->getContent()); + $headers = $response->getHeaders(); + $this->assertSame(['plain-content'], $headers['etag'] ?? false); + $this->assertSame(['12'], $headers['content-length'] ?? false); + $this->assertSame(['text/plain'], $headers['content-type'] ?? false); + } + + public function testBuildJsonResponse(): void + { + $builder = (new MockResponseBuilder()) + ->code(203) + ->etag('json-content') + ->json(['foo' => 'bar']); + + $client = new MockHttpClient($builder->getResponse($this->createRealRequest())); + $response = $client->request('GET', '/query'); + + $this->assertSame(203, $response->getStatusCode()); + $this->assertSame('{"foo":"bar"}', $response->getContent()); + $headers = $response->getHeaders(); + $this->assertSame(['json-content'], $headers['etag'] ?? false); + $this->assertSame(['application/json'], $headers['content-type'] ?? false); + } + + public function testBuildXmlResponse(): void + { + $builder = (new MockResponseBuilder()) + ->code(204) + ->etag('xml-content') + ->xml('bar'); + + $client = new MockHttpClient($builder->getResponse($this->createRealRequest())); + $response = $client->request('GET', '/query'); + + $this->assertSame(204, $response->getStatusCode()); + $this->assertSame('bar', $response->getContent()); + $headers = $response->getHeaders(); + $this->assertSame(['xml-content'], $headers['etag'] ?? false); + $this->assertSame(['text/xml'], $headers['content-type'] ?? false); + } + + public function testBuildThatResponse(): void + { + $builder = (new MockResponseBuilder()) + ->fromCallback(static function (RealRequest $realRequest): MockResponse { + return new MockResponse( + 'response from that', + [ + 'http_code' => 201, + 'response_headers' => [ + 'content-type' => 'text/plain', + 'content-length' => '18', + ], + ], + ); + }); + + $client = new MockHttpClient($builder->getResponse($this->createRealRequest())); + $response = $client->request('GET', '/query'); + + $this->assertSame(201, $response->getStatusCode()); + $this->assertSame('response from that', $response->getContent()); + $headers = $response->getHeaders(); + $this->assertSame(['text/plain'], $headers['content-type'] ?? false); + $this->assertSame(['18'], $headers['content-length'] ?? false); + } + + public function testThatIgnoresOtherValues(): void + { + $builder = (new MockResponseBuilder()) + ->fromCallback(static function (RealRequest $realRequest): MockResponse { + return new MockResponse('response from that', ['http_code' => 201]); + }) + ->code(200) + ->contentType('image/gif') + ->contentLength(6) + ->etag('foobar') + ->content('barbaz'); + + $client = new MockHttpClient($builder->getResponse($this->createRealRequest())); + $response = $client->request('GET', '/query'); + + $this->assertSame(201, $response->getStatusCode()); + $this->assertSame('response from that', $response->getContent()); + $headers = $response->getHeaders(); + $this->assertFalse($headers['etag'] ?? false); + $this->assertFalse($headers['content-length'] ?? false); + $this->assertFalse($headers['content-type'] ?? false); + } + public function testConvertableToStringWithJson(): void { $builder = (new MockResponseBuilder()) @@ -71,4 +179,18 @@ public function testConvertableToStringWithHeaders(): void self::assertSame(implode(PHP_EOL, $parts), (string) $builder); } + + public function testConvertableToStringWithThat(): void + { + $builder = (new MockResponseBuilder()) + ->fromCallback(static function (RealRequest $realRequest): MockResponse { + return new MockResponse('foo'); + }) + ->contentType('image/gif') + ->contentLength(6) + ->etag('foobar') + ->content('barbaz'); + + self::assertSame('callable(realRequest)', (string) $builder); + } } diff --git a/tests/HttpClientMock/RealRequestFactoryTest.php b/tests/HttpClientMock/RealRequestFactoryTest.php new file mode 100644 index 0000000..ec1117b --- /dev/null +++ b/tests/HttpClientMock/RealRequestFactoryTest.php @@ -0,0 +1,122 @@ +realRequestFactory = new RealRequestFactory(); + } + + public function testBuildsRequestWithoutBody(): void + { + $options = [ + 'headers' => ['Content-Type: application/json'], + 'json' => ['foo' => 'bar'], + ]; + $request = ($this->realRequestFactory)('POST', 'https://service.com', $options); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://service.com', $request->getUri()); + self::assertSame(['foo' => 'bar'], $request->getJson()); + } + + public function testBuildsRequestWithJsonInBody(): void + { + $options = [ + 'headers' => [ + 'Content-Length: 1', + 'Content-Type: application/json', + ], + 'body' => '{"foo": "bar"}', + ]; + + $request = ($this->realRequestFactory)('POST', 'https://service.com', $options); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://service.com', $request->getUri()); + self::assertSame(['foo' => 'bar'], $request->getJson()); + } + + public function testBuildsRequestWithCallableInBody(): void + { + $body = $this->getMockBuilder(StreamInterface::class)->getMock(); + $body->method('read') + ->willReturn('{"foo": "bar"}'); + + $options = [ + 'headers' => [ + 'Content-Length: 1', + 'Content-Type: application/json', + ], + 'body' => static fn (int $size) => $body->read($size), + ]; + + $request = ($this->realRequestFactory)('POST', 'https://service.com', $options); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://service.com', $request->getUri()); + self::assertSame(['foo' => 'bar'], $request->getJson()); + } + + public function testBuildsRequestWithFormUrlEncodedInBody(): void + { + $options = [ + 'headers' => [ + 'Content-Length: 1', + 'Content-Type: application/x-www-form-urlencoded', + ], + 'body' => 'foo=bar', + ]; + + $request = ($this->realRequestFactory)('POST', 'https://service.com', $options); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://service.com', $request->getUri()); + self::assertSame(['foo' => 'bar'], $request->getRequestParams()); + } + + public function testBuildsRequestWithMultiPartInBody(): void + { + $options = [ + 'headers' => [ + 'Content-Length: 1', + 'Content-Type: multipart/form-data; boundary=12345', + ], + 'body' => <<<'BODY' + --12345 + Content-Disposition: form-data; name="key" + + content + --12345-- + BODY, + ]; + + $request = ($this->realRequestFactory)('POST', 'https://service.com', $options); + + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://service.com', $request->getUri()); + self::assertEquals( + [ + 'key' => [ + 'name' => 'key', + 'filename' => null, + 'mimetype' => 'application/octet-stream', + 'content' => 'content', + ], + ], + $request->getMultiparts(), + ); + } +} diff --git a/tests/HttpClientMock/RealRequestTrait.php b/tests/HttpClientMock/RealRequestTrait.php new file mode 100644 index 0000000..76038f3 --- /dev/null +++ b/tests/HttpClientMock/RealRequestTrait.php @@ -0,0 +1,30 @@ + $headers + * @param mixed[] $json + * @param array $queryParams + * @param array $requestParams + * @param array $multiparts + */ + private function createRealRequest( + string $method = 'DELETE', + string $uri = '/bar', + array|null $headers = [], + string|null $content = null, + array|null $json = null, + array|null $queryParams = [], + array|null $requestParams = [], + array|null $multiparts = [], + ): RealRequest { + return new RealRequest($method, $uri, $headers, $content, $json, $queryParams, $requestParams, $multiparts); + } +} diff --git a/tests/Request/RequestTraitTest.php b/tests/Request/RequestTraitTest.php index 0025f07..636324e 100644 --- a/tests/Request/RequestTraitTest.php +++ b/tests/Request/RequestTraitTest.php @@ -6,13 +6,11 @@ use Brainbits\FunctionalTestHelpers\Request\RequestBuilder; use Brainbits\FunctionalTestHelpers\Request\RequestTrait; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\BrowserKit\HttpBrowser; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -#[CoversClass(RequestTrait::class)] final class RequestTraitTest extends TestCase { use RequestTrait; diff --git a/tests/Schema/SchemaTraitTest.php b/tests/Schema/SchemaTraitTest.php index 2ce822b..a5ac5bc 100644 --- a/tests/Schema/SchemaTraitTest.php +++ b/tests/Schema/SchemaTraitTest.php @@ -10,7 +10,6 @@ use Brainbits\FunctionalTestHelpers\Schema\Strategy\SchemaStrategy; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\Schema; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -18,7 +17,6 @@ use function Safe\putenv; use function sprintf; -#[CoversClass(SchemaTrait::class)] final class SchemaTraitTest extends TestCase { use SchemaTrait; diff --git a/tests/SevenZipContents/SevenZipContentsTraitTest.php b/tests/SevenZipContents/SevenZipContentsTraitTest.php index 935df61..b6b8fcb 100644 --- a/tests/SevenZipContents/SevenZipContentsTraitTest.php +++ b/tests/SevenZipContents/SevenZipContentsTraitTest.php @@ -7,11 +7,9 @@ use Archive7z\Exception; use Brainbits\FunctionalTestHelpers\SevenZipContents\SevenZipContents; use Brainbits\FunctionalTestHelpers\SevenZipContents\SevenZipContentsTrait; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -#[CoversClass(SevenZipContentsTrait::class)] final class SevenZipContentsTraitTest extends TestCase { use SevenZipContentsTrait; diff --git a/tests/Snapshot/ArraySnapshotTest.php b/tests/Snapshot/ArraySnapshotTest.php index 373e794..de6c84d 100644 --- a/tests/Snapshot/ArraySnapshotTest.php +++ b/tests/Snapshot/ArraySnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -17,7 +16,6 @@ use function Safe\json_decode; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class ArraySnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Snapshot/HtmlSnapshotTest.php b/tests/Snapshot/HtmlSnapshotTest.php index 9e67294..a924e53 100644 --- a/tests/Snapshot/HtmlSnapshotTest.php +++ b/tests/Snapshot/HtmlSnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -16,7 +15,6 @@ use function Safe\file_put_contents; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class HtmlSnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Snapshot/JsonLdSnapshotTest.php b/tests/Snapshot/JsonLdSnapshotTest.php index 0311fb2..658fe2a 100644 --- a/tests/Snapshot/JsonLdSnapshotTest.php +++ b/tests/Snapshot/JsonLdSnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -17,7 +16,6 @@ use function Safe\file_put_contents; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class JsonLdSnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Snapshot/JsonSnapshotTest.php b/tests/Snapshot/JsonSnapshotTest.php index fd15d8d..c9048fa 100644 --- a/tests/Snapshot/JsonSnapshotTest.php +++ b/tests/Snapshot/JsonSnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -16,7 +15,6 @@ use function Safe\file_put_contents; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class JsonSnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Snapshot/TextSnapshotTest.php b/tests/Snapshot/TextSnapshotTest.php index 784406b..8d26fc6 100644 --- a/tests/Snapshot/TextSnapshotTest.php +++ b/tests/Snapshot/TextSnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -16,7 +15,6 @@ use function Safe\file_put_contents; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class TextSnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Snapshot/XmlSnapshotTest.php b/tests/Snapshot/XmlSnapshotTest.php index 7769076..15ff9e8 100644 --- a/tests/Snapshot/XmlSnapshotTest.php +++ b/tests/Snapshot/XmlSnapshotTest.php @@ -8,7 +8,6 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamContent; use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; @@ -16,7 +15,6 @@ use function Safe\file_put_contents; use function Safe\putenv; -#[CoversClass(SnapshotTrait::class)] final class XmlSnapshotTest extends TestCase { use SnapshotTrait; diff --git a/tests/Uuid/UuidTraitTest.php b/tests/Uuid/UuidTraitTest.php index 94be4bf..02b2210 100644 --- a/tests/Uuid/UuidTraitTest.php +++ b/tests/Uuid/UuidTraitTest.php @@ -5,7 +5,6 @@ namespace Brainbits\FunctionalTestHelpers\Tests\Uuid; use Brainbits\FunctionalTestHelpers\Uuid\UuidTrait; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\Uid\Uuid; @@ -13,7 +12,6 @@ use function Safe\json_encode; -#[CoversClass(UuidTrait::class)] final class UuidTraitTest extends TestCase { use UuidTrait; diff --git a/tests/ZipContents/ZipContentsTraitTest.php b/tests/ZipContents/ZipContentsTraitTest.php index a5dfd92..994f86c 100644 --- a/tests/ZipContents/ZipContentsTraitTest.php +++ b/tests/ZipContents/ZipContentsTraitTest.php @@ -5,14 +5,12 @@ namespace Brainbits\FunctionalTestHelpers\Tests\ZipContents; use Brainbits\FunctionalTestHelpers\ZipContents\ZipContentsTrait; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use function Safe\filesize; use function Safe\fopen; -#[CoversClass(ZipContentsTrait::class)] final class ZipContentsTraitTest extends TestCase { use ZipContentsTrait; diff --git a/tests/files/test.txt b/tests/files/test.txt new file mode 100644 index 0000000..f7daa86 --- /dev/null +++ b/tests/files/test.txt @@ -0,0 +1 @@ +this is a txt file