ldc-file-storage-service/tests/Unit/Lib/PathGenerators/DateHashPathGeneratorTest.php
Jérémy Munsch dcfec3de04
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.
2026-11-03 17:04:29 +01:00

199 lines
6.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Uwil\Storage\Tests\Unit\Lib\PathGenerators;
use Uwil\Storage\Lib\PathGenerators\DateHashPathGenerator;
use Uwil\Storage\Lib\ValueObjects\DiskName;
use Uwil\Storage\Lib\ValueObjects\FileStream;
use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
covers(DateHashPathGenerator::class);
describe('DateHashPathGenerator generation without directory', function (): void {
it('generates a storage file path with UTC date segments and hash prefix', function (): void {
// GIVEN
$generator = new DateHashPathGenerator();
$disk = new DiskName('local', 'local');
$sanitizedFile = new SanitizedFile(
originalName: 'document.pdf',
sanitizedName: 'document.pdf',
contents: FileStream::fromString('test content')
);
$expectedYear = gmdate('Y');
$expectedMonth = gmdate('m');
$expectedDay = gmdate('d');
$expectedHash = substr(hash('sha256', 'document.pdf'), 0, 2);
// WHEN
$result = $generator->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$#');
});
});