From ed771ee5e5be46670d3fcb2db3bee88c4890ca2c Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Wed, 30 Sep 2026 15:56:58 +0200 Subject: [PATCH] chore(tests) #13 Add missing tests: ZipTask and UnzipTask edge cases, bundle and DI extension Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + tests/CleverAgeArchiveProcessBundleTest.php | 30 ++++ .../CleverAgeArchiveProcessExtensionTest.php | 55 +++++++ tests/Task/UnzipTaskTest.php | 94 ++++++++++++ tests/Task/ZipTaskTest.php | 138 ++++++++++++++++++ 5 files changed, 320 insertions(+) create mode 100644 tests/CleverAgeArchiveProcessBundleTest.php create mode 100644 tests/DependencyInjection/CleverAgeArchiveProcessExtensionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9231346..cc3ec0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Latest ------ +### Changes +* [#13](https://github.com/cleverage/archive-process-bundle/issues/13) Add missing tests: ZipTask and UnzipTask (options precedence and validation, missing or unreadable files, overwriting, open failures), bundle and DI extension. + ### Fixes * [#15](https://github.com/cleverage/archive-process-bundle/issues/15) Fix ZipTask and UnzipTask: resolve the options on every execution, so that each input is used (the options of the first input were reused for the following ones). Update documentation, add tests. * [#16](https://github.com/cleverage/archive-process-bundle/issues/16) Fix ZipTask: only remove the leading `files_base_path` from the file paths (every occurrence was removed), and read the files at the given path when `files_base_path` is empty (relative paths were resolved from the filesystem root). Update documentation, add tests. diff --git a/tests/CleverAgeArchiveProcessBundleTest.php b/tests/CleverAgeArchiveProcessBundleTest.php new file mode 100644 index 0000000..905af54 --- /dev/null +++ b/tests/CleverAgeArchiveProcessBundleTest.php @@ -0,0 +1,30 @@ +getPath(); + + self::assertSame(\dirname(__DIR__), $path); + self::assertDirectoryExists($path.'/config/services'); + } +} diff --git a/tests/DependencyInjection/CleverAgeArchiveProcessExtensionTest.php b/tests/DependencyInjection/CleverAgeArchiveProcessExtensionTest.php new file mode 100644 index 0000000..8b26dba --- /dev/null +++ b/tests/DependencyInjection/CleverAgeArchiveProcessExtensionTest.php @@ -0,0 +1,55 @@ + + */ + public static function provideTasks(): iterable + { + yield 'zip' => ['cleverage_archive_process.task.zip', ZipTask::class]; + yield 'unzip' => ['cleverage_archive_process.task.unzip', UnzipTask::class]; + } + + /** + * @param class-string $class + */ + #[DataProvider('provideTasks')] + public function testTaskIsRegistered(string $id, string $class): void + { + $container = new ContainerBuilder(); + (new CleverAgeArchiveProcessExtension())->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()); + + // Referenced as '@' in process configurations + $alias = $container->getAlias($class); + self::assertSame($id, (string) $alias); + self::assertTrue($alias->isPublic()); + } +} diff --git a/tests/Task/UnzipTaskTest.php b/tests/Task/UnzipTaskTest.php index 63848be..0048948 100644 --- a/tests/Task/UnzipTaskTest.php +++ b/tests/Task/UnzipTaskTest.php @@ -22,6 +22,9 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; #[CoversClass(UnzipTask::class)] class UnzipTaskTest extends TestCase @@ -111,6 +114,97 @@ public function testNonArrayInput(): void } } + public function testInputOverridesConfiguredOptions(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive1.zip', + 'destination' => $this->dir.'/configured', + ]); + + self::assertSame($this->dir.'/input', $this->execute($task, $state, ['destination' => $this->dir.'/input'])); + self::assertFileExists($this->dir.'/input/file1.txt'); + self::assertDirectoryDoesNotExist($this->dir.'/configured'); + } + + public function testExistingFilesAreOverwrittenOtherFilesAreKept(): void + { + mkdir($this->dir.'/destination'); + file_put_contents($this->dir.'/destination/file1.txt', 'old content'); + file_put_contents($this->dir.'/destination/other.txt', 'other content'); + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive1.zip', + 'destination' => $this->dir.'/destination', + ]); + + $this->execute($task, $state, null); + + self::assertStringEqualsFile($this->dir.'/destination/file1.txt', 'content of file1.txt'); + self::assertStringEqualsFile($this->dir.'/destination/other.txt', 'other content'); + } + + public function testMissingFile(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/missing.zip', + 'destination' => $this->dir.'/destination', + ]); + + try { + $this->execute($task, $state, null); + self::fail('An \UnexpectedValueException should have been thrown'); + } catch (\UnexpectedValueException $e) { + self::assertSame("File does not exists: '{$this->dir}/missing.zip'", $e->getMessage()); + } + self::assertNull($state->getOutput()); + } + + public function testUnreadableFile(): void + { + chmod($this->dir.'/archive1.zip', 0o000); + if (is_readable($this->dir.'/archive1.zip')) { + self::markTestSkipped('Files are always readable by root'); + } + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive1.zip', + 'destination' => $this->dir.'/destination', + ]); + + try { + $this->execute($task, $state, null); + self::fail('An \UnexpectedValueException should have been thrown'); + } catch (\UnexpectedValueException $e) { + self::assertSame("File is not readable: '{$this->dir}/archive1.zip'", $e->getMessage()); + } + } + + public function testOptionsAreValidatedOnExecution(): void + { + // No exception at initialization: the options may come from the input + [$task, $state] = $this->createTask([]); + + $this->expectException(MissingOptionsException::class); + $this->execute($task, $state, ['filename' => $this->dir.'/archive1.zip']); + } + + public function testUndefinedOptionInInput(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive1.zip', + 'destination' => $this->dir.'/destination', + ]); + + $this->expectException(UndefinedOptionsException::class); + $this->execute($task, $state, ['unknown' => 'value']); + } + + public function testInvalidOptionType(): void + { + [$task, $state] = $this->createTask(['filename' => $this->dir.'/archive1.zip']); + + $this->expectException(InvalidOptionsException::class); + $this->execute($task, $state, ['destination' => ['not', 'a', 'string']]); + } + /** * @param array $options * diff --git a/tests/Task/ZipTaskTest.php b/tests/Task/ZipTaskTest.php index cab5fc0..c2c4398 100644 --- a/tests/Task/ZipTaskTest.php +++ b/tests/Task/ZipTaskTest.php @@ -22,6 +22,9 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; #[CoversClass(ZipTask::class)] class ZipTaskTest extends TestCase @@ -128,6 +131,141 @@ public function testNonArrayInput(): void } } + public function testSingleFileAsString(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'file1.txt', + 'files_base_path' => $this->dir, + ]); + + $this->execute($task, $state, null); + + self::assertSame(['file1.txt'], $this->getEntries($this->dir.'/archive.zip')); + } + + public function testInputOverridesConfiguredOptions(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/configured.zip', + 'files' => 'file1.txt', + 'files_base_path' => $this->dir, + ]); + + self::assertSame($this->dir.'/input.zip', $this->execute($task, $state, ['filename' => $this->dir.'/input.zip'])); + self::assertSame(['file1.txt'], $this->getEntries($this->dir.'/input.zip')); + self::assertFileDoesNotExist($this->dir.'/configured.zip'); + } + + public function testExistingArchiveIsOverwritten(): void + { + $zip = new \ZipArchive(); + self::assertTrue($zip->open($this->dir.'/archive.zip', \ZipArchive::CREATE)); + $zip->addFromString('old.txt', 'old'); + $zip->close(); + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'file1.txt', + 'files_base_path' => $this->dir, + ]); + + $this->execute($task, $state, null); + + self::assertSame(['file1.txt'], $this->getEntries($this->dir.'/archive.zip')); + } + + public function testMissingFile(): void + { + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'missing.txt', + 'files_base_path' => $this->dir, + ]); + + try { + $this->execute($task, $state, null); + self::fail('An \UnexpectedValueException should have been thrown'); + } catch (\UnexpectedValueException $e) { + self::assertSame("File does not exists: '{$this->dir}/missing.txt'", $e->getMessage()); + } + self::assertNull($state->getOutput()); + } + + public function testUnreadableFile(): void + { + chmod($this->dir.'/file1.txt', 0o000); + if (is_readable($this->dir.'/file1.txt')) { + self::markTestSkipped('Files are always readable by root'); + } + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'file1.txt', + 'files_base_path' => $this->dir, + ]); + + try { + $this->execute($task, $state, null); + self::fail('An \UnexpectedValueException should have been thrown'); + } catch (\UnexpectedValueException $e) { + self::assertSame("File is not readable: '{$this->dir}/file1.txt'", $e->getMessage()); + } + } + + public function testDirectoryCannotBeAdded(): void + { + mkdir($this->dir.'/sub'); + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'sub', + 'files_base_path' => $this->dir, + ]); + + $this->expectException(\RuntimeException::class); + $this->execute($task, $state, null); + } + + public function testOpenFailure(): void + { + mkdir($this->dir.'/archive.zip'); + [$task, $state] = $this->createTask([ + 'filename' => $this->dir.'/archive.zip', + 'files' => 'file1.txt', + 'files_base_path' => $this->dir, + ]); + + try { + $this->execute($task, $state, null); + self::fail('A \RuntimeException should have been thrown'); + } catch (\RuntimeException $e) { + self::assertStringStartsWith("Fail to open file {$this->dir}/archive.zip with code ", $e->getMessage()); + } + } + + public function testOptionsAreValidatedOnExecution(): void + { + // No exception at initialization: the options may come from the input + [$task, $state] = $this->createTask([]); + + $this->expectException(MissingOptionsException::class); + $this->execute($task, $state, ['files' => 'file1.txt']); + } + + public function testUndefinedOptionInInput(): void + { + [$task, $state] = $this->createTask(['filename' => $this->dir.'/archive.zip', 'files' => 'file1.txt']); + + $this->expectException(UndefinedOptionsException::class); + $this->execute($task, $state, ['unknown' => 'value']); + } + + public function testInvalidOptionType(): void + { + [$task, $state] = $this->createTask(['filename' => $this->dir.'/archive.zip']); + + $this->expectException(InvalidOptionsException::class); + $this->execute($task, $state, ['files' => 12]); + } + /** * @param array $options *