Repository navigation
Add functional tests that run the bundle with a database #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8f889fb
Conflict with DoctrineBundle 2.11.0-2.11.2
loevgaard bf56c78
Add functional tests that run the bundle in a Symfony application wit…
loevgaard 4923efe
Use Kernel::getEnvironment() in the test kernel
loevgaard c75c3f5
Use Webmozart Assert in the test controller
loevgaard a05affa
Validate the client id with the Uid component in the cookie tests
loevgaard 63907df
Don't depend on the order of JSON keys in the functional tests
loevgaard bbb195f
Harden the functional tests
loevgaard 217e83f
Require DoctrineBundle 2.11.3 instead of conflicting with 2.11.0 to 2…
loevgaard 9691b11
Isolate the functional tests with DAMA DoctrineTestBundle
loevgaard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Setono\ClientBundle\Tests\Functional\App\Controller; | ||
|
|
||
| use Doctrine\DBAL\Connection; | ||
| use Doctrine\ORM\EntityManagerInterface; | ||
| use Doctrine\ORM\Events; | ||
| use Doctrine\ORM\Mapping\ClassMetadata; | ||
| use Doctrine\Persistence\ManagerRegistry; | ||
| use Setono\Client\Client; | ||
| use Setono\ClientBundle\Context\ClientContextInterface; | ||
| use Setono\ClientBundle\Entity\MetadataInterface; | ||
| use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||
| use Symfony\Component\HttpFoundation\Request; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
| use Webmozart\Assert\Assert; | ||
|
|
||
| final class ClientController | ||
| { | ||
| /** | ||
| * @param class-string<MetadataInterface> $metadataClass | ||
| */ | ||
| public function __construct( | ||
| private readonly ClientContextInterface $clientContext, | ||
| private readonly ManagerRegistry $managerRegistry, | ||
| #[Autowire('%setono_client.metadata_class%')] | ||
| private readonly string $metadataClass, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * Uses the client, but not its metadata | ||
| */ | ||
| public function id(): Response | ||
| { | ||
| return new Response($this->clientContext->getClient()->id); | ||
| } | ||
|
|
||
| public function showMetadata(Client $client): JsonResponse | ||
| { | ||
| return new JsonResponse(iterator_to_array($client->metadata)); | ||
| } | ||
|
|
||
| public function setMetadata(Request $request, Client $client): Response | ||
| { | ||
| $ttl = $request->query->get('ttl'); | ||
|
|
||
| $client->metadata->set( | ||
| $request->query->getString('key'), | ||
| $request->query->getString('value'), | ||
| null === $ttl ? null : (int) $ttl, | ||
| ); | ||
|
|
||
| return new Response(); | ||
| } | ||
|
|
||
| public function removeMetadata(Request $request, Client $client): Response | ||
| { | ||
| $client->metadata->remove($request->query->getString('key')); | ||
|
|
||
| return new Response(); | ||
| } | ||
|
|
||
| /** | ||
| * Sets the metadata like setMetadata() does, but simulates a concurrent request from the same client | ||
| * that inserts the client's row right before this request does | ||
| */ | ||
| public function setMetadataConcurrently(Request $request, Client $client): Response | ||
| { | ||
| $client->metadata->set($request->query->getString('key'), $request->query->getString('value')); | ||
|
|
||
| $manager = $this->managerRegistry->getManagerForClass($this->metadataClass); | ||
| Assert::isInstanceOf($manager, EntityManagerInterface::class); | ||
|
|
||
| $manager->getEventManager()->addEventListener(Events::onFlush, new class($manager->getConnection(), $manager->getClassMetadata($this->metadataClass), $client->id) { | ||
| /** | ||
| * @param ClassMetadata<object> $classMetadata | ||
| */ | ||
| public function __construct( | ||
| private readonly Connection $connection, | ||
| private readonly ClassMetadata $classMetadata, | ||
| private readonly string $clientId, | ||
| ) { | ||
| } | ||
|
|
||
| public function onFlush(): void | ||
| { | ||
| $this->connection->insert($this->classMetadata->getTableName(), [ | ||
| $this->classMetadata->getColumnName('clientId') => $this->clientId, | ||
| $this->classMetadata->getColumnName('metadata') => json_encode(['set_by' => 'concurrent request']), | ||
| ]); | ||
| } | ||
| }); | ||
|
|
||
| return new Response(); | ||
| } | ||
| } |
29 changes: 29 additions & 0 deletions
29
tests/Functional/App/EventSubscriber/SkipCookieSubscriber.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Setono\ClientBundle\Tests\Functional\App\EventSubscriber; | ||
|
|
||
| use Setono\ClientBundle\Event\PreStoreCookieEvent; | ||
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
|
|
||
| /** | ||
| * Vetoes the cookie when the request has the skip_cookie query parameter, like an application would do for a visitor | ||
| * that has not consented to cookies | ||
| */ | ||
| final class SkipCookieSubscriber implements EventSubscriberInterface | ||
| { | ||
| public static function getSubscribedEvents(): array | ||
| { | ||
| return [ | ||
| PreStoreCookieEvent::class => 'skipCookie', | ||
| ]; | ||
| } | ||
|
|
||
| public function skipCookie(PreStoreCookieEvent $event): void | ||
| { | ||
| if ($event->request->query->has('skip_cookie')) { | ||
| $event->store = false; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Setono\ClientBundle\Tests\Functional\App; | ||
|
|
||
| use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; | ||
| use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; | ||
| use Doctrine\DBAL\Connection; | ||
| use Setono\ClientBundle\SetonoClientBundle; | ||
| use Setono\ClientBundle\Tests\Functional\App\Controller\ClientController; | ||
| use Symfony\Bundle\FrameworkBundle\FrameworkBundle; | ||
| use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; | ||
| use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; | ||
| use Symfony\Component\HttpKernel\Kernel as BaseKernel; | ||
| use Symfony\Component\HttpKernel\Log\Logger; | ||
| use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; | ||
|
|
||
| final class Kernel extends BaseKernel | ||
| { | ||
| use MicroKernelTrait; | ||
|
|
||
| public function registerBundles(): iterable | ||
| { | ||
| yield new FrameworkBundle(); | ||
| yield new DoctrineBundle(); | ||
| yield new DAMADoctrineTestBundle(); | ||
| yield new SetonoClientBundle(); | ||
| } | ||
|
|
||
| public function getProjectDir(): string | ||
| { | ||
| return __DIR__; | ||
| } | ||
|
|
||
| public function getCacheDir(): string | ||
| { | ||
| return self::getVarDir() . '/cache/' . $this->getEnvironment(); | ||
| } | ||
|
|
||
| public function getLogDir(): string | ||
| { | ||
| return self::getVarDir() . '/log'; | ||
| } | ||
|
|
||
| /** | ||
| * The application is configured in this class, so the config dir only receives the config reference | ||
| * that Symfony 7.4+ dumps in debug mode | ||
| */ | ||
| protected function getConfigDir(): string | ||
| { | ||
| return self::getVarDir() . '/config'; | ||
| } | ||
|
|
||
| /** | ||
| * Each checkout gets its own directory, and so does each of the parallel processes that Infection runs and gives a token. | ||
| * That way, no two test runs share a container or a database | ||
| */ | ||
| public static function getVarDir(): string | ||
| { | ||
| return sprintf('%s/setono-client-bundle-%s%s', sys_get_temp_dir(), hash('xxh3', __DIR__), (string) getenv('TEST_TOKEN')); | ||
| } | ||
|
|
||
| protected function configureContainer(ContainerConfigurator $container): void | ||
| { | ||
| $container->extension('framework', [ | ||
| 'secret' => 'secret', | ||
| 'test' => true, | ||
| 'http_method_override' => false, | ||
| 'handle_all_throwables' => true, | ||
| 'php_errors' => ['log' => true], | ||
| ]); | ||
|
|
||
| // Set the SETONO_CLIENT_BUNDLE_DATABASE_URL environment variable to run the tests against another database. The tests drop | ||
| // the bundle's tables in it. The variable isn't DATABASE_URL, so the tests can't pick up the database of another application | ||
| $container->parameters()->set('env(SETONO_CLIENT_BUNDLE_DATABASE_URL)', 'sqlite:///%kernel.cache_dir%/test.db'); | ||
|
|
||
| $dbal = ['url' => '%env(resolve:SETONO_CLIENT_BUNDLE_DATABASE_URL)%']; | ||
|
|
||
| // DAMA DoctrineTestBundle runs each test in a transaction, so the transactions of the bundle need savepoints. | ||
| // DBAL 4 always uses them and deprecates the option. Like DoctrineBundle, this tells DBAL 3 by its getEventManager() | ||
| if (method_exists(Connection::class, 'getEventManager')) { | ||
| $dbal['use_savepoints'] = true; | ||
| } | ||
|
|
||
| $container->extension('doctrine', [ | ||
| 'dbal' => $dbal, | ||
| 'orm' => [], | ||
| ]); | ||
|
|
||
| $services = $container->services(); | ||
|
|
||
| // Logs warnings and errors to a file instead of the test output. It's in the cache dir, so it's removed with the cache | ||
| $services->set('logger', Logger::class)->args(['warning', '%kernel.cache_dir%/log.txt']); | ||
|
|
||
| // Lets the tests read the queries that a request executed | ||
| $services->alias('test.doctrine.debug_data_holder', 'doctrine.debug_data_holder')->public(); | ||
|
|
||
| $services | ||
| ->defaults()->autowire()->autoconfigure() | ||
| ->load(__NAMESPACE__ . '\\Controller\\', __DIR__ . '/Controller')->tag('controller.service_arguments') | ||
| ->load(__NAMESPACE__ . '\\EventSubscriber\\', __DIR__ . '/EventSubscriber') | ||
| ; | ||
| } | ||
|
|
||
| protected function configureRoutes(RoutingConfigurator $routes): void | ||
| { | ||
| $routes->add('client_id', '/')->controller([ClientController::class, 'id']); | ||
| $routes->add('metadata_show', '/metadata')->controller([ClientController::class, 'showMetadata']); | ||
| $routes->add('metadata_set', '/metadata/set')->controller([ClientController::class, 'setMetadata']); | ||
| $routes->add('metadata_remove', '/metadata/remove')->controller([ClientController::class, 'removeMetadata']); | ||
| $routes->add('metadata_set_concurrently', '/metadata/set-concurrently')->controller([ClientController::class, 'setMetadataConcurrently']); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.