From dcfec3de04e384e3c8d7ea531158dc7829d019b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Tue, 3 Nov 2026 17:04:29 +0100 Subject: [PATCH] test(DateHashPathGenerator): add unit tests for path generation Added comprehensive unit tests for DateHashPathGenerator to validate path generation with and without directories, ensuring unique paths for identical files and proper slugging of filenames. --- .../DateHashPathGeneratorTest.php | 199 ++++++++++++++++++ .../PathGenerators/UuidPathGeneratorTest.php | 140 ++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 tests/Unit/Lib/PathGenerators/DateHashPathGeneratorTest.php create mode 100644 tests/Unit/Lib/PathGenerators/UuidPathGeneratorTest.php diff --git a/tests/Unit/Lib/PathGenerators/DateHashPathGeneratorTest.php b/tests/Unit/Lib/PathGenerators/DateHashPathGeneratorTest.php new file mode 100644 index 0000000..1d240d5 --- /dev/null +++ b/tests/Unit/Lib/PathGenerators/DateHashPathGeneratorTest.php @@ -0,0 +1,199 @@ +generate($disk, $sanitizedFile); + + // THEN + expect($result)->toBeInstanceOf(StorageFilePath::class); + + $pattern = sprintf( + '#^%s/%s/%s/%s/document-[a-f0-9]{8}\.pdf$#', + $expectedYear, + $expectedMonth, + $expectedDay, + $expectedHash + ); + expect($result->value)->toMatch($pattern); + }); + + it('generates a path for a file without extension', function (): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'LICENSE', + sanitizedName: 'LICENSE', + contents: FileStream::fromString('MIT License') + ); + + $expectedHash = substr(hash('sha256', 'LICENSE'), 0, 2); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($result)->toBeInstanceOf(StorageFilePath::class); + + $pattern = sprintf( + '#^%s/%s/%s/%s/license-[a-f0-9]{8}$#', + gmdate('Y'), + gmdate('m'), + gmdate('d'), + $expectedHash + ); + expect($result->value)->toMatch($pattern); + }); + + it('generates unique paths for identical files generated consecutively', function (): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'photo.jpg', + sanitizedName: 'photo.jpg', + contents: FileStream::fromString('image bytes') + ); + + // WHEN + $path1 = $generator->generate($disk, $sanitizedFile); + $path2 = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($path1->value)->not->toBe($path2->value); + }); +}); + +describe('DateHashPathGenerator generation with directory', function (): void { + it('prepends the given directory to the path', function (): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'avatar.png', + sanitizedName: 'avatar.png', + contents: FileStream::fromString('image data') + ); + $directory = 'avatars/users'; + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + $expectedHash = substr(hash('sha256', 'avatar.png'), 0, 2); + $pattern = sprintf( + '#^avatars/users/%s/%s/%s/%s/avatar-[a-f0-9]{8}\.png$#', + gmdate('Y'), + gmdate('m'), + gmdate('d'), + $expectedHash + ); + expect($result->value)->toMatch($pattern); + }); + + it('trims leading and trailing slashes from the directory', function (string $directory): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'file.txt', + sanitizedName: 'file.txt', + contents: FileStream::fromString('txt') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + expect($result->value)->toStartWith('uploads/archive/'); + })->with([ + 'leading slash' => '/uploads/archive', + 'trailing slash' => 'uploads/archive/', + 'both slashes' => '/uploads/archive/', + 'multiple outer slashes' => '///uploads/archive///', + ]); + + it('ignores empty or slash-only directory', function (string $directory): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'report.pdf', + sanitizedName: 'report.pdf', + contents: FileStream::fromString('pdf') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + expect($result->value)->toStartWith(gmdate('Y') . '/'); + })->with([ + 'empty string' => '', + 'single slash' => '/', + 'multi slashes' => '///', + ]); +}); + +describe('DateHashPathGenerator filename slugging', function (): void { + it('slugs filename containing underscores or mixed case', function (): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'My_Monthly_Report_2026.csv', + sanitizedName: 'My_Monthly_Report_2026.csv', + contents: FileStream::fromString('a,b,c') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($result->value)->toMatch('#/my-monthly-report-2026-[a-f0-9]{8}\.csv$#'); + }); + + it('falls back to "file" when filename produces an empty slug', function (): void { + // GIVEN + $generator = new DateHashPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: '.env.backup', + sanitizedName: '.backup', + contents: FileStream::fromString('KEY=VAL') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($result->value)->toMatch('#/file-[a-f0-9]{8}\.backup$#'); + }); +}); diff --git a/tests/Unit/Lib/PathGenerators/UuidPathGeneratorTest.php b/tests/Unit/Lib/PathGenerators/UuidPathGeneratorTest.php new file mode 100644 index 0000000..ac57c0e --- /dev/null +++ b/tests/Unit/Lib/PathGenerators/UuidPathGeneratorTest.php @@ -0,0 +1,140 @@ +generate($disk, $sanitizedFile); + + // THEN + expect($result)->toBeInstanceOf(StorageFilePath::class); + + $filename = $result->value; + expect($filename)->toMatch('#^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.png$#'); + + $uuidOnly = pathinfo($filename, PATHINFO_FILENAME); + expect(Str::isUuid($uuidOnly))->toBeTrue(); + }); + + it('generates a storage file path without extension when file has no extension', function (): void { + // GIVEN + $generator = new UuidPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'DOCKERFILE', + sanitizedName: 'DOCKERFILE', + contents: FileStream::fromString('FROM php:8.5') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($result)->toBeInstanceOf(StorageFilePath::class) + ->and(Str::isUuid($result->value))->toBeTrue(); + }); + + it('generates unique paths on successive calls for the same file', function (): void { + // GIVEN + $generator = new UuidPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'data.json', + sanitizedName: 'data.json', + contents: FileStream::fromString('{}') + ); + + // WHEN + $path1 = $generator->generate($disk, $sanitizedFile); + $path2 = $generator->generate($disk, $sanitizedFile); + + // THEN + expect($path1->value)->not->toBe($path2->value); + }); +}); + +describe('UuidPathGenerator generation with directory', function (): void { + it('prepends the given directory to the generated UUID filename', function (): void { + // GIVEN + $generator = new UuidPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'invoice.pdf', + sanitizedName: 'invoice.pdf', + contents: FileStream::fromString('PDF DATA') + ); + $directory = 'billing/invoices'; + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + expect($result)->toBeInstanceOf(StorageFilePath::class) + ->and($result->value)->toStartWith('billing/invoices/') + ->and($result->value)->toMatch('#^billing/invoices/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.pdf$#'); + }); + + it('trims leading and trailing slashes from the directory', function (string $directory): void { + // GIVEN + $generator = new UuidPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'file.txt', + sanitizedName: 'file.txt', + contents: FileStream::fromString('text') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + expect($result->value)->toStartWith('custom/path/'); + })->with([ + 'leading slash' => '/custom/path', + 'trailing slash' => 'custom/path/', + 'both slashes' => '/custom/path/', + 'multiple outer slashes' => '///custom/path///', + ]); + + it('ignores empty or slash-only directory', function (string $directory): void { + // GIVEN + $generator = new UuidPathGenerator(); + $disk = new DiskName('local', 'local'); + $sanitizedFile = new SanitizedFile( + originalName: 'file.txt', + sanitizedName: 'file.txt', + contents: FileStream::fromString('text') + ); + + // WHEN + $result = $generator->generate($disk, $sanitizedFile, $directory); + + // THEN + expect($result->value)->toMatch('#^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.txt$#'); + })->with([ + 'empty string' => '', + 'single slash' => '/', + 'multi slashes' => '///', + ]); +});