From e5c3d554349ef90709c29767eb9c45dba3ed00ef Mon Sep 17 00:00:00 2001 From: Stephan Wentz Date: Thu, 3 Jul 2025 16:42:28 +0200 Subject: [PATCH] fix: Fix http mock request builder assertions --- composer.json | 6 +- phpstan.neon.dist | 2 + src/HttpClientMock/CallbackHandler.php | 41 +++++------ src/HttpClientMock/HttpClientMockTrait.php | 67 ++++++++++++++--- src/HttpClientMock/MockRequestBuilder.php | 15 +++- .../HttpClientMockTraitTest.php | 12 +++- .../MockRequestBuilderCollectionTest.php | 40 ++++++----- .../HttpClientMock/MockRequestBuilderTest.php | 72 +++++++++++-------- 8 files changed, 171 insertions(+), 84 deletions(-) diff --git a/composer.json b/composer.json index 89bdaef..2d45dba 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,11 @@ "brainbits/phpstan-rules": "^4.0", "dama/doctrine-test-bundle": "^8.2", "doctrine/dbal": "^4.2", - "ergebnis/phpstan-rules": "^2.8", + "ergebnis/phpstan-rules": "^2.10", "gemorroj/archive7z": "^5.7", "mikey179/vfsstream": "^1.6.12", - "monolog/monolog": "^2.0 || ^3.0", - "phpstan/phpstan": "^2.1.4", + "monolog/monolog": "^3.0", + "phpstan/phpstan": "^2.1.8", "phpstan/phpstan-phpunit": "^2.0.4", "phpstan/phpstan-symfony": "^2.0.2", "phpunit/phpunit": "^12.0.2", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index da110c7..54f3102 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,6 +10,8 @@ parameters: - '#Constructor in .* has parameter .* with default value#' - '#SchemaBuilder::foo\(\)#' ergebnis: + noNamedArgument: + enabled: false noAssignByReference: enabled: false noParameterPassedByReference: diff --git a/src/HttpClientMock/CallbackHandler.php b/src/HttpClientMock/CallbackHandler.php index 5fd0551..5082486 100644 --- a/src/HttpClientMock/CallbackHandler.php +++ b/src/HttpClientMock/CallbackHandler.php @@ -8,35 +8,28 @@ use Monolog\Level; use Monolog\LogRecord; -use function class_exists; +final class CallbackHandler extends AbstractProcessingHandler +{ + /** @var callable */ + private $fn; -if (class_exists(LogRecord::class)) { - /** - * Callback handler for Monolog 3.x - */ - final class CallbackHandler extends AbstractProcessingHandler + public function __construct(callable $fn, int|string|Level $level = Level::Debug, bool $bubble = true) { - /** @var callable */ - private $fn; + parent::__construct($level, $bubble); - public function __construct(callable $fn, int|string|Level $level = Level::Debug, bool $bubble = true) - { - parent::__construct($level, $bubble); - - $this->fn = $fn; - } + $this->fn = $fn; + } - public function clear(): void - { - } + public function clear(): void + { + } - public function reset(): void - { - } + public function reset(): void + { + } - protected function write(LogRecord $record): void - { - ($this->fn)($record); - } + protected function write(LogRecord $record): void + { + ($this->fn)($record); } } diff --git a/src/HttpClientMock/HttpClientMockTrait.php b/src/HttpClientMock/HttpClientMockTrait.php index 4e685e9..a12cdec 100644 --- a/src/HttpClientMock/HttpClientMockTrait.php +++ b/src/HttpClientMock/HttpClientMockTrait.php @@ -7,6 +7,8 @@ use ArrayObject; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\HttpClientMockException; use Monolog\Logger; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; @@ -22,15 +24,67 @@ use function Safe\parse_url; use function sprintf; use function str_contains; -use function trigger_deprecation; +use function trigger_error; use function ucfirst; use function urldecode; use function vsprintf; +use const E_USER_DEPRECATED; + /** @mixin TestCase */ trait HttpClientMockTrait { - protected function registerNoMatchingMockRequestAsserts( + /** @var list */ + protected array $createdMockRequestBuilders = []; + /** @var list */ + protected array $mockRequestLoggerNames = ['monolog.logger']; + + #[Before] + final protected function setUpMockRequestBuilder(): void + { + $container = static::getContainer(); + $eventDispatcher = $container->get('event_dispatcher'); + assert($eventDispatcher instanceof EventDispatcherInterface); + + $loggerNames = $this->mockRequestLoggerNames; + + $this->registerNoMatchingMockRequestListeners( + $eventDispatcher, + ...array_map( + static function ($loggerName) { + $logger = static::getContainer()->get($loggerName); + assert($logger instanceof Logger); + + return $logger; + }, + $loggerNames, + ), + ); + } + + #[After] + protected function assertNoFailedMockRequests(): void + { + $mockRequestBuilders = $this->createdMockRequestBuilders; + $this->createdMockRequestBuilders = []; + + foreach ($mockRequestBuilders as $mockRequestBuilder) { + foreach ($mockRequestBuilder->getFailedAssertions() as $failedAssertion) { + throw $failedAssertion; + } + } + } + + protected function registerNoMatchingMockRequestAsserts(): void + { + // legacy start - remove in 8.0.0 + + trigger_error('no matching mock request listeners are now automatically registered', E_USER_DEPRECATED); + + // legacy end - remove in 8.0.0 + } + + protected function registerNoMatchingMockRequestListeners( EventDispatcherInterface $eventDispatcher, Logger ...$loggers, ): void { @@ -130,18 +184,13 @@ protected function mockRequest(string|null $method = null, string|callable|null $stack = self::getContainer()->get(MockRequestBuilderCollection::class); assert($stack instanceof MockRequestBuilderCollection); - $builder = (new MockRequestBuilder()) + $this->createdMockRequestBuilders[] = $builder = (new MockRequestBuilder()) ->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()', - ); + trigger_error('Query parameters in uri is deprecated. Use queryParam() instead', E_USER_DEPRECATED); $uriParts = parse_url($uri); $uri = ($uriParts['scheme'] ?? false ? $uriParts['scheme'] . '://' : '') . ($uriParts['host'] ?? false ? $uriParts['host'] : '') . diff --git a/src/HttpClientMock/MockRequestBuilder.php b/src/HttpClientMock/MockRequestBuilder.php index 94a21c9..a2bc73b 100644 --- a/src/HttpClientMock/MockRequestBuilder.php +++ b/src/HttpClientMock/MockRequestBuilder.php @@ -51,6 +51,9 @@ final class MockRequestBuilder /** @var callable|null */ public mixed $onMatch = null; + /** @var list */ + private array $failedAssertions = []; + public function __construct() { $this->responses = new MockResponseCollection(); @@ -165,10 +168,20 @@ public function assertThat(callable $assert): self return $this; } + /** @return list */ + public function getFailedAssertions(): array + { + return $this->failedAssertions; + } + public function assert(RealRequest $realRequest): void { foreach ($this->assertions as $assertion) { - $assertion($realRequest, $this); + try { + $assertion($realRequest, $this); + } catch (Throwable $e) { + $this->failedAssertions[] = $e; + } } } diff --git a/tests/HttpClientMock/HttpClientMockTraitTest.php b/tests/HttpClientMock/HttpClientMockTraitTest.php index 45b5973..aebf4b0 100644 --- a/tests/HttpClientMock/HttpClientMockTraitTest.php +++ b/tests/HttpClientMock/HttpClientMockTraitTest.php @@ -11,24 +11,34 @@ use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\UriParams; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection; use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher; +use Monolog\Logger; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; final class HttpClientMockTraitTest extends TestCase { use HttpClientMockTrait; private static MockRequestBuilderCollection|null $collection = null; + private static EventDispatcherInterface|null $dispatcher = null; + private static Logger|null $logger = null; - public function setUp(): void + #[Before(100)] + public function createContainerServices(): void { self::$collection = new MockRequestBuilderCollection(); + self::$dispatcher = $this->createMock(EventDispatcherInterface::class); + self::$logger = $this->createMock(Logger::class); } public static function getContainer(): Container { $container = new Container(); $container->set(MockRequestBuilderCollection::class, self::$collection); + $container->set('event_dispatcher', self::$dispatcher); + $container->set('monolog.logger', self::$logger); return $container; } diff --git a/tests/HttpClientMock/MockRequestBuilderCollectionTest.php b/tests/HttpClientMock/MockRequestBuilderCollectionTest.php index 4dca0b1..8b99518 100644 --- a/tests/HttpClientMock/MockRequestBuilderCollectionTest.php +++ b/tests/HttpClientMock/MockRequestBuilderCollectionTest.php @@ -9,9 +9,9 @@ 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\Attributes\DataProvider; +use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; #[CoversClass(MockRequestBuilder::class)] @@ -173,7 +173,7 @@ public function testAssertContent(): void { $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder( - (new MockRequestBuilder()) + $builder = (new MockRequestBuilder()) ->assertContent(function (string $content): void { $this->assertSame('this is content', $content); }) @@ -181,33 +181,36 @@ public function testAssertContent(): void ); $collection('GET', '/query', ['body' => 'this is content']); + + $this->assertSame([], $builder->getFailedAssertions()); } public function testAssertContentFails(): void { $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder( - (new MockRequestBuilder()) + $builder = (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; - } + $collection('GET', '/query', ['body' => 'does-not-match']); - $this->fail('Expected assertion was not thrown'); + $failedAssertions = $builder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } public function testAssertThat(): void { $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder( - (new MockRequestBuilder()) + $builder = (new MockRequestBuilder()) ->assertThat(function (RealRequest $realRequest): void { $this->assertSame('this is content', $realRequest->getContent()); }) @@ -215,26 +218,29 @@ public function testAssertThat(): void ); $collection('GET', '/query', ['body' => 'this is content']); + + $this->assertSame([], $builder->getFailedAssertions()); } public function testAssertThatFails(): void { $collection = new MockRequestBuilderCollection(); $collection->addMockRequestBuilder( - (new MockRequestBuilder()) + $builder = (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; - } + $collection('GET', '/query', ['body' => 'does-not-match']); - $this->fail('Expected assertion was not thrown'); + $failedAssertions = $builder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } /** @return mixed[] */ diff --git a/tests/HttpClientMock/MockRequestBuilderTest.php b/tests/HttpClientMock/MockRequestBuilderTest.php index 3495277..f8710ab 100644 --- a/tests/HttpClientMock/MockRequestBuilderTest.php +++ b/tests/HttpClientMock/MockRequestBuilderTest.php @@ -8,11 +8,12 @@ use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\InvalidMockRequest; use Brainbits\FunctionalTestHelpers\HttpClientMock\Exception\NoResponseMock; use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\HeaderMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\Hit; use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\JsonMatcher; +use Brainbits\FunctionalTestHelpers\HttpClientMock\Matcher\MatchResult; 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; @@ -20,8 +21,8 @@ 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\ExpectationFailedException; use PHPUnit\Framework\TestCase; use RuntimeException; use Symfony\Component\HttpFoundation\File\File; @@ -200,10 +201,11 @@ public function testThat(): void $matcher = $mockRequestBuilder->getMatcher(); + $result = $matcher(new RealRequest('GET', '/test', [], null, null, [], [], [])); + self::assertEquals( - // phpcs:ignore Generic.Files.LineLength.TooLong - new MockRequestMatcher(null, [new ThatMatcher(static fn ($request) => true)]), - $matcher, + MatchResult::create(null)->withResult(Hit::matchesThat()), + $result, ); } @@ -215,6 +217,8 @@ public function testAssertMethod(): void }); $mockRequestBuilder->assert($this->createRealRequest(method: 'POST')); + + $this->assertSame([], $mockRequestBuilder->getFailedAssertions()); } public function testAssertMethodFails(): void @@ -224,13 +228,14 @@ public function testAssertMethodFails(): void $this->assertSame('does-not-match', $method); }); - try { - $mockRequestBuilder->assert($this->createRealRequest(method: 'POST')); - } catch (AssertionFailedError) { - return; - } + $mockRequestBuilder->assert($this->createRealRequest(method: 'POST')); - $this->fail('Expected assertion error was not thrown'); + $failedAssertions = $mockRequestBuilder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } public function testAssertUri(): void @@ -241,6 +246,8 @@ public function testAssertUri(): void }); $mockRequestBuilder->assert($this->createRealRequest(uri: '/query')); + + $this->assertSame([], $mockRequestBuilder->getFailedAssertions()); } public function testAssertUriFails(): void @@ -250,13 +257,14 @@ public function testAssertUriFails(): void $this->assertSame('does-not-match', $content); }); - try { - $mockRequestBuilder->assert($this->createRealRequest(uri: '/query')); - } catch (AssertionFailedError) { - return; - } + $mockRequestBuilder->assert($this->createRealRequest(uri: '/query')); - $this->fail('Expected assertion error was not thrown'); + $failedAssertions = $mockRequestBuilder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } public function testAssertContent(): void @@ -267,6 +275,8 @@ public function testAssertContent(): void }); $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + + $this->assertSame([], $mockRequestBuilder->getFailedAssertions()); } public function testAssertContentFails(): void @@ -276,13 +286,14 @@ public function testAssertContentFails(): void $this->assertSame('does-not-match', $content); }); - try { - $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); - } catch (AssertionFailedError) { - return; - } + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); - $this->fail('Expected assertion error was not thrown'); + $failedAssertions = $mockRequestBuilder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } public function testAssertThat(): void @@ -293,6 +304,8 @@ public function testAssertThat(): void }); $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); + + $this->assertSame([], $mockRequestBuilder->getFailedAssertions()); } public function testAssertThatFails(): void @@ -302,13 +315,14 @@ public function testAssertThatFails(): void $this->assertSame('does-not-match', $realRequest->getContent()); }); - try { - $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); - } catch (AssertionFailedError) { - return; - } + $mockRequestBuilder->assert($this->createRealRequest(content: 'this is content')); - $this->fail('Expected assertion error was not thrown'); + $failedAssertions = $mockRequestBuilder->getFailedAssertions(); + $this->assertCount(1, $failedAssertions); + $this->assertInstanceOf( + ExpectationFailedException::class, + $failedAssertions[0], + ); } public function testEmptyResponsesThrowsException(): void