From 16a1d7fc7beeb500fcc0d8eb7e06ae8e69e76841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 09:53:46 +0100 Subject: [PATCH 1/7] Move the existing tests into tests/Unit The tests now live in the Setono\ClientBundle\Tests\Unit namespace and run as the Unit test suite, which makes room for a Functional suite next to it. --- CLAUDE.md | 4 ++-- phpunit.xml.dist | 4 ++-- tests/{ => Unit}/Client/ChangeAwareMetadataTest.php | 2 +- tests/{ => Unit}/Context/CachedClientContextTest.php | 2 +- tests/{ => Unit}/Context/CookieBasedClientContextTest.php | 2 +- tests/{ => Unit}/Context/DefaultClientContextTest.php | 2 +- tests/{ => Unit}/Controller/ClientValueResolverTest.php | 2 +- .../{ => Unit}/CookieProvider/CachedCookieProviderTest.php | 2 +- .../CookieProvider/RequestBasedCookieProviderTest.php | 2 +- .../DependencyInjection/SetonoClientExtensionTest.php | 2 +- tests/{ => Unit}/Entity/MetadataTest.php | 2 +- .../EventListener/Doctrine/ConvertToEntityListenerTest.php | 2 +- .../EventSubscriber/StoreCookieSubscriberTest.php | 2 +- .../EventSubscriber/StoreMetadataSubscriberTest.php | 2 +- .../DoctrineOrmBasedMetadataPersisterTest.php | 2 +- .../DoctrineOrmBasedMetadataProviderTest.php | 2 +- .../MetadataProvider/EmptyMetadataProviderTest.php | 2 +- tests/{ => Unit}/SetonoClientBundleTest.php | 6 +++--- 18 files changed, 22 insertions(+), 22 deletions(-) rename tests/{ => Unit}/Client/ChangeAwareMetadataTest.php (95%) rename tests/{ => Unit}/Context/CachedClientContextTest.php (95%) rename tests/{ => Unit}/Context/CookieBasedClientContextTest.php (97%) rename tests/{ => Unit}/Context/DefaultClientContextTest.php (92%) rename tests/{ => Unit}/Controller/ClientValueResolverTest.php (96%) rename tests/{ => Unit}/CookieProvider/CachedCookieProviderTest.php (97%) rename tests/{ => Unit}/CookieProvider/RequestBasedCookieProviderTest.php (98%) rename tests/{ => Unit}/DependencyInjection/SetonoClientExtensionTest.php (96%) rename tests/{ => Unit}/Entity/MetadataTest.php (95%) rename tests/{ => Unit}/EventListener/Doctrine/ConvertToEntityListenerTest.php (96%) rename tests/{ => Unit}/EventSubscriber/StoreCookieSubscriberTest.php (98%) rename tests/{ => Unit}/EventSubscriber/StoreMetadataSubscriberTest.php (97%) rename tests/{ => Unit}/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php (99%) rename tests/{ => Unit}/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php (98%) rename tests/{ => Unit}/MetadataProvider/EmptyMetadataProviderTest.php (88%) rename tests/{ => Unit}/SetonoClientBundleTest.php (82%) diff --git a/CLAUDE.md b/CLAUDE.md index b63c3b0..1ae18bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,8 @@ This is a library/bundle — there is no runnable application here. It is exerci PHP 8.1+ is required (CI runs 8.1 and 8.2). Use the `8.1`/`8.2` shell switchers if needed. The dev tooling is the individually-pinned set from `setono/code-quality-pack` (inlined into `require-dev` rather than depending on the meta-package); PHPUnit is held at `^10.5` so PHP 8.1 stays supported (PHPUnit 11 needs 8.2). -- Tests: `composer phpunit` (or `vendor/bin/phpunit`) -- Single test: `vendor/bin/phpunit --filter after_loading_the_correct_parameter_has_been_set` or by path `vendor/bin/phpunit tests/DependencyInjection/SetonoClientExtensionTest.php` +- Tests: `composer phpunit` (or `vendor/bin/phpunit`). Unit tests live in `tests/Unit` (the `Unit` suite: `vendor/bin/phpunit --testsuite Unit`) +- Single test: `vendor/bin/phpunit --filter after_loading_the_correct_parameter_has_been_set` or by path `vendor/bin/phpunit tests/Unit/DependencyInjection/SetonoClientExtensionTest.php` - Static analysis: `composer analyse` (PHPStan at `level: max`, config in `phpstan.neon.dist`; auto-loads the doctrine/symfony/phpunit/strict-rules extensions via `phpstan/extension-installer`) - Check style: `composer check-style` (ECS, Sylius Labs ruleset) - Fix style: `composer fix-style` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b171336..76980bb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,8 +3,8 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache"> - - tests + + tests/Unit diff --git a/tests/Client/ChangeAwareMetadataTest.php b/tests/Unit/Client/ChangeAwareMetadataTest.php similarity index 95% rename from tests/Client/ChangeAwareMetadataTest.php rename to tests/Unit/Client/ChangeAwareMetadataTest.php index a3f161e..a4b1389 100644 --- a/tests/Client/ChangeAwareMetadataTest.php +++ b/tests/Unit/Client/ChangeAwareMetadataTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Client; +namespace Setono\ClientBundle\Tests\Unit\Client; use PHPUnit\Framework\TestCase; use Setono\ClientBundle\Client\ChangeAwareMetadata; diff --git a/tests/Context/CachedClientContextTest.php b/tests/Unit/Context/CachedClientContextTest.php similarity index 95% rename from tests/Context/CachedClientContextTest.php rename to tests/Unit/Context/CachedClientContextTest.php index fe92ba0..2c909cc 100644 --- a/tests/Context/CachedClientContextTest.php +++ b/tests/Unit/Context/CachedClientContextTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Context; +namespace Setono\ClientBundle\Tests\Unit\Context; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Context/CookieBasedClientContextTest.php b/tests/Unit/Context/CookieBasedClientContextTest.php similarity index 97% rename from tests/Context/CookieBasedClientContextTest.php rename to tests/Unit/Context/CookieBasedClientContextTest.php index 6e6a1ad..f1ad93f 100644 --- a/tests/Context/CookieBasedClientContextTest.php +++ b/tests/Unit/Context/CookieBasedClientContextTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Context; +namespace Setono\ClientBundle\Tests\Unit\Context; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/Context/DefaultClientContextTest.php b/tests/Unit/Context/DefaultClientContextTest.php similarity index 92% rename from tests/Context/DefaultClientContextTest.php rename to tests/Unit/Context/DefaultClientContextTest.php index a646776..a165f18 100644 --- a/tests/Context/DefaultClientContextTest.php +++ b/tests/Unit/Context/DefaultClientContextTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Context; +namespace Setono\ClientBundle\Tests\Unit\Context; use PHPUnit\Framework\TestCase; use Setono\ClientBundle\Client\ChangeAwareMetadata; diff --git a/tests/Controller/ClientValueResolverTest.php b/tests/Unit/Controller/ClientValueResolverTest.php similarity index 96% rename from tests/Controller/ClientValueResolverTest.php rename to tests/Unit/Controller/ClientValueResolverTest.php index 8665f48..2d7dbd9 100644 --- a/tests/Controller/ClientValueResolverTest.php +++ b/tests/Unit/Controller/ClientValueResolverTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Controller; +namespace Setono\ClientBundle\Tests\Unit\Controller; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/CookieProvider/CachedCookieProviderTest.php b/tests/Unit/CookieProvider/CachedCookieProviderTest.php similarity index 97% rename from tests/CookieProvider/CachedCookieProviderTest.php rename to tests/Unit/CookieProvider/CachedCookieProviderTest.php index e55618a..4d512ae 100644 --- a/tests/CookieProvider/CachedCookieProviderTest.php +++ b/tests/Unit/CookieProvider/CachedCookieProviderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\CookieProvider; +namespace Setono\ClientBundle\Tests\Unit\CookieProvider; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/CookieProvider/RequestBasedCookieProviderTest.php b/tests/Unit/CookieProvider/RequestBasedCookieProviderTest.php similarity index 98% rename from tests/CookieProvider/RequestBasedCookieProviderTest.php rename to tests/Unit/CookieProvider/RequestBasedCookieProviderTest.php index eb4b28e..a24cab7 100644 --- a/tests/CookieProvider/RequestBasedCookieProviderTest.php +++ b/tests/Unit/CookieProvider/RequestBasedCookieProviderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\CookieProvider; +namespace Setono\ClientBundle\Tests\Unit\CookieProvider; use PHPUnit\Framework\TestCase; use Setono\Client\Cookie; diff --git a/tests/DependencyInjection/SetonoClientExtensionTest.php b/tests/Unit/DependencyInjection/SetonoClientExtensionTest.php similarity index 96% rename from tests/DependencyInjection/SetonoClientExtensionTest.php rename to tests/Unit/DependencyInjection/SetonoClientExtensionTest.php index 47778c3..6d68fbb 100644 --- a/tests/DependencyInjection/SetonoClientExtensionTest.php +++ b/tests/Unit/DependencyInjection/SetonoClientExtensionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\DependencyInjection; +namespace Setono\ClientBundle\Tests\Unit\DependencyInjection; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Setono\ClientBundle\Context\ClientContextInterface; diff --git a/tests/Entity/MetadataTest.php b/tests/Unit/Entity/MetadataTest.php similarity index 95% rename from tests/Entity/MetadataTest.php rename to tests/Unit/Entity/MetadataTest.php index 1aab1f7..e9fed6f 100644 --- a/tests/Entity/MetadataTest.php +++ b/tests/Unit/Entity/MetadataTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\Entity; +namespace Setono\ClientBundle\Tests\Unit\Entity; use PHPUnit\Framework\TestCase; use Setono\ClientBundle\Entity\Metadata; diff --git a/tests/EventListener/Doctrine/ConvertToEntityListenerTest.php b/tests/Unit/EventListener/Doctrine/ConvertToEntityListenerTest.php similarity index 96% rename from tests/EventListener/Doctrine/ConvertToEntityListenerTest.php rename to tests/Unit/EventListener/Doctrine/ConvertToEntityListenerTest.php index a7457ac..9b196ef 100644 --- a/tests/EventListener/Doctrine/ConvertToEntityListenerTest.php +++ b/tests/Unit/EventListener/Doctrine/ConvertToEntityListenerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\EventListener\Doctrine; +namespace Setono\ClientBundle\Tests\Unit\EventListener\Doctrine; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; diff --git a/tests/EventSubscriber/StoreCookieSubscriberTest.php b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php similarity index 98% rename from tests/EventSubscriber/StoreCookieSubscriberTest.php rename to tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php index 8a5c4bf..9327c44 100644 --- a/tests/EventSubscriber/StoreCookieSubscriberTest.php +++ b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\EventSubscriber; +namespace Setono\ClientBundle\Tests\Unit\EventSubscriber; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/EventSubscriber/StoreMetadataSubscriberTest.php b/tests/Unit/EventSubscriber/StoreMetadataSubscriberTest.php similarity index 97% rename from tests/EventSubscriber/StoreMetadataSubscriberTest.php rename to tests/Unit/EventSubscriber/StoreMetadataSubscriberTest.php index b5cb0cd..f0d12f7 100644 --- a/tests/EventSubscriber/StoreMetadataSubscriberTest.php +++ b/tests/Unit/EventSubscriber/StoreMetadataSubscriberTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\EventSubscriber; +namespace Setono\ClientBundle\Tests\Unit\EventSubscriber; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php similarity index 99% rename from tests/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php rename to tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php index 452b279..d2fa08c 100644 --- a/tests/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php +++ b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\MetadataPersister; +namespace Setono\ClientBundle\Tests\Unit\MetadataPersister; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; diff --git a/tests/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php similarity index 98% rename from tests/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php rename to tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php index d9feaa3..c182397 100644 --- a/tests/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php +++ b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\MetadataProvider; +namespace Setono\ClientBundle\Tests\Unit\MetadataProvider; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; diff --git a/tests/MetadataProvider/EmptyMetadataProviderTest.php b/tests/Unit/MetadataProvider/EmptyMetadataProviderTest.php similarity index 88% rename from tests/MetadataProvider/EmptyMetadataProviderTest.php rename to tests/Unit/MetadataProvider/EmptyMetadataProviderTest.php index 304dbdd..b2afffd 100644 --- a/tests/MetadataProvider/EmptyMetadataProviderTest.php +++ b/tests/Unit/MetadataProvider/EmptyMetadataProviderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests\MetadataProvider; +namespace Setono\ClientBundle\Tests\Unit\MetadataProvider; use PHPUnit\Framework\TestCase; use Setono\ClientBundle\MetadataProvider\EmptyMetadataProvider; diff --git a/tests/SetonoClientBundleTest.php b/tests/Unit/SetonoClientBundleTest.php similarity index 82% rename from tests/SetonoClientBundleTest.php rename to tests/Unit/SetonoClientBundleTest.php index 9e2f03c..db0148c 100644 --- a/tests/SetonoClientBundleTest.php +++ b/tests/Unit/SetonoClientBundleTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Setono\ClientBundle\Tests; +namespace Setono\ClientBundle\Tests\Unit; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use PHPUnit\Framework\TestCase; @@ -16,7 +16,7 @@ final class SetonoClientBundleTest extends TestCase */ public function its_path_points_to_the_package_root(): void { - self::assertSame(\dirname(__DIR__), (new SetonoClientBundle())->getPath()); + self::assertSame(\dirname(__DIR__, 2), (new SetonoClientBundle())->getPath()); } /** @@ -38,7 +38,7 @@ public function it_registers_the_doctrine_mapping_for_the_metadata_entity(): voi self::assertInstanceOf(DoctrineOrmMappingsPass::class, $pass); self::assertSame( - [\dirname(__DIR__) . '/src/../config/doctrine-mapping' => 'Setono\ClientBundle\Entity'], + [\dirname(__DIR__, 2) . '/src/../config/doctrine-mapping' => 'Setono\ClientBundle\Entity'], (new \ReflectionProperty($pass, 'namespaces'))->getValue($pass), ); } From 73e41d7590aaaa433cbe0efe46325e9bcc8bcde8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 09:56:44 +0100 Subject: [PATCH 2/7] Add missing unit tests - Configuration: defaults, overrides and partial cookie configuration - Extension: custom values become parameters, and the subscribers, value resolver and Doctrine listener are tagged - StoreCookieSubscriber: the last seen timestamp is refreshed, and the configured cookie name and expiration are used - CachedCookieProvider: a missing cookie is cached, and each request is resolved separately - DoctrineOrmBasedMetadataProvider: no query before the metadata is used, the fallback to the decorated provider, and loaded metadata is not dirty - DoctrineOrmBasedMetadataPersister: lazy metadata that was only read is not written, and lazy metadata that was changed is --- composer.json | 1 + .../CachedCookieProviderTest.php | 36 +++++++++ .../DependencyInjection/ConfigurationTest.php | 73 +++++++++++++++++++ .../Fixture/CustomMetadata.php | 11 +++ .../SetonoClientExtensionTest.php | 34 +++++++++ .../StoreCookieSubscriberTest.php | 52 +++++++++++++ .../DoctrineOrmBasedMetadataPersisterTest.php | 55 ++++++++++++++ .../DoctrineOrmBasedMetadataProviderTest.php | 30 ++++++++ 8 files changed, 292 insertions(+) create mode 100644 tests/Unit/DependencyInjection/ConfigurationTest.php create mode 100644 tests/Unit/DependencyInjection/Fixture/CustomMetadata.php diff --git a/composer.json b/composer.json index 9f340eb..6f881b6 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "ergebnis/composer-normalize": "^2.50", "infection/infection": "^0.27 || ^0.28", "jangregor/phpstan-prophecy": "^2.3", + "matthiasnoback/symfony-config-test": "^4.3 || ^5.1", "matthiasnoback/symfony-dependency-injection-test": "^4.3.1 || ^5.1", "phpspec/prophecy": "^1.20", "phpspec/prophecy-phpunit": "^2.5", diff --git a/tests/Unit/CookieProvider/CachedCookieProviderTest.php b/tests/Unit/CookieProvider/CachedCookieProviderTest.php index 4d512ae..f5fb205 100644 --- a/tests/Unit/CookieProvider/CachedCookieProviderTest.php +++ b/tests/Unit/CookieProvider/CachedCookieProviderTest.php @@ -47,6 +47,42 @@ public function it_resolves_the_cookie_once_per_request(): void self::assertSame($cookie, $provider->getCookie($request)); } + /** + * @test + */ + public function it_caches_a_missing_cookie(): void + { + $request = new Request(); + + $decorated = $this->prophesize(CookieProviderInterface::class); + $decorated->getCookie($request)->willReturn(null)->shouldBeCalledOnce(); + + $provider = new CachedCookieProvider($decorated->reveal(), new RequestStack()); + + self::assertNull($provider->getCookie($request)); + self::assertNull($provider->getCookie($request)); + } + + /** + * @test + */ + public function it_resolves_the_cookie_of_each_request_separately(): void + { + $firstRequest = Request::create('/first'); + $firstCookie = new Cookie('first-client-id'); + $secondRequest = Request::create('/second'); + $secondCookie = new Cookie('second-client-id'); + + $decorated = $this->prophesize(CookieProviderInterface::class); + $decorated->getCookie($firstRequest)->willReturn($firstCookie); + $decorated->getCookie($secondRequest)->willReturn($secondCookie); + + $provider = new CachedCookieProvider($decorated->reveal(), new RequestStack()); + + self::assertSame($firstCookie, $provider->getCookie($firstRequest)); + self::assertSame($secondCookie, $provider->getCookie($secondRequest)); + } + /** * @test */ diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..732f0bb --- /dev/null +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,73 @@ +assertProcessedConfigurationEquals([], [ + 'cookie' => [ + 'name' => 'setono_client_id', + 'expiration' => '+365 days', + ], + 'metadata_class' => Metadata::class, + ]); + } + + /** + * @test + */ + public function it_allows_overriding_the_values(): void + { + $this->assertProcessedConfigurationEquals([ + [ + 'cookie' => [ + 'name' => 'client_id', + 'expiration' => '+30 days', + ], + 'metadata_class' => CustomMetadata::class, + ], + ], [ + 'cookie' => [ + 'name' => 'client_id', + 'expiration' => '+30 days', + ], + 'metadata_class' => CustomMetadata::class, + ]); + } + + /** + * @test + */ + public function it_keeps_the_default_of_a_cookie_option_that_is_not_overridden(): void + { + $this->assertProcessedConfigurationEquals([ + ['cookie' => ['name' => 'client_id']], + ], [ + 'cookie' => [ + 'name' => 'client_id', + 'expiration' => '+365 days', + ], + 'metadata_class' => Metadata::class, + ]); + } +} diff --git a/tests/Unit/DependencyInjection/Fixture/CustomMetadata.php b/tests/Unit/DependencyInjection/Fixture/CustomMetadata.php new file mode 100644 index 0000000..8451241 --- /dev/null +++ b/tests/Unit/DependencyInjection/Fixture/CustomMetadata.php @@ -0,0 +1,11 @@ +assertContainerBuilderHasParameter('setono_client.metadata_class', Metadata::class); } + /** + * @test + */ + public function after_loading_with_custom_values_the_parameters_reflect_them(): void + { + $this->load([ + 'cookie' => [ + 'name' => 'client_id', + 'expiration' => '+30 days', + ], + 'metadata_class' => CustomMetadata::class, + ]); + + $this->assertContainerBuilderHasParameter('setono_client.cookie.name', 'client_id'); + $this->assertContainerBuilderHasParameter('setono_client.cookie.expiration', '+30 days'); + $this->assertContainerBuilderHasParameter('setono_client.metadata_class', CustomMetadata::class); + } + /** * @test */ @@ -44,6 +63,21 @@ public function after_loading_the_services_are_registered(): void $this->assertContainerBuilderHasAlias(ClientContextInterface::class, 'setono_client.client_context.default'); } + /** + * @test + */ + public function after_loading_the_services_hooking_into_symfony_and_doctrine_are_tagged(): void + { + $this->load(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag('setono_client.event_subscriber.store_cookie', 'kernel.event_subscriber'); + $this->assertContainerBuilderHasServiceDefinitionWithTag('setono_client.event_subscriber.store_metadata', 'kernel.event_subscriber'); + $this->assertContainerBuilderHasServiceDefinitionWithTag('setono_client.controller.client_value_resolver', 'controller.argument_value_resolver'); + $this->assertContainerBuilderHasServiceDefinitionWithTag('setono_client.event_listener_doctrine.convert_to_entity', 'doctrine.event_listener', [ + 'event' => 'loadClassMetadata', + ]); + } + /** * @test */ diff --git a/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php index 9327c44..062c65d 100644 --- a/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php +++ b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php @@ -78,6 +78,58 @@ public function it_reuses_the_client_id_from_an_existing_cookie(): void self::assertSame('existing-id', Cookie::fromString((string) $cookies[0]->getValue())->clientId); } + /** + * @test + */ + public function it_refreshes_the_last_seen_timestamp_and_keeps_the_first_seen_timestamp(): void + { + $clientContext = $this->prophesize(ClientContextInterface::class); + + $cookieProvider = $this->prophesize(CookieProviderInterface::class); + $cookieProvider->getCookie(Argument::type(Request::class))->willReturn(new Cookie('existing-id', 2, 1_000, 2_000)); + + $before = time(); + $response = new Response(); + $this->createSubscriber($clientContext->reveal(), $cookieProvider->reveal())->store( + $this->createResponseEvent(new Request(), $response), + ); + + $cookie = Cookie::fromString((string) $response->headers->getCookies()[0]->getValue()); + self::assertSame(1_000, $cookie->firstSeenAt); + self::assertGreaterThanOrEqual($before, $cookie->lastSeenAt); + self::assertLessThanOrEqual(time(), $cookie->lastSeenAt); + } + + /** + * @test + */ + public function it_uses_the_configured_cookie_name_and_expiration(): void + { + $clientContext = $this->prophesize(ClientContextInterface::class); + $clientContext->getClient()->willReturn(new Client('client-id')); + + $cookieProvider = $this->prophesize(CookieProviderInterface::class); + $cookieProvider->getCookie(Argument::type(Request::class))->willReturn(null); + + $subscriber = new StoreCookieSubscriber( + $clientContext->reveal(), + $cookieProvider->reveal(), + new EventDispatcher(), + 'custom_name', + '+30 days', + ); + + $before = time(); + $response = new Response(); + $subscriber->store($this->createResponseEvent(new Request(), $response)); + + $cookies = $response->headers->getCookies(); + self::assertCount(1, $cookies); + self::assertSame('custom_name', $cookies[0]->getName()); + self::assertGreaterThanOrEqual(strtotime('+30 days', $before), $cookies[0]->getExpiresTime()); + self::assertLessThanOrEqual(strtotime('+30 days'), $cookies[0]->getExpiresTime()); + } + /** * @test */ diff --git a/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php index d2fa08c..663bdf7 100644 --- a/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php +++ b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php @@ -50,6 +50,61 @@ public function it_does_nothing_when_the_lazy_metadata_is_not_initialized(): voi self::assertFalse($metadata->isLazyObjectInitialized()); } + /** + * @test + */ + public function it_does_nothing_when_the_lazy_metadata_was_only_read(): void + { + $entity = new MetadataEntity(); + $entity->setClientId('client-id'); + $entity->setMetadata(['foo' => 'bar']); + + $manager = $this->prophesize(EntityManagerInterface::class); + $manager->find(MetadataEntity::class, 'client-id')->willReturn($entity)->shouldBeCalledOnce(); + $manager->persist(Argument::cetera())->shouldNotBeCalled(); + $manager->flush()->shouldNotBeCalled(); + + $managerRegistry = $this->createManagerRegistry($manager); + + $metadata = (new DoctrineOrmBasedMetadataProvider( + new EmptyMetadataProvider(), + $managerRegistry->reveal(), + MetadataEntity::class, + ))->getMetadata('client-id'); + + self::assertSame('bar', $metadata->get('foo')); + + $this->createPersister($managerRegistry)->persist(new Client('client-id', $metadata)); + } + + /** + * @test + */ + public function it_updates_the_entity_when_the_lazy_metadata_was_changed(): void + { + $entity = new MetadataEntity(); + $entity->setClientId('client-id'); + $entity->setMetadata(['foo' => 'bar']); + + $manager = $this->prophesize(EntityManagerInterface::class); + $manager->find(MetadataEntity::class, 'client-id')->willReturn($entity); + $manager->persist(Argument::cetera())->shouldNotBeCalled(); + $manager->flush()->shouldBeCalledOnce(); + + $managerRegistry = $this->createManagerRegistry($manager); + + $metadata = (new DoctrineOrmBasedMetadataProvider( + new EmptyMetadataProvider(), + $managerRegistry->reveal(), + MetadataEntity::class, + ))->getMetadata('client-id'); + $metadata->set('baz', 'qux'); + + $this->createPersister($managerRegistry)->persist(new Client('client-id', $metadata)); + + self::assertSame(['foo' => 'bar', 'baz' => 'qux'], $entity->getMetadata()); + } + /** * @test */ diff --git a/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php index c182397..9af678d 100644 --- a/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php +++ b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php @@ -25,6 +25,7 @@ final class DoctrineOrmBasedMetadataProviderTest extends TestCase public function it_returns_lazy_metadata(): void { $manager = $this->prophesize(EntityManagerInterface::class); + $manager->find(Argument::cetera())->shouldNotBeCalled(); $managerRegistry = $this->prophesize(ManagerRegistry::class); $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); @@ -98,5 +99,34 @@ public function it_loads_the_metadata_from_an_existing_entity(): void $metadata = $provider->getMetadata('some-client-id'); self::assertSame('bar', $metadata->get('foo')); + self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); + self::assertFalse($metadata->isDirty()); + } + + /** + * @test + */ + public function it_falls_back_to_the_decorated_provider_when_the_client_has_no_entity(): void + { + $manager = $this->prophesize(EntityManagerInterface::class); + $manager->find(MetadataEntity::class, 'some-client-id')->willReturn(null); + + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); + + $decorated = $this->prophesize(MetadataProviderInterface::class); + $decorated->getMetadata('some-client-id')->willReturn(new Metadata(['foo' => 'bar'])); + + $provider = new DoctrineOrmBasedMetadataProvider( + $decorated->reveal(), + $managerRegistry->reveal(), + MetadataEntity::class, + ); + + $metadata = $provider->getMetadata('some-client-id'); + + self::assertSame('bar', $metadata->get('foo')); + self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); + self::assertFalse($metadata->isDirty()); } } From 6920f7bb7f69654171a1dee35cfc37469ef6a0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 10:25:25 +0100 Subject: [PATCH 3/7] Use lowercase test suite names --- CLAUDE.md | 2 +- phpunit.xml.dist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1ae18bb..3045ea2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ This is a library/bundle — there is no runnable application here. It is exerci PHP 8.1+ is required (CI runs 8.1 and 8.2). Use the `8.1`/`8.2` shell switchers if needed. The dev tooling is the individually-pinned set from `setono/code-quality-pack` (inlined into `require-dev` rather than depending on the meta-package); PHPUnit is held at `^10.5` so PHP 8.1 stays supported (PHPUnit 11 needs 8.2). -- Tests: `composer phpunit` (or `vendor/bin/phpunit`). Unit tests live in `tests/Unit` (the `Unit` suite: `vendor/bin/phpunit --testsuite Unit`) +- Tests: `composer phpunit` (or `vendor/bin/phpunit`). Unit tests live in `tests/Unit` (the `unit` suite: `vendor/bin/phpunit --testsuite unit`) - Single test: `vendor/bin/phpunit --filter after_loading_the_correct_parameter_has_been_set` or by path `vendor/bin/phpunit tests/Unit/DependencyInjection/SetonoClientExtensionTest.php` - Static analysis: `composer analyse` (PHPStan at `level: max`, config in `phpstan.neon.dist`; auto-loads the doctrine/symfony/phpunit/strict-rules extensions via `phpstan/extension-installer`) - Check style: `composer check-style` (ECS, Sylius Labs ruleset) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 76980bb..14e4ada 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,7 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache"> - + tests/Unit From 7ce7fa36675de3987662bc4243ccd527d5996cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 11:28:30 +0100 Subject: [PATCH 4/7] Tell the requests apart by identity in the cookie cache test --- tests/Unit/CookieProvider/CachedCookieProviderTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Unit/CookieProvider/CachedCookieProviderTest.php b/tests/Unit/CookieProvider/CachedCookieProviderTest.php index f5fb205..40aa3dd 100644 --- a/tests/Unit/CookieProvider/CachedCookieProviderTest.php +++ b/tests/Unit/CookieProvider/CachedCookieProviderTest.php @@ -68,14 +68,16 @@ public function it_caches_a_missing_cookie(): void */ public function it_resolves_the_cookie_of_each_request_separately(): void { - $firstRequest = Request::create('/first'); + // The requests are equal, so only their identity tells them apart. Prophecy compares arguments with ==, so the stub checks the identity itself + $firstRequest = new Request(); $firstCookie = new Cookie('first-client-id'); - $secondRequest = Request::create('/second'); + $secondRequest = new Request(); $secondCookie = new Cookie('second-client-id'); $decorated = $this->prophesize(CookieProviderInterface::class); - $decorated->getCookie($firstRequest)->willReturn($firstCookie); - $decorated->getCookie($secondRequest)->willReturn($secondCookie); + $decorated->getCookie(Argument::type(Request::class))->will( + static fn (array $arguments): Cookie => $arguments[0] === $firstRequest ? $firstCookie : $secondCookie, + ); $provider = new CachedCookieProvider($decorated->reveal(), new RequestStack()); From 85b899bba81e7203ed4057af395b06cf64e837cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 11:28:30 +0100 Subject: [PATCH 5/7] Test the persister with a lazy ghost instead of the real provider --- phpstan.neon.dist | 4 ++ .../DoctrineOrmBasedMetadataPersisterTest.php | 54 +++++++------------ 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0d978a3..4a70794 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,3 +12,7 @@ parameters: identifier: constructor.call path: src/MetadataProvider/DoctrineOrmBasedMetadataProvider.php count: 1 + - + identifier: constructor.call + path: tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php + count: 1 diff --git a/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php index 663bdf7..1e727f0 100644 --- a/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php +++ b/tests/Unit/MetadataPersister/DoctrineOrmBasedMetadataPersisterTest.php @@ -17,8 +17,6 @@ use Setono\ClientBundle\Client\LazyChangeAwareMetadata; use Setono\ClientBundle\Entity\Metadata as MetadataEntity; use Setono\ClientBundle\MetadataPersister\DoctrineOrmBasedMetadataPersister; -use Setono\ClientBundle\MetadataProvider\DoctrineOrmBasedMetadataProvider; -use Setono\ClientBundle\MetadataProvider\EmptyMetadataProvider; final class DoctrineOrmBasedMetadataPersisterTest extends TestCase { @@ -34,18 +32,9 @@ public function it_does_nothing_when_the_lazy_metadata_is_not_initialized(): voi $manager->persist(Argument::cetera())->shouldNotBeCalled(); $manager->flush()->shouldNotBeCalled(); - $managerRegistry = $this->createManagerRegistry($manager); - - $metadata = (new DoctrineOrmBasedMetadataProvider( - new EmptyMetadataProvider(), - $managerRegistry->reveal(), - MetadataEntity::class, - ))->getMetadata('client-id'); - - self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); - self::assertFalse($metadata->isLazyObjectInitialized()); + $metadata = self::createLazyMetadata(['foo' => 'bar']); - $this->createPersister($managerRegistry)->persist(new Client('client-id', $metadata)); + $this->createPersister($this->createManagerRegistry($manager))->persist(new Client('client-id', $metadata)); self::assertFalse($metadata->isLazyObjectInitialized()); } @@ -55,26 +44,15 @@ public function it_does_nothing_when_the_lazy_metadata_is_not_initialized(): voi */ public function it_does_nothing_when_the_lazy_metadata_was_only_read(): void { - $entity = new MetadataEntity(); - $entity->setClientId('client-id'); - $entity->setMetadata(['foo' => 'bar']); - $manager = $this->prophesize(EntityManagerInterface::class); - $manager->find(MetadataEntity::class, 'client-id')->willReturn($entity)->shouldBeCalledOnce(); + $manager->find(Argument::cetera())->shouldNotBeCalled(); $manager->persist(Argument::cetera())->shouldNotBeCalled(); $manager->flush()->shouldNotBeCalled(); - $managerRegistry = $this->createManagerRegistry($manager); - - $metadata = (new DoctrineOrmBasedMetadataProvider( - new EmptyMetadataProvider(), - $managerRegistry->reveal(), - MetadataEntity::class, - ))->getMetadata('client-id'); - + $metadata = self::createLazyMetadata(['foo' => 'bar']); self::assertSame('bar', $metadata->get('foo')); - $this->createPersister($managerRegistry)->persist(new Client('client-id', $metadata)); + $this->createPersister($this->createManagerRegistry($manager))->persist(new Client('client-id', $metadata)); } /** @@ -91,16 +69,10 @@ public function it_updates_the_entity_when_the_lazy_metadata_was_changed(): void $manager->persist(Argument::cetera())->shouldNotBeCalled(); $manager->flush()->shouldBeCalledOnce(); - $managerRegistry = $this->createManagerRegistry($manager); - - $metadata = (new DoctrineOrmBasedMetadataProvider( - new EmptyMetadataProvider(), - $managerRegistry->reveal(), - MetadataEntity::class, - ))->getMetadata('client-id'); + $metadata = self::createLazyMetadata(['foo' => 'bar']); $metadata->set('baz', 'qux'); - $this->createPersister($managerRegistry)->persist(new Client('client-id', $metadata)); + $this->createPersister($this->createManagerRegistry($manager))->persist(new Client('client-id', $metadata)); self::assertSame(['foo' => 'bar', 'baz' => 'qux'], $entity->getMetadata()); } @@ -227,6 +199,18 @@ private function createManagerRegistry(ObjectProphecy $manager): ObjectProphecy return $managerRegistry; } + /** + * Returns lazy metadata like the one DoctrineOrmBasedMetadataProvider returns, holding the given data once initialized + * + * @param array $data + */ + private static function createLazyMetadata(array $data): LazyChangeAwareMetadata + { + return LazyChangeAwareMetadata::createLazyGhost(static function (LazyChangeAwareMetadata $instance) use ($data): void { + $instance->__construct($data); + }); + } + /** * @param ObjectProphecy $managerRegistry */ From 492bde103f12cb388ee4b5b85edbd72f7bf63d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 11:28:30 +0100 Subject: [PATCH 6/7] Share the setup in the provider and subscriber tests --- .../StoreCookieSubscriberTest.php | 36 ++++------ .../DoctrineOrmBasedMetadataProviderTest.php | 67 ++++++++----------- 2 files changed, 40 insertions(+), 63 deletions(-) diff --git a/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php index 062c65d..cc9ef5d 100644 --- a/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php +++ b/tests/Unit/EventSubscriber/StoreCookieSubscriberTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Psr\EventDispatcher\EventDispatcherInterface; use Setono\Client\Client; use Setono\Client\Cookie; use Setono\ClientBundle\Context\ClientContextInterface; @@ -111,17 +112,14 @@ public function it_uses_the_configured_cookie_name_and_expiration(): void $cookieProvider = $this->prophesize(CookieProviderInterface::class); $cookieProvider->getCookie(Argument::type(Request::class))->willReturn(null); - $subscriber = new StoreCookieSubscriber( - $clientContext->reveal(), - $cookieProvider->reveal(), - new EventDispatcher(), - 'custom_name', - '+30 days', - ); - $before = time(); $response = new Response(); - $subscriber->store($this->createResponseEvent(new Request(), $response)); + $this->createSubscriber( + $clientContext->reveal(), + $cookieProvider->reveal(), + cookieName: 'custom_name', + cookieExpiration: '+30 days', + )->store($this->createResponseEvent(new Request(), $response)); $cookies = $response->headers->getCookies(); self::assertCount(1, $cookies); @@ -162,14 +160,9 @@ public function it_does_not_store_a_cookie_when_the_pre_store_event_vetoes_it(): }); $response = new Response(); - $subscriber = new StoreCookieSubscriber( - $clientContext->reveal(), - $cookieProvider->reveal(), - $eventDispatcher, - self::COOKIE_NAME, - '+365 days', + $this->createSubscriber($clientContext->reveal(), $cookieProvider->reveal(), $eventDispatcher)->store( + $this->createResponseEvent(new Request(), $response), ); - $subscriber->store($this->createResponseEvent(new Request(), $response)); self::assertCount(0, $response->headers->getCookies()); } @@ -177,14 +170,11 @@ public function it_does_not_store_a_cookie_when_the_pre_store_event_vetoes_it(): private function createSubscriber( ClientContextInterface $clientContext, CookieProviderInterface $cookieProvider, + EventDispatcherInterface $eventDispatcher = new EventDispatcher(), + string $cookieName = self::COOKIE_NAME, + string $cookieExpiration = '+365 days', ): StoreCookieSubscriber { - return new StoreCookieSubscriber( - $clientContext, - $cookieProvider, - new EventDispatcher(), - self::COOKIE_NAME, - '+365 days', - ); + return new StoreCookieSubscriber($clientContext, $cookieProvider, $eventDispatcher, $cookieName, $cookieExpiration); } private function createResponseEvent( diff --git a/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php index 9af678d..9fefe8e 100644 --- a/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php +++ b/tests/Unit/MetadataProvider/DoctrineOrmBasedMetadataProviderTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Setono\Client\Metadata; use Setono\ClientBundle\Client\LazyChangeAwareMetadata; use Setono\ClientBundle\Entity\Metadata as MetadataEntity; @@ -27,18 +28,9 @@ public function it_returns_lazy_metadata(): void $manager = $this->prophesize(EntityManagerInterface::class); $manager->find(Argument::cetera())->shouldNotBeCalled(); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); - $decorated = $this->prophesize(MetadataProviderInterface::class); - $provider = new DoctrineOrmBasedMetadataProvider( - $decorated->reveal(), - $managerRegistry->reveal(), - MetadataEntity::class, - ); - - $metadata = $provider->getMetadata('some-client-id'); + $metadata = $this->createProvider($this->createManagerRegistry($manager), $decorated)->getMetadata('some-client-id'); self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); self::assertFalse($metadata->isLazyObjectInitialized()); @@ -51,19 +43,10 @@ public function it_initializes_metadata_when_accessed(): void { $manager = $this->prophesize(EntityManagerInterface::class); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); - $decorated = $this->prophesize(MetadataProviderInterface::class); $decorated->getMetadata('some-client-id')->willReturn(new Metadata()); - $provider = new DoctrineOrmBasedMetadataProvider( - $decorated->reveal(), - $managerRegistry->reveal(), - MetadataEntity::class, - ); - - $metadata = $provider->getMetadata('some-client-id'); + $metadata = $this->createProvider($this->createManagerRegistry($manager), $decorated)->getMetadata('some-client-id'); $metadata->set('some-key', 'some-value'); self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); @@ -84,19 +67,10 @@ public function it_loads_the_metadata_from_an_existing_entity(): void $manager = $this->prophesize(EntityManagerInterface::class); $manager->find(MetadataEntity::class, 'some-client-id')->willReturn($entity); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); - $decorated = $this->prophesize(MetadataProviderInterface::class); $decorated->getMetadata(Argument::any())->shouldNotBeCalled(); - $provider = new DoctrineOrmBasedMetadataProvider( - $decorated->reveal(), - $managerRegistry->reveal(), - MetadataEntity::class, - ); - - $metadata = $provider->getMetadata('some-client-id'); + $metadata = $this->createProvider($this->createManagerRegistry($manager), $decorated)->getMetadata('some-client-id'); self::assertSame('bar', $metadata->get('foo')); self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); @@ -111,22 +85,35 @@ public function it_falls_back_to_the_decorated_provider_when_the_client_has_no_e $manager = $this->prophesize(EntityManagerInterface::class); $manager->find(MetadataEntity::class, 'some-client-id')->willReturn(null); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); - $decorated = $this->prophesize(MetadataProviderInterface::class); $decorated->getMetadata('some-client-id')->willReturn(new Metadata(['foo' => 'bar'])); - $provider = new DoctrineOrmBasedMetadataProvider( - $decorated->reveal(), - $managerRegistry->reveal(), - MetadataEntity::class, - ); - - $metadata = $provider->getMetadata('some-client-id'); + $metadata = $this->createProvider($this->createManagerRegistry($manager), $decorated)->getMetadata('some-client-id'); self::assertSame('bar', $metadata->get('foo')); self::assertInstanceOf(LazyChangeAwareMetadata::class, $metadata); self::assertFalse($metadata->isDirty()); } + + /** + * @param ObjectProphecy $manager + * + * @return ObjectProphecy + */ + private function createManagerRegistry(ObjectProphecy $manager): ObjectProphecy + { + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass(MetadataEntity::class)->willReturn($manager); + + return $managerRegistry; + } + + /** + * @param ObjectProphecy $managerRegistry + * @param ObjectProphecy $decorated + */ + private function createProvider(ObjectProphecy $managerRegistry, ObjectProphecy $decorated): DoctrineOrmBasedMetadataProvider + { + return new DoctrineOrmBasedMetadataProvider($decorated->reveal(), $managerRegistry->reveal(), MetadataEntity::class); + } } From 741a25de57e9ec01188c02b727c62035df00999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 26 Oct 2026 11:28:30 +0100 Subject: [PATCH 7/7] Mention symfony-config-test in CLAUDE.md --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3045ea2..3ecdf7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,4 +69,4 @@ setono_client: - Strict types everywhere (`declare(strict_types=1)`); concrete classes are `final`, except the metadata/entity classes (`ChangeAwareMetadata`, `LazyChangeAwareMetadata`, `Entity\Metadata`) which are intentionally extensible. - New services go in `config/services.xml` using the `setono_client..` id scheme, with a `setono_client..default` alias and an interface alias for autowiring. To insert behavior into a chain, add a decorator with an appropriate `decoration-priority` rather than editing existing classes. -- Tests use `matthiasnoback/symfony-dependency-injection-test` (e.g. `AbstractExtensionTestCase`) and Prophecy (`ProphecyTrait`), with the `@test` annotation and snake_case method-name style. Keep the suite at 100% MSI — when adding logic, add a test that kills the corresponding mutant. +- Tests use `matthiasnoback/symfony-dependency-injection-test` (e.g. `AbstractExtensionTestCase`), `matthiasnoback/symfony-config-test` (`ConfigurationTestCaseTrait`) and Prophecy (`ProphecyTrait`), with the `@test` annotation and snake_case method-name style. Keep the suite at 100% MSI — when adding logic, add a test that kills the corresponding mutant.