diff --git a/.gitignore b/.gitignore index d021725..b70e5fd 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,6 @@ /.php-cs-fixer.cache .idea/ -docker-compose.yml \ No newline at end of file +.junie/ +docker-compose.yml +.aiignore \ No newline at end of file diff --git a/README.md b/README.md index 08029c7..8a581d9 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ The bundle works out-of-the-box with default configuration. ## Quick Start -### Basic Usage - -Basic Usage Once installed, the bundle automatically: 1. Reads the X-Correlation-ID header from incoming requests @@ -151,7 +148,7 @@ class UserController extends AbstractController #[Route('/api/users', methods: ['GET'])] public function list(): JsonResponse { - $$correlationId = $$this->correlationIdStorage->get(); + $correlationId = $this->correlationIdStorage->get(); $this->logger->info('Fetching users', [ 'correlation_id' => $correlationId, @@ -180,6 +177,7 @@ $this->correlationIdStorage->clear(); ``` ## Monolog Integration + ### Automatic Logging When Monolog integration is enabled (default), the correlation ID is automatically added to all log entries in the `extra` field. **Example log output:** @@ -212,6 +210,33 @@ composer require monolog/monolog ``` If Monolog is not installed, the integration is automatically disabled. +## CLI Integration + +When CLI integration is enabled (default), the bundle manages correlation IDs for Symfony Console commands. + +### Global Option +If `cli.allow_option` is `true`, a global `--correlation-id` option is added to all commands: + +```bash +php bin/console app:my-command --correlation-id=custom-id-123 +``` + +### Automatic ID Generation +If no option is provided, an ID is automatically generated using the configured generator and prefixed with `cli.prefix` (default: `CLI-`): + +**Example output for a generated ID:** `CLI-550e8400-e29b-41d4-a716-446655440000` + +### Access in Commands +You can access the ID in your commands just like in controllers: + +```php +protected function execute(InputInterface $input, OutputInterface $output): int +{ + $correlationId = $this->correlationIdStorage->get(); + // ... +} +``` + ## Advanced Usage ### Custom ID Generator @@ -259,4 +284,4 @@ vendor/bin/phpunit --coverage-html build/coverage This bundle is released under the MIT License. ## Contributing -Contributions are welcome! Please submit a Pull Request. \ No newline at end of file +Contributions are welcome! Please submit a Pull Request. diff --git a/composer.json b/composer.json index f457402..5b6d73f 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/uid": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0" }, "require-dev": { "roave/security-advisories": "dev-latest", diff --git a/config/services.yaml b/config/services.yaml index baab6f5..19ff5ef 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -38,5 +38,16 @@ services: arguments: $storage: '@MdavidDev\SymfonyCorrelationIdBundle\Service\CorrelationIdStorage' $headerName: '%correlation_id.header_name%' + tags: + - { name: kernel.event_subscriber } + + # Console Listener + MdavidDev\SymfonyCorrelationIdBundle\EventListener\ConsoleListener: + arguments: + $storage: '@MdavidDev\SymfonyCorrelationIdBundle\Service\CorrelationIdStorage' + $generator: '@MdavidDev\SymfonyCorrelationIdBundle\Service\Generator\CorrelationIdGeneratorInterface' + $validator: '@MdavidDev\SymfonyCorrelationIdBundle\Validator\CorrelationIdValidator' + $prefix: '%correlation_id.cli.prefix%' + $allowOption: '%correlation_id.cli.allow_option%' tags: - { name: kernel.event_subscriber } \ No newline at end of file diff --git a/src/Console/ApplicationDecorator.php b/src/Console/ApplicationDecorator.php new file mode 100644 index 0000000..0799c00 --- /dev/null +++ b/src/Console/ApplicationDecorator.php @@ -0,0 +1,35 @@ +getName(), $application->getVersion()); + + $this->setCatchExceptions($application->areExceptionsCaught()); + $this->setAutoExit($application->isAutoExitEnabled()); + } + + protected function getDefaultInputDefinition(): InputDefinition + { + $definition = parent::getDefaultInputDefinition(); + + $definition->addOption(new InputOption( + 'correlation-id', + null, + InputOption::VALUE_REQUIRED, + 'Correlation ID for this command execution' + )); + + return $definition; + } +} diff --git a/src/DependencyInjection/Compiler/ConsoleCommandCompilerPass.php b/src/DependencyInjection/Compiler/ConsoleCommandCompilerPass.php new file mode 100644 index 0000000..5ddb872 --- /dev/null +++ b/src/DependencyInjection/Compiler/ConsoleCommandCompilerPass.php @@ -0,0 +1,32 @@ +hasParameter('correlation_id.cli.allow_option') || !$container->getParameter('correlation_id.cli.allow_option')) { + return; + } + + if (!$container->hasDefinition('console.application')) { + return; + } + + if (!$container->hasDefinition(ApplicationDecorator::class)) { + $container->register(ApplicationDecorator::class, ApplicationDecorator::class) + ->setDecoratedService('console.application') + ->setArguments([new Reference('.inner')]) + ->addMethodCall('setDispatcher', [new Reference('event_dispatcher')]) + ->setPublic(false); + } + } +} diff --git a/src/DependencyInjection/Compiler/MonologCompilerPass.php b/src/DependencyInjection/Compiler/MonologCompilerPass.php index a1f319b..16e67f5 100644 --- a/src/DependencyInjection/Compiler/MonologCompilerPass.php +++ b/src/DependencyInjection/Compiler/MonologCompilerPass.php @@ -13,12 +13,10 @@ class MonologCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - // Vérifier si Monolog est disponible if (!$this->isMonologAvailable()) { return; } - // Vérifier si l'intégration Monolog est activée if (!$container->hasParameter('correlation_id.monolog')) { return; } @@ -29,10 +27,8 @@ public function process(ContainerBuilder $container): void return; } - // Enregistrer le processor $this->registerProcessor($container, $monologConfig); - // Ajouter le processor à tous les loggers Monolog $this->addProcessorToLoggers($container); } @@ -59,33 +55,27 @@ private function registerProcessor(ContainerBuilder $container, array $monologCo private function addProcessorToLoggers(ContainerBuilder $container): void { - // Chercher tous les services dont l'ID commence par "monolog.logger" foreach ($container->getDefinitions() as $id => $definition) { - // Filtrer uniquement les vrais loggers Monolog if (!str_starts_with($id, 'monolog.logger')) { continue; } - // Vérifier que c'est bien un logger Monolog $class = $definition->getClass(); if ($class === null) { $class = $id; } - // Résoudre la classe si c'est un paramètre if (str_starts_with($class, '%') && str_ends_with($class, '%')) { $class = $container->getParameter(trim($class, '%')); } - // Vérifier que c'est bien Monolog\Logger ou une sous-classe if ($class !== 'Monolog\\Logger' && !is_subclass_of($class, 'Monolog\\Logger')) { continue; } - // Ajouter le processor $definition->addMethodCall('pushProcessor', [ new Reference(CorrelationIdProcessor::class) ]); } } -} \ No newline at end of file +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c570669..61b5076 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -16,90 +16,90 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->children() - ->scalarNode('header_name') - ->info('HTTP header name for correlation ID') - ->defaultValue('X-Correlation-ID') - ->cannotBeEmpty() - ->end() - ->scalarNode('generator') - ->info('ID generator type: uuid_v4, uuid_v7, ulid, or service ID') - ->defaultValue('uuid_v4') - ->cannotBeEmpty() - ->end() - ->booleanNode('trust_header') - ->info('Trust incoming correlation ID from header') - ->defaultTrue() - ->end() - ->arrayNode('validation') - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled') - ->info('Enable validation of incoming correlation IDs') - ->defaultTrue() - ->end() - ->integerNode('max_length') - ->info('Maximum length for correlation ID') - ->defaultValue(255) - ->min(1) - ->end() - ->scalarNode('pattern') - ->info('Regex pattern to validate correlation ID format') - ->defaultNull() - ->end() - ->end() - ->end() - ->arrayNode('monolog') - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled') - ->info('Enable Monolog integration') - ->defaultTrue() - ->end() - ->scalarNode('key') - ->info('Log context key for correlation ID') - ->defaultValue('correlation_id') - ->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('http_client') - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled') - ->info('Enable HttpClient integration') - ->defaultTrue() - ->end() - ->end() - ->end() - ->arrayNode('messenger') - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled') - ->info('Enable Messenger integration') - ->defaultTrue() - ->end() - ->end() - ->end() - ->arrayNode('cli') - ->addDefaultsIfNotSet() - ->children() - ->booleanNode('enabled') - ->info('Enable CLI integration') - ->defaultTrue() - ->end() - ->scalarNode('prefix') - ->info('Prefix for CLI-generated correlation IDs') - ->defaultValue('CLI-') - ->end() - ->booleanNode('allow_option') - ->info('Allow --correlation-id option in commands') - ->defaultTrue() - ->end() - ->end() - ->end() + ->scalarNode('header_name') + ->info('HTTP header name for correlation ID') + ->defaultValue('X-Correlation-ID') + ->cannotBeEmpty() + ->end() + ->scalarNode('generator') + ->info('ID generator type: uuid_v4, uuid_v7, ulid, or service ID') + ->defaultValue('uuid_v4') + ->cannotBeEmpty() + ->end() + ->booleanNode('trust_header') + ->info('Trust incoming correlation ID from header') + ->defaultTrue() + ->end() + ->arrayNode('validation') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('Enable validation of incoming correlation IDs') + ->defaultTrue() + ->end() + ->integerNode('max_length') + ->info('Maximum length for correlation ID') + ->defaultValue(255) + ->min(1) + ->end() + ->scalarNode('pattern') + ->info('Regex pattern to validate correlation ID format') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('monolog') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('Enable Monolog integration') + ->defaultTrue() + ->end() + ->scalarNode('key') + ->info('Log context key for correlation ID') + ->defaultValue('correlation_id') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->arrayNode('http_client') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('Enable HttpClient integration') + ->defaultTrue() + ->end() + ->end() + ->end() + ->arrayNode('messenger') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('Enable Messenger integration') + ->defaultTrue() + ->end() + ->end() + ->end() + ->arrayNode('cli') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled') + ->info('Enable CLI integration') + ->defaultTrue() + ->end() + ->scalarNode('prefix') + ->info('Prefix for CLI-generated correlation IDs') + ->defaultValue('CLI-') + ->end() + ->booleanNode('allow_option') + ->info('Allow --correlation-id option in commands') + ->defaultTrue() + ->end() + ->end() + ->end() ->end() ; return $treeBuilder; } -} \ No newline at end of file +} diff --git a/src/DependencyInjection/CorrelationIdExtension.php b/src/DependencyInjection/CorrelationIdExtension.php index 1f2bba8..e5d340b 100644 --- a/src/DependencyInjection/CorrelationIdExtension.php +++ b/src/DependencyInjection/CorrelationIdExtension.php @@ -20,12 +20,10 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - // Enregistrer la configuration comme paramètres $container->setParameter('correlation_id.header_name', $config['header_name']); $container->setParameter('correlation_id.generator', $config['generator']); $container->setParameter('correlation_id.trust_header', $config['trust_header']); - // Enregistrer les sous-paramètres de validation $container->setParameter('correlation_id.validation.enabled', $config['validation']['enabled']); $container->setParameter('correlation_id.validation.max_length', $config['validation']['max_length']); $container->setParameter('correlation_id.validation.pattern', $config['validation']['pattern']); @@ -34,17 +32,23 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('correlation_id.http_client', $config['http_client']); $container->setParameter('correlation_id.messenger', $config['messenger']); $container->setParameter('correlation_id.cli', $config['cli']); + $container->setParameter('correlation_id.cli.enabled', $config['cli']['enabled']); + $container->setParameter('correlation_id.cli.prefix', $config['cli']['prefix']); + $container->setParameter('correlation_id.cli.allow_option', $config['cli']['allow_option']); - // Charger les services $loader = new YamlFileLoader( $container, new FileLocator(__DIR__ . '/../../config') ); $loader->load('services.yaml'); + + if (!$config['cli']['enabled']) { + $container->removeDefinition('MdavidDev\SymfonyCorrelationIdBundle\EventListener\ConsoleListener'); + } } public function getAlias(): string { return 'correlation_id'; } -} \ No newline at end of file +} diff --git a/src/EventListener/ConsoleListener.php b/src/EventListener/ConsoleListener.php new file mode 100644 index 0000000..a401138 --- /dev/null +++ b/src/EventListener/ConsoleListener.php @@ -0,0 +1,72 @@ + ['onConsoleCommand', 512], + ConsoleEvents::TERMINATE => ['onConsoleTerminate', -512], + ConsoleEvents::ERROR => ['onConsoleError', -512], + ]; + } + + public function onConsoleCommand(ConsoleCommandEvent $event): void + { + $command = $event->getCommand(); + if ($command === null) { + return; + } + + $correlationId = null; + $input = $event->getInput(); + + if ($this->allowOption && $input->hasParameterOption('--' . self::OPTION_NAME)) { + $value = $input->getParameterOption('--' . self::OPTION_NAME); + if (is_string($value)) { + $correlationId = $this->validator->sanitize($value); + } + } + + if ($correlationId === null) { + $correlationId = $this->prefix . $this->generator->generate(); + } + + $this->storage->set($correlationId); + } + + public function onConsoleTerminate(ConsoleTerminateEvent $event): void + { + $this->storage->clear(); + } + + public function onConsoleError(ConsoleErrorEvent $event): void + { + $this->storage->clear(); + } +} diff --git a/src/EventListener/RequestListener.php b/src/EventListener/RequestListener.php index 03b36ff..47ff33c 100644 --- a/src/EventListener/RequestListener.php +++ b/src/EventListener/RequestListener.php @@ -11,27 +11,27 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; -final readonly class RequestListener implements EventSubscriberInterface +final class RequestListener implements EventSubscriberInterface { public function __construct( - private CorrelationIdStorage $storage, - private CorrelationIdGeneratorInterface $generator, - private CorrelationIdValidator $validator, - private string $headerName, - private bool $trustHeader - ) { + private readonly CorrelationIdStorage $storage, + private readonly CorrelationIdGeneratorInterface $generator, + private readonly CorrelationIdValidator $validator, + private readonly string $headerName, + private readonly bool $trustHeader + ) + { } public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['onKernelRequest', 512], // Priorité haute + KernelEvents::REQUEST => ['onKernelRequest', 512], ]; } public function onKernelRequest(RequestEvent $event): void { - // Ne traiter que la requête principale (pas les sub-requests) if (!$event->isMainRequest()) { return; } @@ -39,12 +39,10 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); $correlationId = null; - // Vérifier si un ID existe déjà dans le storage (peut arriver dans certains cas) if ($this->storage->has()) { return; } - // Essayer de récupérer l'ID depuis le header if ($this->trustHeader && $request->headers->has($this->headerName)) { $headerValue = $request->headers->get($this->headerName); $sanitizedId = $this->validator->sanitize($headerValue); @@ -54,12 +52,10 @@ public function onKernelRequest(RequestEvent $event): void } } - // Si pas d'ID valide, en générer un nouveau if ($correlationId === null) { $correlationId = $this->generator->generate(); } - // Stocker l'ID $this->storage->set($correlationId); } -} \ No newline at end of file +} diff --git a/src/EventListener/ResponseListener.php b/src/EventListener/ResponseListener.php index f0471f2..3939172 100644 --- a/src/EventListener/ResponseListener.php +++ b/src/EventListener/ResponseListener.php @@ -9,12 +9,13 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -final readonly class ResponseListener implements EventSubscriberInterface +final class ResponseListener implements EventSubscriberInterface { public function __construct( - private CorrelationIdStorage $storage, - private string $headerName - ) { + private readonly CorrelationIdStorage $storage, + private readonly string $headerName + ) + { } public static function getSubscribedEvents(): array @@ -26,12 +27,10 @@ public static function getSubscribedEvents(): array public function onKernelResponse(ResponseEvent $event): void { - // Ne traiter que la requête principale (pas les sub-requests) if (!$event->isMainRequest()) { return; } - // Si pas d'ID de corrélation, on ne fait rien if (!$this->storage->has()) { return; } @@ -39,7 +38,6 @@ public function onKernelResponse(ResponseEvent $event): void $correlationId = $this->storage->get(); $response = $event->getResponse(); - // Ajouter l'ID dans le header de réponse $response->headers->set($this->headerName, $correlationId); } -} \ No newline at end of file +} diff --git a/src/Monolog/CorrelationIdProcessor.php b/src/Monolog/CorrelationIdProcessor.php index f493a36..b77ad68 100644 --- a/src/Monolog/CorrelationIdProcessor.php +++ b/src/Monolog/CorrelationIdProcessor.php @@ -8,26 +8,25 @@ use Monolog\LogRecord; use Monolog\Processor\ProcessorInterface; -final readonly class CorrelationIdProcessor implements ProcessorInterface +final class CorrelationIdProcessor implements ProcessorInterface { public function __construct( - private CorrelationIdStorage $storage, - private string $key - ) { + private readonly CorrelationIdStorage $storage, + private readonly string $key + ) + { } public function __invoke(LogRecord $record): LogRecord { - // Si pas d'ID, on ne fait rien if (!$this->storage->has()) { return $record; } $correlationId = $this->storage->get(); - // Ajouter l'ID dans le contexte du log $record->extra[$this->key] = $correlationId; return $record; } -} \ No newline at end of file +} diff --git a/src/Service/CorrelationIdStorage.php b/src/Service/CorrelationIdStorage.php index 65ebbb1..bf7a392 100644 --- a/src/Service/CorrelationIdStorage.php +++ b/src/Service/CorrelationIdStorage.php @@ -14,7 +14,8 @@ final class CorrelationIdStorage public function __construct( private readonly RequestStack $requestStack - ) { + ) + { } /** @@ -24,12 +25,10 @@ public function get(): ?string { $request = $this->requestStack->getCurrentRequest(); - // Si une requête existe, on retourne son ID (ou null si elle n'en a pas) if ($request !== null) { return $request->attributes->get(self::ATTRIBUTE_NAME); } - // Sinon, on utilise le fallback (contexte CLI, worker, etc.) return $this->fallbackId; } @@ -43,7 +42,6 @@ public function set(string $correlationId): void if ($request !== null) { $request->attributes->set(self::ATTRIBUTE_NAME, $correlationId); } else { - // Fallback pour les contextes sans requête (CLI, tests, etc.) $this->fallbackId = $correlationId; } } @@ -67,4 +65,4 @@ public function clear(): void $this->fallbackId = null; } -} \ No newline at end of file +} diff --git a/src/SymfonyCorrelationIdBundle.php b/src/SymfonyCorrelationIdBundle.php index 9436ed2..f8514f6 100644 --- a/src/SymfonyCorrelationIdBundle.php +++ b/src/SymfonyCorrelationIdBundle.php @@ -4,6 +4,7 @@ namespace MdavidDev\SymfonyCorrelationIdBundle; +use MdavidDev\SymfonyCorrelationIdBundle\DependencyInjection\Compiler\ConsoleCommandCompilerPass; use MdavidDev\SymfonyCorrelationIdBundle\DependencyInjection\Compiler\MonologCompilerPass; use MdavidDev\SymfonyCorrelationIdBundle\DependencyInjection\CorrelationIdExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -28,5 +29,6 @@ public function build(ContainerBuilder $container): void parent::build($container); $container->addCompilerPass(new MonologCompilerPass()); + $container->addCompilerPass(new ConsoleCommandCompilerPass()); } -} \ No newline at end of file +} diff --git a/src/Validator/CorrelationIdValidator.php b/src/Validator/CorrelationIdValidator.php index 6f6c22e..675454b 100644 --- a/src/Validator/CorrelationIdValidator.php +++ b/src/Validator/CorrelationIdValidator.php @@ -4,36 +4,30 @@ namespace MdavidDev\SymfonyCorrelationIdBundle\Validator; -final readonly class CorrelationIdValidator +final class CorrelationIdValidator { public function __construct( - private bool $enabled, - private int $maxLength, - private ?string $pattern - ) { + private readonly bool $enabled, + private readonly int $maxLength, + private readonly ?string $pattern + ) + { } - /** - * Validate if a correlation ID is valid. - */ public function isValid(?string $correlationId): bool { - // Si la validation est désactivée, tout est valide if (!$this->enabled) { return true; } - // Null ou chaîne vide = invalide if ($correlationId === null || $correlationId === '') { return false; } - // Vérifier la longueur max if (mb_strlen($correlationId) > $this->maxLength) { return false; } - // Vérifier le pattern si défini if ($this->pattern !== null) { return preg_match($this->pattern, $correlationId) === 1; } @@ -41,22 +35,16 @@ public function isValid(?string $correlationId): bool return true; } - /** - * Validate and sanitize a correlation ID. - * Returns the sanitized ID if valid, null otherwise. - */ public function sanitize(?string $correlationId): ?string { - // Trim d'abord (si pas null) if ($correlationId !== null) { $correlationId = trim($correlationId); } - // Puis valider if (!$this->isValid($correlationId)) { return null; } return $correlationId; } -} \ No newline at end of file +} diff --git a/tests/Functional/BundleInitializationTest.php b/tests/Functional/BundleInitializationTest.php index 8e84dab..33ce9e2 100644 --- a/tests/Functional/BundleInitializationTest.php +++ b/tests/Functional/BundleInitializationTest.php @@ -31,18 +31,14 @@ public function testBundlePath(): void $bundle = new SymfonyCorrelationIdBundle(); $path = $bundle->getPath(); - // Vérifie que le path existe $this->assertDirectoryExists($path); - // Vérifie que le path contient bien le dossier src/ $this->assertDirectoryExists($path . '/src'); - // Vérifie que le fichier SymfonyCorrelationIdBundle.php existe dans src/ $this->assertFileExists($path . '/src/SymfonyCorrelationIdBundle.php'); } } -// Kernel de test minimaliste class CorrelationIdTestKernel extends Kernel { public function registerBundles(): array @@ -75,4 +71,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/EventListener/RequestListenerTest.php b/tests/Functional/EventListener/RequestListenerTest.php index 81819f6..bc10054 100644 --- a/tests/Functional/EventListener/RequestListenerTest.php +++ b/tests/Functional/EventListener/RequestListenerTest.php @@ -44,16 +44,13 @@ public function testRequestListenerGeneratesIdWhenNoHeader(): void $storage = $container->get(CorrelationIdStorage::class); $listener = $container->get(RequestListener::class); - // Créer une requête sans header $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // Simuler l'événement $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); - // Vérifier qu'un ID a été généré $this->assertTrue($storage->has()); $this->assertNotNull($storage->get()); $this->assertMatchesRegularExpression( @@ -73,17 +70,14 @@ public function testRequestListenerUsesHeaderWhenPresent(): void $storage = $container->get(CorrelationIdStorage::class); $listener = $container->get(RequestListener::class); - // Créer une requête avec header $request = Request::create('/test'); $request->headers->set('X-Correlation-ID', 'my-custom-id-123'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // Simuler l'événement $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); - // Vérifier que l'ID du header est utilisé $this->assertTrue($storage->has()); $this->assertSame('my-custom-id-123', $storage->get()); @@ -99,17 +93,14 @@ public function testRequestListenerGeneratesNewIdWhenHeaderInvalid(): void $storage = $container->get(CorrelationIdStorage::class); $listener = $container->get(RequestListener::class); - // Créer une requête avec header invalide (vide) $request = Request::create('/test'); $request->headers->set('X-Correlation-ID', ''); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // Simuler l'événement $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); - // Vérifier qu'un nouvel ID a été généré $this->assertTrue($storage->has()); $this->assertNotNull($storage->get()); $this->assertNotEmpty($storage->get()); @@ -139,7 +130,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void 'test' => true, ]); - // Rendre request_stack public pour les tests if ($container->hasAlias('request_stack')) { $container->getAlias('request_stack')->setPublic(true); } @@ -177,4 +167,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/EventListener/ResponseListenerTest.php b/tests/Functional/EventListener/ResponseListenerTest.php index 9127c77..b6743e3 100644 --- a/tests/Functional/EventListener/ResponseListenerTest.php +++ b/tests/Functional/EventListener/ResponseListenerTest.php @@ -45,20 +45,16 @@ public function testResponseListenerAddsHeaderToResponse(): void $storage = $container->get(CorrelationIdStorage::class); $listener = $container->get(ResponseListener::class); - // Créer une requête et définir un ID $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); $storage->set('functional-test-id'); - // Créer une réponse $response = new Response('OK', 200); $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); - // Appeler le listener $listener->onKernelResponse($event); - // Vérifier que le header est présent $this->assertTrue($response->headers->has('X-Correlation-ID')); $this->assertSame('functional-test-id', $response->headers->get('X-Correlation-ID')); @@ -73,19 +69,15 @@ public function testResponseListenerDoesNotAddHeaderWhenNoId(): void $container = $kernel->getContainer(); $listener = $container->get(ResponseListener::class); - // Créer une requête SANS définir d'ID $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // Créer une réponse $response = new Response('OK', 200); $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); - // Appeler le listener $listener->onKernelResponse($event); - // Vérifier que le header n'est PAS présent $this->assertFalse($response->headers->has('X-Correlation-ID')); $kernel->shutdown(); @@ -113,7 +105,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void 'test' => true, ]); - // Rendre request_stack public pour les tests if ($container->hasAlias('request_stack')) { $container->getAlias('request_stack')->setPublic(true); } @@ -151,4 +142,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/Integration/ConsoleIntegrationTest.php b/tests/Functional/Integration/ConsoleIntegrationTest.php new file mode 100644 index 0000000..d8fa18a --- /dev/null +++ b/tests/Functional/Integration/ConsoleIntegrationTest.php @@ -0,0 +1,170 @@ +boot(); + + $container = $kernel->getContainer(); + /** @var CorrelationIdStorage $storage */ + $storage = $container->get(CorrelationIdStorage::class); + + $application = new Application(); + /** @var EventDispatcherInterface $dispatcher */ + $dispatcher = $container->get('event_dispatcher'); + $application->setDispatcher($dispatcher); + $application->setAutoExit(false); + + $command = new class($storage) extends Command { + public function __construct(private readonly CorrelationIdStorage $storage) + { + parent::__construct('test:command'); + } + + protected function execute($input, $output): int + { + $output->writeln('ID: ' . ($this->storage->get() ?? 'NULL')); + return Command::SUCCESS; + } + }; + $application->addCommands([$command]); + + $this->assertFalse($storage->has()); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new NullOutput(); + $application->run($input, $output); + + $this->assertFalse($storage->has()); + + $kernel->shutdown(); + } + + /** + * @throws Exception + */ + public function testConsoleCommandUsesProvidedOption(): void + { + $kernel = new ConsoleTestKernel('test', true); + $kernel->boot(); + + $container = $kernel->getContainer(); + /** @var CorrelationIdStorage $storage */ + $storage = $container->get(CorrelationIdStorage::class); + + /** @var Application $application */ + $application = $container->get('console.application'); + $application->setAutoExit(false); + + $capturedId = null; + $command = new class($storage, $capturedId) extends Command { + public ?string $capturedId = null; + public function __construct(private readonly CorrelationIdStorage $storage, &$capturedId) + { + parent::__construct('test:option'); + $this->capturedId = &$capturedId; + } + + protected function execute($input, $output): int + { + $this->capturedId = $this->storage->get(); + return Command::SUCCESS; + } + }; + $application->addCommands([$command]); + + $input = new ArrayInput([ + 'command' => 'test:option', + '--correlation-id' => 'manual-id-123' + ]); + $application->run($input, new NullOutput()); + + $this->assertSame('manual-id-123', $capturedId); + $this->assertFalse($storage->has()); + + $kernel->shutdown(); + } +} + +class ConsoleTestKernel extends Kernel +{ + public function registerBundles(): array + { + return [ + new FrameworkBundle(), + new SymfonyCorrelationIdBundle(), + ]; + } + + /** + * @throws Exception + */ + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'secret' => 'test-secret', + 'test' => true, + ]); + + $container->register('console.application', Application::class) + ->setPublic(true) + ->addMethodCall('setDispatcher', [new Reference('event_dispatcher')]); + + if ($container->hasAlias(CorrelationIdStorage::class)) { + $container->getAlias(CorrelationIdStorage::class)->setPublic(true); + } + }); + } + + protected function build(ContainerBuilder $container): void + { + parent::build($container); + + $container->addCompilerPass(new class implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + foreach ($container->getDefinitions() as $id => $definition) { + if (str_starts_with($id, 'MdavidDev\\SymfonyCorrelationIdBundle\\')) { + $definition->setPublic(true); + } + } + } + }); + } + + public function getCacheDir(): string + { + return sys_get_temp_dir() . '/symfony-correlation-id-bundle/cache/' . spl_object_hash($this); + } + + public function getLogDir(): string + { + return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; + } +} diff --git a/tests/Functional/Integration/FullCycleTest.php b/tests/Functional/Integration/FullCycleTest.php index f99f04d..26fd7c8 100644 --- a/tests/Functional/Integration/FullCycleTest.php +++ b/tests/Functional/Integration/FullCycleTest.php @@ -33,16 +33,13 @@ public function testFullCycleWithoutIncomingHeader(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer une requête SANS header $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // 2. Traiter la requête $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 3. Vérifier qu'un ID a été généré $this->assertTrue($storage->has()); $generatedId = $storage->get(); $this->assertNotNull($generatedId); @@ -51,12 +48,10 @@ public function testFullCycleWithoutIncomingHeader(): void $generatedId ); - // 4. Créer et traiter la réponse $response = new Response('OK', 200); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $responseListener->onKernelResponse($responseEvent); - // 5. Vérifier que l'ID est dans le header de réponse $this->assertTrue($response->headers->has('X-Correlation-ID')); $this->assertSame($generatedId, $response->headers->get('X-Correlation-ID')); @@ -73,26 +68,21 @@ public function testFullCycleWithIncomingHeader(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer une requête AVEC header $request = Request::create('/test'); $request->headers->set('X-Correlation-ID', 'incoming-id-from-client'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // 2. Traiter la requête $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 3. Vérifier que l'ID du header a été utilisé $this->assertTrue($storage->has()); $this->assertSame('incoming-id-from-client', $storage->get()); - // 4. Créer et traiter la réponse $response = new Response('OK', 200); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $responseListener->onKernelResponse($responseEvent); - // 5. Vérifier que le même ID est dans le header de réponse $this->assertTrue($response->headers->has('X-Correlation-ID')); $this->assertSame('incoming-id-from-client', $response->headers->get('X-Correlation-ID')); @@ -109,29 +99,24 @@ public function testFullCycleWithInvalidIncomingHeader(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer une requête avec header INVALIDE $request = Request::create('/test'); - $request->headers->set('X-Correlation-ID', ''); // Vide = invalide + $request->headers->set('X-Correlation-ID', ''); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // 2. Traiter la requête $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 3. Vérifier qu'un nouvel ID a été généré (pas le header invalide) $this->assertTrue($storage->has()); $generatedId = $storage->get(); $this->assertNotNull($generatedId); $this->assertNotEmpty($generatedId); $this->assertNotSame('', $generatedId); - // 4. Créer et traiter la réponse $response = new Response('OK', 200); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $responseListener->onKernelResponse($responseEvent); - // 5. Vérifier que l'ID généré est dans le header de réponse $this->assertTrue($response->headers->has('X-Correlation-ID')); $this->assertSame($generatedId, $response->headers->get('X-Correlation-ID')); @@ -148,27 +133,22 @@ public function testFullCycleWithTrustHeaderDisabled(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer une requête avec header $request = Request::create('/test'); $request->headers->set('X-Correlation-ID', 'header-should-be-ignored'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // 2. Traiter la requête $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 3. Vérifier qu'un nouvel ID a été généré (header ignoré) $this->assertTrue($storage->has()); $generatedId = $storage->get(); $this->assertNotSame('header-should-be-ignored', $generatedId); - // 4. Créer et traiter la réponse $response = new Response('OK', 200); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $responseListener->onKernelResponse($responseEvent); - // 5. Vérifier que l'ID généré est dans le header de réponse $this->assertTrue($response->headers->has('X-Correlation-ID')); $this->assertSame($generatedId, $response->headers->get('X-Correlation-ID')); @@ -185,26 +165,21 @@ public function testFullCycleWithCustomHeaderName(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer une requête avec header custom $request = Request::create('/test'); $request->headers->set('X-Request-ID', 'custom-header-id'); $requestStack = $container->get('request_stack'); $requestStack->push($request); - // 2. Traiter la requête $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 3. Vérifier que l'ID du header custom a été utilisé $this->assertTrue($storage->has()); $this->assertSame('custom-header-id', $storage->get()); - // 4. Créer et traiter la réponse $response = new Response('OK', 200); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $responseListener->onKernelResponse($responseEvent); - // 5. Vérifier que le header custom est dans la réponse $this->assertTrue($response->headers->has('X-Request-ID')); $this->assertSame('custom-header-id', $response->headers->get('X-Request-ID')); $this->assertFalse($response->headers->has('X-Correlation-ID')); @@ -222,7 +197,6 @@ public function testSubRequestsKeepSameId(): void $requestListener = $container->get(RequestListener::class); $responseListener = $container->get(ResponseListener::class); - // 1. Créer et traiter la requête PRINCIPALE $mainRequest = Request::create('/main'); $requestStack = $container->get('request_stack'); $requestStack->push($mainRequest); @@ -233,38 +207,28 @@ public function testSubRequestsKeepSameId(): void $mainId = $storage->get(); $this->assertNotNull($mainId); - // 2. Créer une SUB-REQUEST $subRequest = Request::create('/sub'); $requestStack->push($subRequest); - // 3. Le listener doit IGNORER les sub-requests $subRequestEvent = new RequestEvent($kernel, $subRequest, HttpKernelInterface::SUB_REQUEST); $requestListener->onKernelRequest($subRequestEvent); - // 4. Le storage cherche dans la requête courante (sub-request) - // qui n'a pas l'ID, donc retourne null $this->assertNull($storage->get()); - // 5. Traiter la réponse de la sub-request $subResponse = new Response('SUB OK', 200); $subResponseEvent = new ResponseEvent($kernel, $subRequest, HttpKernelInterface::SUB_REQUEST, $subResponse); $responseListener->onKernelResponse($subResponseEvent); - // 6. La sub-response ne doit PAS avoir le header (sub-request ignorée) $this->assertFalse($subResponse->headers->has('X-Correlation-ID')); - // 7. Revenir à la requête principale - $requestStack->pop(); // Retirer sub-request + $requestStack->pop(); - // 8. Maintenant on retrouve l'ID de la requête principale $this->assertSame($mainId, $storage->get()); - // 9. Traiter la réponse principale $mainResponse = new Response('MAIN OK', 200); $mainResponseEvent = new ResponseEvent($kernel, $mainRequest, HttpKernelInterface::MAIN_REQUEST, $mainResponse); $responseListener->onKernelResponse($mainResponseEvent); - // 10. La main response doit avoir le header $this->assertTrue($mainResponse->headers->has('X-Correlation-ID')); $this->assertSame($mainId, $mainResponse->headers->get('X-Correlation-ID')); @@ -272,7 +236,7 @@ public function testSubRequestsKeepSameId(): void } } -// Kernel par défaut +// Default Kernel class FullCycleTestKernel extends Kernel { public function registerBundles(): array @@ -461,4 +425,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/Integration/MonologFullCycleTest.php b/tests/Functional/Integration/MonologFullCycleTest.php index 90db8f4..b778db5 100644 --- a/tests/Functional/Integration/MonologFullCycleTest.php +++ b/tests/Functional/Integration/MonologFullCycleTest.php @@ -36,7 +36,6 @@ public function testCorrelationIdAppearsInLogs(): void $logger = $container->get('test.logger'); $testHandler = $container->get('test.handler'); - // 1. Créer et traiter une requête $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); @@ -47,10 +46,8 @@ public function testCorrelationIdAppearsInLogs(): void $correlationId = $storage->get(); $this->assertNotNull($correlationId); - // 2. Écrire un log $logger->info('Test log message', ['user_id' => 123]); - // 3. Vérifier que le log contient l'ID de corrélation $records = $testHandler->getRecords(); $this->assertCount(1, $records); @@ -75,7 +72,6 @@ public function testCorrelationIdAppearsWithCustomKey(): void $logger = $container->get('test.logger'); $testHandler = $container->get('test.handler'); - // 1. Créer et traiter une requête $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); @@ -86,10 +82,8 @@ public function testCorrelationIdAppearsWithCustomKey(): void $correlationId = $storage->get(); $this->assertNotNull($correlationId); - // 2. Écrire un log $logger->warning('Custom key test'); - // 3. Vérifier que le log contient l'ID avec la clé custom $records = $testHandler->getRecords(); $this->assertCount(1, $records); @@ -110,7 +104,6 @@ public function testLogsWithoutCorrelationIdWhenNoRequest(): void $logger = $container->get('test.logger'); $testHandler = $container->get('test.handler'); - // Pas de requête, pas d'ID $logger->error('Error without correlation ID'); $records = $testHandler->getRecords(); @@ -134,7 +127,6 @@ public function testMultipleLogsHaveSameCorrelationId(): void $logger = $container->get('test.logger'); $testHandler = $container->get('test.handler'); - // 1. Créer et traiter une requête $request = Request::create('/test'); $requestStack = $container->get('request_stack'); $requestStack->push($request); @@ -144,13 +136,11 @@ public function testMultipleLogsHaveSameCorrelationId(): void $correlationId = $storage->get(); - // 2. Écrire plusieurs logs $logger->debug('First log'); $logger->info('Second log'); $logger->warning('Third log'); $logger->error('Fourth log'); - // 3. Vérifier que tous les logs ont le même ID $records = $testHandler->getRecords(); $this->assertCount(4, $records); @@ -172,7 +162,6 @@ public function testCorrelationIdWithIncomingHeader(): void $logger = $container->get('test.logger'); $testHandler = $container->get('test.handler'); - // 1. Créer une requête AVEC header $request = Request::create('/test'); $request->headers->set('X-Correlation-ID', 'incoming-correlation-123'); $requestStack = $container->get('request_stack'); @@ -181,10 +170,8 @@ public function testCorrelationIdWithIncomingHeader(): void $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestListener->onKernelRequest($requestEvent); - // 2. Écrire un log $logger->info('Log with incoming ID'); - // 3. Vérifier que l'ID du header est dans le log $records = $testHandler->getRecords(); $this->assertCount(1, $records); @@ -196,7 +183,7 @@ public function testCorrelationIdWithIncomingHeader(): void } } -// Kernel de test avec Monolog activé +// Test kernel with Monolog enabled class MonologFullCycleTestKernel extends Kernel { public function registerBundles(): array @@ -225,7 +212,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ], ]); - // Créer un logger de test avec TestHandler $container->register('test.handler', TestHandler::class) ->addArgument(Level::Debug) ->setPublic(true); @@ -275,7 +261,7 @@ public function getLogDir(): string } } -// Kernel avec clé custom +// Kernel with custom key class MonologFullCycleTestKernelCustomKey extends Kernel { public function registerBundles(): array @@ -351,4 +337,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/Service/CorrelationIdStorageTest.php b/tests/Functional/Service/CorrelationIdStorageTest.php index d199322..2c81dc0 100644 --- a/tests/Functional/Service/CorrelationIdStorageTest.php +++ b/tests/Functional/Service/CorrelationIdStorageTest.php @@ -39,19 +39,15 @@ public function testStorageServiceCanStoreAndRetrieveId(): void $container = $kernel->getContainer(); $storage = $container->get(CorrelationIdStorage::class); - // Initialement, pas d'ID $this->assertFalse($storage->has()); $this->assertNull($storage->get()); - // Définir un ID $correlationId = 'test-functional-id-789'; $storage->set($correlationId); - // Vérifier qu'on peut le récupérer $this->assertTrue($storage->has()); $this->assertSame($correlationId, $storage->get()); - // Clear et vérifier $storage->clear(); $this->assertFalse($storage->has()); $this->assertNull($storage->get()); @@ -87,7 +83,6 @@ protected function build(ContainerBuilder $container): void { parent::build($container); - // Rendre les services publics pour les tests $container->addCompilerPass(new class implements CompilerPassInterface { public function process(ContainerBuilder $container): void { @@ -115,4 +110,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Functional/Service/GeneratorServiceTest.php b/tests/Functional/Service/GeneratorServiceTest.php index 5066297..531d526 100644 --- a/tests/Functional/Service/GeneratorServiceTest.php +++ b/tests/Functional/Service/GeneratorServiceTest.php @@ -80,18 +80,15 @@ protected function build(ContainerBuilder $container): void { parent::build($container); - // Ajouter un CompilerPass pour rendre les services publics pour les tests $container->addCompilerPass(new class implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - // Rendre le service public pour les tests foreach ($container->getDefinitions() as $id => $definition) { if (str_starts_with($id, 'MdavidDev\\SymfonyCorrelationIdBundle\\')) { $definition->setPublic(true); } } - // Rendre aussi les alias publics foreach ($container->getAliases() as $id => $alias) { if (str_starts_with($id, 'MdavidDev\\SymfonyCorrelationIdBundle\\')) { $alias->setPublic(true); @@ -110,4 +107,4 @@ public function getLogDir(): string { return sys_get_temp_dir() . '/symfony-correlation-id-bundle/logs'; } -} \ No newline at end of file +} diff --git a/tests/Unit/DependencyInjection/Compiler/ConsoleCommandCompilerPassTest.php b/tests/Unit/DependencyInjection/Compiler/ConsoleCommandCompilerPassTest.php new file mode 100644 index 0000000..d0d1dd7 --- /dev/null +++ b/tests/Unit/DependencyInjection/Compiler/ConsoleCommandCompilerPassTest.php @@ -0,0 +1,84 @@ +compilerPass = new ConsoleCommandCompilerPass(); + $this->container = new ContainerBuilder(); + } + + public function testProcessDoesNothingIfCliOptionIsDisabled(): void + { + $this->container->setParameter('correlation_id.cli.allow_option', false); + $this->container->register('console.application', 'Symfony\Component\Console\Application'); + + $this->compilerPass->process($this->container); + + $this->assertFalse($this->container->hasDefinition(ApplicationDecorator::class)); + } + + public function testProcessDoesNothingIfCliOptionParameterIsMissing(): void + { + $this->container->register('console.application', 'Symfony\Component\Console\Application'); + + $this->compilerPass->process($this->container); + + $this->assertFalse($this->container->hasDefinition(ApplicationDecorator::class)); + } + + public function testProcessDoesNothingIfConsoleApplicationDefinitionIsMissing(): void + { + $this->container->setParameter('correlation_id.cli.allow_option', true); + + $this->compilerPass->process($this->container); + + $this->assertFalse($this->container->hasDefinition(ApplicationDecorator::class)); + } + + public function testProcessRegistersDecorator(): void + { + $this->container->setParameter('correlation_id.cli.allow_option', true); + $this->container->register('console.application', 'Symfony\Component\Console\Application'); + + $this->compilerPass->process($this->container); + + $this->assertTrue($this->container->hasDefinition(ApplicationDecorator::class)); + $definition = $this->container->getDefinition(ApplicationDecorator::class); + + $this->assertSame('console.application', $definition->getDecoratedService()[0]); + $this->assertSame('.inner', (string) $definition->getArgument(0)); + + $methodCalls = $definition->getMethodCalls(); + $this->assertCount(1, $methodCalls); + $this->assertSame('setDispatcher', $methodCalls[0][0]); + $this->assertSame('event_dispatcher', (string) $methodCalls[0][1][0]); + } + + public function testProcessDoesNotRegisterDecoratorIfAlreadyDefined(): void + { + $this->container->setParameter('correlation_id.cli.allow_option', true); + $this->container->register('console.application', 'Symfony\Component\Console\Application'); + + $manualDefinition = new Definition(ApplicationDecorator::class); + $this->container->setDefinition(ApplicationDecorator::class, $manualDefinition); + + $this->compilerPass->process($this->container); + + $this->assertSame($manualDefinition, $this->container->getDefinition(ApplicationDecorator::class)); + $this->assertEmpty($manualDefinition->getMethodCalls()); + } +} diff --git a/tests/Unit/DependencyInjection/Compiler/MonologCompilerPassTest.php b/tests/Unit/DependencyInjection/Compiler/MonologCompilerPassTest.php index 64f2d68..b23c479 100644 --- a/tests/Unit/DependencyInjection/Compiler/MonologCompilerPassTest.php +++ b/tests/Unit/DependencyInjection/Compiler/MonologCompilerPassTest.php @@ -21,10 +21,8 @@ public function testRegistersProcessorWhenMonologIsAvailableAndEnabled(): void { $container = new ContainerBuilder(); - // Simuler la présence du service CorrelationIdStorage $container->register(CorrelationIdStorage::class, CorrelationIdStorage::class); - // Simuler la configuration $container->setParameter('correlation_id.monolog', [ 'enabled' => true, 'key' => 'correlation_id', @@ -33,17 +31,14 @@ public function testRegistersProcessorWhenMonologIsAvailableAndEnabled(): void $pass = new MonologCompilerPass(); $pass->process($container); - // Vérifier que le processor est enregistré $this->assertTrue($container->hasDefinition(CorrelationIdProcessor::class)); $definition = $container->getDefinition(CorrelationIdProcessor::class); - // Vérifier les arguments $this->assertCount(2, $definition->getArguments()); $this->assertInstanceOf(Reference::class, $definition->getArgument(0)); $this->assertSame('correlation_id', $definition->getArgument(1)); - // Vérifier le tag $this->assertTrue($definition->hasTag('monolog.processor')); } @@ -96,7 +91,6 @@ public function testHandlesInvalidMonologParameter(): void { $container = new ContainerBuilder(); - // Paramètre invalide (pas un tableau) $container->setParameter('correlation_id.monolog', 'invalid'); $pass = new MonologCompilerPass(); @@ -111,13 +105,11 @@ public function testHandlesMonologConfigWithoutEnabledKey(): void $container->setParameter('correlation_id.monolog', [ 'key' => 'correlation_id', - // 'enabled' manquant ]); $pass = new MonologCompilerPass(); $pass->process($container); - // Sans 'enabled' ou avec 'enabled' = false, ne doit pas enregistrer $this->assertFalse($container->hasDefinition(CorrelationIdProcessor::class)); } @@ -131,17 +123,15 @@ public function testDoesNotRegisterProcessorWhenMonologNotAvailable(): void 'key' => 'correlation_id', ]); - // Créer un mock du CompilerPass qui simule l'absence de Monolog $pass = new class extends MonologCompilerPass { protected function isMonologAvailable(): bool { - return false; // Simuler l'absence de Monolog + return false; } }; $pass->process($container); - // Ne doit pas enregistrer le processor si Monolog n'est pas disponible $this->assertFalse($container->hasDefinition(CorrelationIdProcessor::class)); } @@ -152,12 +142,10 @@ public function testIsMonologAvailableReturnsTrue(): void { $pass = new MonologCompilerPass(); - // Utiliser la réflexion pour tester la méthode protégée $reflection = new ReflectionClass($pass); $method = $reflection->getMethod('isMonologAvailable'); $method->setAccessible(true); - // Monolog est disponible dans les tests $this->assertTrue($method->invoke($pass)); } @@ -172,21 +160,17 @@ public function testAddsProcessorToAllTaggedLoggers(): void 'key' => 'correlation_id', ]); - // Simuler plusieurs loggers Monolog avec des IDs qui commencent par "monolog.logger" - // ET avec la classe Monolog\Logger explicitement définie $logger1 = $container->register('monolog.logger.app'); $logger1->setClass(Logger::class); $logger2 = $container->register('monolog.logger.security'); $logger2->setClass(Logger::class); - // Ajouter aussi un service qui ne doit PAS recevoir le processor $container->register('some.other.service', stdClass::class); $pass = new MonologCompilerPass(); $pass->process($container); - // Vérifier que le processor a été ajouté aux deux loggers $logger1Def = $container->getDefinition('monolog.logger.app'); $logger2Def = $container->getDefinition('monolog.logger.security'); @@ -196,14 +180,12 @@ public function testAddsProcessorToAllTaggedLoggers(): void $this->assertNotEmpty($calls1, 'Logger app should have method calls'); $this->assertNotEmpty($calls2, 'Logger security should have method calls'); - // Vérifier qu'il y a un appel à pushProcessor $pushProcessorCalls1 = array_filter($calls1, fn($call) => $call[0] === 'pushProcessor'); $pushProcessorCalls2 = array_filter($calls2, fn($call) => $call[0] === 'pushProcessor'); $this->assertCount(1, $pushProcessorCalls1, 'Logger app should have one pushProcessor call'); $this->assertCount(1, $pushProcessorCalls2, 'Logger security should have one pushProcessor call'); - // Vérifier que l'autre service n'a PAS de pushProcessor $otherServiceDef = $container->getDefinition('some.other.service'); $otherCalls = $otherServiceDef->getMethodCalls(); $this->assertEmpty($otherCalls, 'Other service should not have method calls'); @@ -220,14 +202,12 @@ public function testSkipsServicesWithoutMonologLoggerPrefix(): void 'key' => 'correlation_id', ]); - // Service qui ne commence pas par "monolog.logger" $otherService = $container->register('app.logger'); $otherService->setClass(Logger::class); $pass = new MonologCompilerPass(); $pass->process($container); - // Ce service ne doit PAS avoir le processor $calls = $otherService->getMethodCalls(); $this->assertEmpty($calls); } @@ -243,14 +223,12 @@ public function testSkipsServicesWithNonMonologClass(): void 'key' => 'correlation_id', ]); - // Service avec le bon ID mais pas la bonne classe $notALogger = $container->register('monolog.logger.fake'); $notALogger->setClass(stdClass::class); $pass = new MonologCompilerPass(); $pass->process($container); - // Ce service ne doit PAS avoir le processor (ce n'est pas un Monolog\Logger) $calls = $notALogger->getMethodCalls(); $this->assertEmpty($calls); } @@ -266,17 +244,14 @@ public function testHandlesLoggerWithParameterClass(): void 'key' => 'correlation_id', ]); - // Définir le paramètre correctement avec clé et valeur $container->setParameter('monolog.logger.class', Logger::class); - // Logger dont la classe est définie par un paramètre $logger = $container->register('monolog.logger.param'); $logger->setClass('%monolog.logger.class%'); $pass = new MonologCompilerPass(); $pass->process($container); - // Ce logger DOIT avoir le processor $calls = $logger->getMethodCalls(); $this->assertNotEmpty($calls); @@ -295,16 +270,12 @@ public function testSkipsLoggerWithNullClassAndNonMonologId(): void 'key' => 'correlation_id', ]); - // Service avec le bon préfixe mais classe null et ID qui n'est pas Monolog\Logger $logger = $container->register('monolog.logger.null_class'); - // Ne pas définir de classe (getClass() retournera null) - // L'ID sera utilisé comme fallback, mais ce n'est pas une classe valide $pass = new MonologCompilerPass(); $pass->process($container); - // Ce service ne doit PAS avoir le processor car l'ID n'est pas une classe Monolog $calls = $logger->getMethodCalls(); $this->assertEmpty($calls); } -} \ No newline at end of file +} diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index 21ba643..7c1936c 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -85,11 +85,9 @@ public function testPartialConfiguration(): void $config = $this->processor->processConfiguration($this->configuration, $partialConfig); - // Vérifie que header_name est personnalisé $this->assertSame('X-Trace-ID', $config['header_name']); - // Vérifie que les autres valeurs sont les valeurs par défaut $this->assertSame('uuid_v4', $config['generator']); $this->assertTrue($config['trust_header']); } -} \ No newline at end of file +} diff --git a/tests/Unit/DependencyInjection/CorrelationIdExtensionTest.php b/tests/Unit/DependencyInjection/CorrelationIdExtensionTest.php new file mode 100644 index 0000000..3c7ce08 --- /dev/null +++ b/tests/Unit/DependencyInjection/CorrelationIdExtensionTest.php @@ -0,0 +1,36 @@ +assertSame('correlation_id', $extension->getAlias()); + } + + public function testLoadRemovesConsoleListenerWhenCliDisabled(): void + { + $container = new ContainerBuilder(); + $extension = new CorrelationIdExtension(); + + $configs = [ + 'correlation_id' => [ + 'cli' => [ + 'enabled' => false + ] + ] + ]; + + $extension->load($configs, $container); + + $this->assertFalse($container->hasDefinition('MdavidDev\SymfonyCorrelationIdBundle\EventListener\ConsoleListener')); + } +} diff --git a/tests/Unit/EventListener/ConsoleListenerTest.php b/tests/Unit/EventListener/ConsoleListenerTest.php new file mode 100644 index 0000000..c430eee --- /dev/null +++ b/tests/Unit/EventListener/ConsoleListenerTest.php @@ -0,0 +1,155 @@ +storage = new CorrelationIdStorage($requestStack); + $this->generator = $this->createMock(CorrelationIdGeneratorInterface::class); + $this->validator = new CorrelationIdValidator(true, 255, null); + + $this->listener = new ConsoleListener( + $this->storage, + $this->generator, + $this->validator, + 'CLI-', + true + ); + } + + public function testSubscribedEvents(): void + { + $events = ConsoleListener::getSubscribedEvents(); + + $this->assertArrayHasKey(ConsoleEvents::COMMAND, $events); + $this->assertArrayHasKey(ConsoleEvents::TERMINATE, $events); + $this->assertArrayHasKey(ConsoleEvents::ERROR, $events); + } + + public function testGeneratesIdWithPrefixWhenNoOptionProvided(): void + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $command = $this->createMock(Command::class); + + $input->method('hasParameterOption')->with('--correlation-id')->willReturn(false); + + $this->generator->expects($this->once())->method('generate')->willReturn('uuid-123'); + + $event = new ConsoleCommandEvent($command, $input, $output); + $this->listener->onConsoleCommand($event); + + $this->assertSame('CLI-uuid-123', $this->storage->get()); + } + + public function testUsesOptionWhenProvidedAndValid(): void + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $command = $this->createMock(Command::class); + + $input->method('hasParameterOption')->with('--correlation-id')->willReturn(true); + $input->method('getParameterOption')->with('--correlation-id')->willReturn('custom-id'); + + $this->generator->expects($this->never())->method('generate'); + + $event = new ConsoleCommandEvent($command, $input, $output); + $this->listener->onConsoleCommand($event); + + $this->assertSame('custom-id', $this->storage->get()); + } + + public function testGeneratesIdWhenOptionProvidedButInvalid(): void + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $command = $this->createMock(Command::class); + + $input->method('hasParameterOption')->with('--correlation-id')->willReturn(true); + $input->method('getParameterOption')->with('--correlation-id')->willReturn(' '); + + $this->generator->expects($this->once())->method('generate')->willReturn('uuid-456'); + + $event = new ConsoleCommandEvent($command, $input, $output); + $this->listener->onConsoleCommand($event); + + $this->assertSame('CLI-uuid-456', $this->storage->get()); + } + + public function testDoesNotAddOptionWhenAllowOptionIsFalse(): void + { + $listener = new ConsoleListener($this->storage, $this->generator, $this->validator, 'CLI-', false); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $command = $this->createMock(Command::class); + + $this->generator->expects($this->once())->method('generate')->willReturn('uuid-789'); + + $event = new ConsoleCommandEvent($command, $input, $output); + $listener->onConsoleCommand($event); + + $this->assertSame('CLI-uuid-789', $this->storage->get()); + } + + public function testOnConsoleTerminateClearsStorage(): void + { + $command = $this->createMock(Command::class); + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $this->storage->set('some-id'); + $event = new ConsoleTerminateEvent($command, $input, $output, 0); + $this->listener->onConsoleTerminate($event); + $this->assertNull($this->storage->get()); + } + + public function testOnConsoleErrorClearsStorage(): void + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $error = new Exception('error'); + + $this->storage->set('some-id'); + $event = new ConsoleErrorEvent($input, $output, $error); + $this->listener->onConsoleError($event); + $this->assertNull($this->storage->get()); + } + + public function testOnConsoleCommandDoesNothingIfCommandIsNull(): void + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + + $event = new ConsoleCommandEvent(null, $input, $output); + $this->listener->onConsoleCommand($event); + $this->assertNull($this->storage->get()); + } +} diff --git a/tests/Unit/EventListener/RequestListenerTest.php b/tests/Unit/EventListener/RequestListenerTest.php index b3955f2..4e960e8 100644 --- a/tests/Unit/EventListener/RequestListenerTest.php +++ b/tests/Unit/EventListener/RequestListenerTest.php @@ -104,7 +104,7 @@ public function testGeneratesIdWhenHeaderInvalid(): void ); $request = new Request(); - $request->headers->set('X-Correlation-ID', ''); // Invalide (vide) + $request->headers->set('X-Correlation-ID', ''); $this->requestStack->push($request); $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); @@ -126,7 +126,7 @@ public function testGeneratesIdWhenTrustHeaderIsFalse(): void $generator, $this->validator, 'X-Correlation-ID', - false // trust_header = false + false ); $request = new Request(); @@ -157,14 +157,12 @@ public function testDoesNothingWhenIdAlreadyExists(): void $request = new Request(); $this->requestStack->push($request); - // Définir un ID existant $this->storage->set('existing-id'); $event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); - // L'ID existant ne doit pas avoir changé $this->assertSame('existing-id', $this->storage->get()); } @@ -225,7 +223,7 @@ public function testUsesCustomHeaderName(): void $this->storage, $generator, $this->validator, - 'X-Request-ID', // Custom header name + 'X-Request-ID', true ); @@ -238,4 +236,4 @@ public function testUsesCustomHeaderName(): void $this->assertSame('custom-header-id', $this->storage->get()); } -} \ No newline at end of file +} diff --git a/tests/Unit/Service/CorrelationIdStorageTest.php b/tests/Unit/Service/CorrelationIdStorageTest.php index 101c75f..edbe058 100644 --- a/tests/Unit/Service/CorrelationIdStorageTest.php +++ b/tests/Unit/Service/CorrelationIdStorageTest.php @@ -44,7 +44,6 @@ public function testSetAndGetWithRequest(): void public function testSetAndGetWithoutRequest(): void { - // Pas de requête dans le stack $correlationId = 'fallback-id-456'; $this->storage->set($correlationId); @@ -86,13 +85,10 @@ public function testMultipleRequestsInStack(): void $this->requestStack->push($request2); $this->storage->set('id-2'); - // La requête courante (request2) doit avoir 'id-2' $this->assertSame('id-2', $this->storage->get()); - // Retirer request2 $this->requestStack->pop(); - // Maintenant on doit avoir 'id-1' (de request1) $this->assertSame('id-1', $this->storage->get()); } @@ -110,19 +106,15 @@ public function testSetOverwritesPreviousValue(): void public function testFallbackIsUsedWhenNoRequest(): void { - // Définir un ID sans requête $this->storage->set('fallback-id'); $this->assertSame('fallback-id', $this->storage->get()); - // Ajouter une requête $request = new Request(); $this->requestStack->push($request); - // Le fallback ne doit plus être utilisé (pas d'ID dans la requête) $this->assertNull($this->storage->get()); - // Définir un ID dans la requête $this->storage->set('request-id'); $this->assertSame('request-id', $this->storage->get()); } -} \ No newline at end of file +} diff --git a/tests/Unit/Service/Generator/UuidV4GeneratorTest.php b/tests/Unit/Service/Generator/UuidV4GeneratorTest.php index 4970d61..edc0305 100644 --- a/tests/Unit/Service/Generator/UuidV4GeneratorTest.php +++ b/tests/Unit/Service/Generator/UuidV4GeneratorTest.php @@ -28,8 +28,6 @@ public function testGenerateReturnsValidUuidV4Format(): void { $id = $this->generator->generate(); - // Format UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - // où y est 8, 9, a, ou b $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; $this->assertMatchesRegularExpression($pattern, $id); @@ -39,12 +37,10 @@ public function testGenerateReturnsUniqueIds(): void { $ids = []; - // Génère 100 IDs for ($i = 0; $i < 100; $i++) { $ids[] = $this->generator->generate(); } - // Vérifie qu'ils sont tous uniques $uniqueIds = array_unique($ids); $this->assertCount(100, $uniqueIds); } @@ -53,7 +49,6 @@ public function testGenerateReturnsCorrectLength(): void { $id = $this->generator->generate(); - // UUID v4 format: 36 caractères (32 hexa + 4 tirets) $this->assertSame(36, strlen($id)); } @@ -62,7 +57,6 @@ public function testGenerateIsIdempotent(): void $id1 = $this->generator->generate(); $id2 = $this->generator->generate(); - // Deux appels consécutifs doivent produire des IDs différents $this->assertNotSame($id1, $id2); } -} \ No newline at end of file +} diff --git a/tests/Unit/Validator/CorrelationIdValidatorTest.php b/tests/Unit/Validator/CorrelationIdValidatorTest.php index d74e052..cf9fd66 100644 --- a/tests/Unit/Validator/CorrelationIdValidatorTest.php +++ b/tests/Unit/Validator/CorrelationIdValidatorTest.php @@ -17,7 +17,6 @@ public function testValidationDisabled(): void pattern: null ); - // Quand la validation est désactivée, tout est valide $this->assertTrue($validator->isValid('anything')); $this->assertTrue($validator->isValid('very-long-' . str_repeat('x', 1000))); $this->assertTrue($validator->isValid('with-special-chars-!@#$%')); @@ -53,14 +52,14 @@ public function testMaxLengthValidation(): void pattern: null ); - $this->assertTrue($validator->isValid('123456789')); // 9 chars - $this->assertTrue($validator->isValid('1234567890')); // 10 chars (limite) - $this->assertFalse($validator->isValid('12345678901')); // 11 chars (trop long) + $this->assertTrue($validator->isValid('123456789')); + $this->assertTrue($validator->isValid('1234567890')); + $this->assertFalse($validator->isValid('12345678901')); } public function testPatternValidation(): void { - // Pattern: seulement des lettres minuscules et tirets + // Pattern: lowercase letters and dashes only $validator = new CorrelationIdValidator( enabled: true, maxLength: 255, @@ -69,14 +68,14 @@ public function testPatternValidation(): void $this->assertTrue($validator->isValid('valid-id')); $this->assertTrue($validator->isValid('another-valid-one')); - $this->assertFalse($validator->isValid('Invalid-ID')); // Majuscules - $this->assertFalse($validator->isValid('invalid_id')); // Underscore - $this->assertFalse($validator->isValid('invalid123')); // Chiffres + $this->assertFalse($validator->isValid('Invalid-ID')); + $this->assertFalse($validator->isValid('invalid_id')); + $this->assertFalse($validator->isValid('invalid123')); } public function testUuidV4Pattern(): void { - // Pattern UUID v4 + // UUID v4 pattern $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; $validator = new CorrelationIdValidator( @@ -85,18 +84,15 @@ public function testUuidV4Pattern(): void pattern: $pattern ); - // UUID v4 valides $this->assertTrue($validator->isValid('550e8400-e29b-41d4-a716-446655440000')); $this->assertTrue($validator->isValid('f47ac10b-58cc-4372-a567-0e02b2c3d479')); - // UUID v4 invalides $this->assertFalse($validator->isValid('not-a-uuid')); $this->assertFalse($validator->isValid('550e8400-e29b-11d4-a716-446655440000')); // UUID v1 } public function testCombinedValidation(): void { - // Max length + pattern $validator = new CorrelationIdValidator( enabled: true, maxLength: 20, @@ -104,8 +100,8 @@ public function testCombinedValidation(): void ); $this->assertTrue($validator->isValid('VALID-ID-123')); - $this->assertFalse($validator->isValid('TOO-LONG-ID-123456789')); // Trop long - $this->assertFalse($validator->isValid('invalid-lowercase')); // Pattern non respecté + $this->assertFalse($validator->isValid('TOO-LONG-ID-123456789')); + $this->assertFalse($validator->isValid('invalid-lowercase')); } public function testSanitizeValidId(): void @@ -116,7 +112,6 @@ public function testSanitizeValidId(): void pattern: null ); - // Trim les espaces $this->assertSame('valid-id', $validator->sanitize(' valid-id ')); $this->assertSame('another-id', $validator->sanitize('another-id')); } @@ -129,13 +124,10 @@ public function testSanitizeInvalidId(): void pattern: null ); - // ID trop long $this->assertNull($validator->sanitize('this-is-too-long')); - // Null $this->assertNull($validator->sanitize(null)); - // Chaîne vide $this->assertNull($validator->sanitize('')); } @@ -148,7 +140,7 @@ public function testSanitizeWithPattern(): void ); $this->assertSame('valid-id-123', $validator->sanitize(' valid-id-123 ')); - $this->assertNull($validator->sanitize('Invalid-ID')); // Majuscules + $this->assertNull($validator->sanitize('Invalid-ID')); } public function testSanitizeWhenValidationDisabled(): void @@ -159,7 +151,6 @@ public function testSanitizeWhenValidationDisabled(): void pattern: '/^[a-z]+$/' ); - // Même si trop long et ne match pas le pattern, c'est valide $this->assertSame('ANYTHING-GOES-123', $validator->sanitize(' ANYTHING-GOES-123 ')); } @@ -171,8 +162,7 @@ public function testMultibyteStringLength(): void pattern: null ); - // Test avec des caractères multi-octets - $this->assertTrue($validator->isValid('émoji🎉')); // 7 caractères - $this->assertFalse($validator->isValid('émoji🎉test123')); // 14 caractères + $this->assertTrue($validator->isValid('émoji🎉')); + $this->assertFalse($validator->isValid('émoji🎉test123')); } -} \ No newline at end of file +}