From 7872fefce5070c3db9671b725f7fe045ed43fc1f Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Wed, 30 Sep 2026 17:52:08 +0200 Subject: [PATCH 1/2] chore(tests) #20 Add missing tests: GetTask and SetTask edge cases, custom cache tasks, Adapter, bundle and DI extension Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + tests/Adapter/AdapterTest.php | 93 ++++++++++++++++++ tests/CleverAgeCacheProcessBundleTest.php | 44 +++++++++ .../CleverAgeCacheProcessExtensionTest.php | 68 +++++++++++++ tests/Task/AbstractCacheTaskTest.php | 96 +++++++++++++++++++ tests/Task/GetTaskTest.php | 77 ++++++++++++++- tests/Task/SetTaskTest.php | 79 ++++++++++++++- 7 files changed, 454 insertions(+), 4 deletions(-) create mode 100644 tests/Adapter/AdapterTest.php create mode 100644 tests/CleverAgeCacheProcessBundleTest.php create mode 100644 tests/DependencyInjection/CleverAgeCacheProcessExtensionTest.php create mode 100644 tests/Task/AbstractCacheTaskTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 843aef8..33b1faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ### Changes +* [#20](https://github.com/cleverage/cache-process-bundle/issues/20) Add missing tests: GetTask and SetTask (options validation at initialization, context, missing adapter, invalid key, stored `null`, overwriting), custom tasks extending AbstractCacheTask, Adapter, bundle and DI extension. * [#25](https://github.com/cleverage/cache-process-bundle/issues/25) Give the ids of both services in the error on duplicate adapter codes: the adapters are registered by a compiler pass of the bundle, `AdapterRegistry::addAdapter()` gets an optional `$serviceId` argument. Update documentation, add tests. ### Fixes diff --git a/tests/Adapter/AdapterTest.php b/tests/Adapter/AdapterTest.php new file mode 100644 index 0000000..1c2589a --- /dev/null +++ b/tests/Adapter/AdapterTest.php @@ -0,0 +1,93 @@ +pool = new ArrayAdapter(); + $this->adapter = new Adapter($this->pool, 'memory'); + } + + public function testGetCode(): void + { + self::assertSame('memory', $this->adapter->getCode()); + } + + public function testSaveAndGetItem(): void + { + self::assertFalse($this->adapter->getItem('key1')->isHit()); + + self::assertTrue($this->adapter->save($this->adapter->getItem('key1')->set('value1'))); + + self::assertTrue($this->adapter->hasItem('key1')); + self::assertSame('value1', $this->adapter->getItem('key1')->get()); + // Stored in the decorated pool + self::assertSame('value1', $this->pool->getItem('key1')->get()); + } + + public function testGetItems(): void + { + $this->pool->save($this->pool->getItem('key1')->set('value1')); + + $values = []; + foreach ($this->adapter->getItems(['key1', 'key2']) as $key => $item) { + $values[$key] = $item->get(); + } + + self::assertSame(['key1' => 'value1', 'key2' => null], $values); + } + + public function testSaveDeferredAndCommit(): void + { + self::assertTrue($this->adapter->saveDeferred($this->adapter->getItem('key1')->set('value1'))); + self::assertTrue($this->adapter->commit()); + + self::assertSame('value1', $this->pool->getItem('key1')->get()); + } + + public function testDeleteItems(): void + { + foreach (['key1', 'key2', 'key3'] as $key) { + $this->pool->save($this->pool->getItem($key)->set($key)); + } + + self::assertTrue($this->adapter->deleteItem('key1')); + self::assertTrue($this->adapter->deleteItems(['key2'])); + + self::assertFalse($this->pool->hasItem('key1')); + self::assertFalse($this->pool->hasItem('key2')); + self::assertTrue($this->pool->hasItem('key3')); + } + + public function testClear(): void + { + $this->pool->save($this->pool->getItem('key1')->set('value1')); + + self::assertTrue($this->adapter->clear()); + + self::assertFalse($this->pool->hasItem('key1')); + } +} diff --git a/tests/CleverAgeCacheProcessBundleTest.php b/tests/CleverAgeCacheProcessBundleTest.php new file mode 100644 index 0000000..14fc169 --- /dev/null +++ b/tests/CleverAgeCacheProcessBundleTest.php @@ -0,0 +1,44 @@ +getPath(); + + self::assertSame(\dirname(__DIR__), $path); + self::assertDirectoryExists($path.'/config/services'); + } + + public function testAdaptersPassIsRegistered(): void + { + $container = new ContainerBuilder(); + (new CleverAgeCacheProcessBundle())->build($container); + + $passes = array_filter( + $container->getCompilerPassConfig()->getBeforeOptimizationPasses(), + static fn (object $pass): bool => $pass instanceof RegisterAdaptersPass + ); + self::assertCount(1, $passes); + } +} diff --git a/tests/DependencyInjection/CleverAgeCacheProcessExtensionTest.php b/tests/DependencyInjection/CleverAgeCacheProcessExtensionTest.php new file mode 100644 index 0000000..fc5b101 --- /dev/null +++ b/tests/DependencyInjection/CleverAgeCacheProcessExtensionTest.php @@ -0,0 +1,68 @@ +load([], $container); + + $definition = $container->getDefinition('cleverage_cache_process.registry.adapter'); + self::assertSame(AdapterRegistry::class, $definition->getClass()); + self::assertTrue($definition->isShared()); + } + + /** + * @return iterable + */ + public static function provideTasks(): iterable + { + yield 'get' => ['cleverage_cache_process.task.get', GetTask::class]; + yield 'set' => ['cleverage_cache_process.task.set', SetTask::class]; + } + + /** + * @param class-string $class + */ + #[DataProvider('provideTasks')] + public function testTaskIsRegistered(string $id, string $class): void + { + $container = new ContainerBuilder(); + (new CleverAgeCacheProcessExtension())->load([], $container); + + $definition = $container->getDefinition($id); + self::assertSame($class, $definition->getClass()); + // Tasks are stateful: each process execution must get its own instance + self::assertFalse($definition->isShared()); + self::assertEquals([new Reference('cleverage_cache_process.registry.adapter')], $definition->getArguments()); + + // Referenced as '@' in process configurations + $alias = $container->getAlias($class); + self::assertSame($id, (string) $alias); + self::assertTrue($alias->isPublic()); + } +} diff --git a/tests/Task/AbstractCacheTaskTest.php b/tests/Task/AbstractCacheTaskTest.php new file mode 100644 index 0000000..f8307d8 --- /dev/null +++ b/tests/Task/AbstractCacheTaskTest.php @@ -0,0 +1,96 @@ +createTask(['adapter' => 'memory', 'key' => 'key1']); + + // Resolved options: the order of the keys is not relevant + self::assertEquals( + ['adapter' => 'memory', 'key' => 'key2', 'ttl' => 60], + $this->execute($task, $state, ['key' => 'key2', 'ttl' => 60, 'sku' => 'ignored']) + ); + self::assertEquals(['adapter' => 'memory', 'key' => 'key1', 'ttl' => null], $this->execute($task, $state, null)); + } + + public function testCustomOptionFromInputIsValidated(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'key1']); + + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "ttl" with value "60" is expected to be of type "int" or "null", but is of type "string".'); + $this->execute($task, $state, ['ttl' => '60']); + } + + /** + * @param array $options + * + * @return array{AbstractCacheTask, ProcessState} + */ + private function createTask(array $options): array + { + $processConfiguration = new ProcessConfiguration('test', []); + $state = new ProcessState($processConfiguration, new ProcessHistory($processConfiguration)); + $state->setContextualOptionResolver(new ContextualOptionResolver()); + $state->setContext([]); + $state->setTaskConfiguration(new TaskConfiguration('custom', AbstractCacheTask::class, $options)); + + $task = new class(new AdapterRegistry()) extends AbstractCacheTask { + public function execute(ProcessState $state): void + { + $state->setOutput($this->getMergedOptions($state)); + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + + $resolver->setDefault('ttl', null); + $resolver->setAllowedTypes('ttl', ['int', 'null']); + } + }; + $task->initialize($state); + + return [$task, $state]; + } + + private function execute(AbstractCacheTask $task, ProcessState $state, mixed $input): mixed + { + $state->reset(false); + $state->setInput($input); + $task->execute($state); + + return $state->getOutput(); + } +} diff --git a/tests/Task/GetTaskTest.php b/tests/Task/GetTaskTest.php index 5942993..2fa7e74 100644 --- a/tests/Task/GetTaskTest.php +++ b/tests/Task/GetTaskTest.php @@ -14,6 +14,7 @@ namespace CleverAge\CacheProcessBundle\Tests\Task; use CleverAge\CacheProcessBundle\Adapter\Adapter; +use CleverAge\CacheProcessBundle\Exception\MissingAdapterException; use CleverAge\CacheProcessBundle\Registry\AdapterRegistry; use CleverAge\CacheProcessBundle\Task\GetTask; use CleverAge\ProcessBundle\Configuration\ProcessConfiguration; @@ -24,12 +25,16 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; #[CoversClass(GetTask::class)] #[UsesClass(Adapter::class)] #[UsesClass(AdapterRegistry::class)] +#[UsesClass(MissingAdapterException::class)] class GetTaskTest extends TestCase { private Adapter $adapter; @@ -81,17 +86,85 @@ public function testInputValuesAreValidated(): void $this->execute($task, $state, ['key' => 1]); } + public function testGetStoredNullValue(): void + { + $this->adapter->save($this->adapter->getItem('null')->set(null)); + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'null']); + + self::assertNull($this->execute($task, $state, null)); + } + + public function testKeyFromContext(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '{{ sku }}'], ['sku' => 'key2']); + + self::assertSame('value2', $this->execute($task, $state, null)); + } + + public function testEmptyArrayInputUsesOptions(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'key1']); + + self::assertSame('value1', $this->execute($task, $state, [])); + } + + public function testInputOverridesAdapter(): void + { + [$task, $state] = $this->createTask(['adapter' => 'other', 'key' => 'key1']); + + self::assertSame('value1', $this->execute($task, $state, ['adapter' => 'memory'])); + } + + public function testMissingAdapter(): void + { + [$task, $state] = $this->createTask(['adapter' => 'missing', 'key' => 'key1']); + + $this->expectException(MissingAdapterException::class); + $this->expectExceptionMessage('Adapter missing is missing'); + $this->execute($task, $state, null); + } + + public function testInvalidKey(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '']); + + // Placeholder key not overridden by the input + $this->expectException(InvalidArgumentException::class); + $this->execute($task, $state, null); + } + + public function testRequiredOptionsAtInitialization(): void + { + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "key" is missing.'); + $this->createTask(['adapter' => 'memory']); + } + + public function testUndefinedOptionAtInitialization(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->createTask(['adapter' => 'memory', 'key' => 'key1', 'value' => 'value1']); + } + + public function testInvalidOptionTypeAtInitialization(): void + { + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "adapter" with value 1 is expected to be of type "string", but is of type "int".'); + $this->createTask(['adapter' => 1, 'key' => 'key1']); + } + /** * @param array $options + * @param array $context * * @return array{GetTask, ProcessState} */ - private function createTask(array $options): array + private function createTask(array $options, array $context = []): array { $processConfiguration = new ProcessConfiguration('test', []); $state = new ProcessState($processConfiguration, new ProcessHistory($processConfiguration)); $state->setContextualOptionResolver(new ContextualOptionResolver()); - $state->setContext([]); + $state->setContext($context); $state->setTaskConfiguration(new TaskConfiguration('get', GetTask::class, $options)); $registry = new AdapterRegistry(); diff --git a/tests/Task/SetTaskTest.php b/tests/Task/SetTaskTest.php index 51f9654..d869128 100644 --- a/tests/Task/SetTaskTest.php +++ b/tests/Task/SetTaskTest.php @@ -14,6 +14,7 @@ namespace CleverAge\CacheProcessBundle\Tests\Task; use CleverAge\CacheProcessBundle\Adapter\Adapter; +use CleverAge\CacheProcessBundle\Exception\MissingAdapterException; use CleverAge\CacheProcessBundle\Registry\AdapterRegistry; use CleverAge\CacheProcessBundle\Task\SetTask; use CleverAge\ProcessBundle\Configuration\ProcessConfiguration; @@ -24,12 +25,16 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; #[CoversClass(SetTask::class)] #[UsesClass(Adapter::class)] #[UsesClass(AdapterRegistry::class)] +#[UsesClass(MissingAdapterException::class)] class SetTaskTest extends TestCase { private Adapter $adapter; @@ -77,17 +82,87 @@ public function testInputValuesAreValidated(): void $this->execute($task, $state, ['adapter' => 1, 'key' => 'key1', 'value' => 'value1']); } + public function testNoOutput(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'key1', 'value' => 'value1']); + + $this->execute($task, $state, ['sku' => 'ABC-001']); + + self::assertNull($state->getOutput()); + } + + public function testExistingItemIsOverwritten(): void + { + $this->adapter->save($this->adapter->getItem('key1')->set('old')); + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'key1', 'value' => 'new']); + + $this->execute($task, $state, null); + + self::assertSame('new', $this->adapter->getItem('key1')->get()); + } + + public function testNullValueIsStored(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => 'key1', 'value' => null]); + + $this->execute($task, $state, null); + + self::assertTrue($this->adapter->hasItem('key1')); + self::assertNull($this->adapter->getItem('key1')->get()); + } + + public function testKeyFromContext(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '{{ sku }}', 'value' => 'value1'], ['sku' => 'ABC-001']); + + $this->execute($task, $state, null); + + self::assertSame('value1', $this->adapter->getItem('ABC-001')->get()); + } + + public function testMissingAdapter(): void + { + [$task, $state] = $this->createTask(['adapter' => 'missing', 'key' => 'key1', 'value' => 'value1']); + + $this->expectException(MissingAdapterException::class); + $this->expectExceptionMessage('Adapter missing is missing'); + $this->execute($task, $state, null); + } + + public function testInvalidKey(): void + { + [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '', 'value' => null]); + + // Placeholder key not overridden by the input + $this->expectException(InvalidArgumentException::class); + $this->execute($task, $state, ['value' => 'value1']); + } + + public function testRequiredOptionsAtInitialization(): void + { + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "value" is missing.'); + $this->createTask(['adapter' => 'memory', 'key' => 'key1']); + } + + public function testUndefinedOptionAtInitialization(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->createTask(['adapter' => 'memory', 'key' => 'key1', 'value' => 'value1', 'ttl' => 60]); + } + /** * @param array $options + * @param array $context * * @return array{SetTask, ProcessState} */ - private function createTask(array $options): array + private function createTask(array $options, array $context = []): array { $processConfiguration = new ProcessConfiguration('test', []); $state = new ProcessState($processConfiguration, new ProcessHistory($processConfiguration)); $state->setContextualOptionResolver(new ContextualOptionResolver()); - $state->setContext([]); + $state->setContext($context); $state->setTaskConfiguration(new TaskConfiguration('set', SetTask::class, $options)); $registry = new AdapterRegistry(); From e4f3f89c9a5758d4e0a3b777fa351d9babf1e807 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Wed, 30 Sep 2026 17:56:27 +0200 Subject: [PATCH 2/2] chore(tests) #20 Remove the invalid key tests: the key validation depends on the adapter (assertions only for ArrayAdapter) Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- tests/Task/GetTaskTest.php | 10 ---------- tests/Task/SetTaskTest.php | 10 ---------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b1faf..9af4bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Latest ------ ### Changes -* [#20](https://github.com/cleverage/cache-process-bundle/issues/20) Add missing tests: GetTask and SetTask (options validation at initialization, context, missing adapter, invalid key, stored `null`, overwriting), custom tasks extending AbstractCacheTask, Adapter, bundle and DI extension. +* [#20](https://github.com/cleverage/cache-process-bundle/issues/20) Add missing tests: GetTask and SetTask (options validation at initialization, context, missing adapter, stored `null`, overwriting), custom tasks extending AbstractCacheTask, Adapter, bundle and DI extension. * [#25](https://github.com/cleverage/cache-process-bundle/issues/25) Give the ids of both services in the error on duplicate adapter codes: the adapters are registered by a compiler pass of the bundle, `AdapterRegistry::addAdapter()` gets an optional `$serviceId` argument. Update documentation, add tests. ### Fixes diff --git a/tests/Task/GetTaskTest.php b/tests/Task/GetTaskTest.php index 2fa7e74..fcc498c 100644 --- a/tests/Task/GetTaskTest.php +++ b/tests/Task/GetTaskTest.php @@ -25,7 +25,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; @@ -124,15 +123,6 @@ public function testMissingAdapter(): void $this->execute($task, $state, null); } - public function testInvalidKey(): void - { - [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '']); - - // Placeholder key not overridden by the input - $this->expectException(InvalidArgumentException::class); - $this->execute($task, $state, null); - } - public function testRequiredOptionsAtInitialization(): void { $this->expectException(MissingOptionsException::class); diff --git a/tests/Task/SetTaskTest.php b/tests/Task/SetTaskTest.php index d869128..c2f87f7 100644 --- a/tests/Task/SetTaskTest.php +++ b/tests/Task/SetTaskTest.php @@ -25,7 +25,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; @@ -129,15 +128,6 @@ public function testMissingAdapter(): void $this->execute($task, $state, null); } - public function testInvalidKey(): void - { - [$task, $state] = $this->createTask(['adapter' => 'memory', 'key' => '', 'value' => null]); - - // Placeholder key not overridden by the input - $this->expectException(InvalidArgumentException::class); - $this->execute($task, $state, ['value' => 'value1']); - } - public function testRequiredOptionsAtInitialization(): void { $this->expectException(MissingOptionsException::class);