178 lines
5.1 KiB
PHP
178 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Uwil\Storage\Tests\Unit\Lib\ValueObjects;
|
|
|
|
use Uwil\Storage\Exceptions\UnexpectedStorageException;
|
|
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
|
|
covers(StorageFilePath::class);
|
|
|
|
describe('StorageFilePath instantiation', function (): void {
|
|
it('can instantiate with a valid single file name', function (): void {
|
|
// GIVEN
|
|
$path = 'document.pdf';
|
|
|
|
// WHEN
|
|
$vo = new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($vo)->toBeInstanceOf(StorageFilePath::class)
|
|
->and($vo->value)->toBe('document.pdf');
|
|
});
|
|
|
|
it('can instantiate with a valid nested path', function (string $validPath): void {
|
|
// GIVEN
|
|
$path = $validPath;
|
|
|
|
// WHEN
|
|
$vo = new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($vo)->toBeInstanceOf(StorageFilePath::class)
|
|
->and($vo->value)->toBe($validPath);
|
|
})->with([
|
|
'simple directory' => 'uploads/file.png',
|
|
'multi-level nested' => '2026/11/03/invoice_123.pdf',
|
|
'with hyphens dots' => 'user-data/avatar.min.jpg',
|
|
]);
|
|
});
|
|
|
|
describe('StorageFilePath validation', function (): void {
|
|
it('throws exception if path is empty or only whitespace', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path cannot be empty.'
|
|
);
|
|
})->with([
|
|
'empty string' => '',
|
|
'spaces only' => ' ',
|
|
'tabs and newlines' => "\t \n \r",
|
|
]);
|
|
|
|
it('throws exception if path starts with a slash', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path must not start with a "/".'
|
|
);
|
|
})->with([
|
|
'single slash root' => '/file.txt',
|
|
'leading nested path' => '/uploads/document.pdf',
|
|
]);
|
|
|
|
it('throws exception if path ends with a slash', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path must not end with a "/".'
|
|
);
|
|
})->with([
|
|
'trailing slash on file' => 'file.txt/',
|
|
'trailing directory' => 'uploads/2026/',
|
|
]);
|
|
|
|
it('throws exception if path contains double slashes', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path must not contain "//".'
|
|
);
|
|
})->with([
|
|
'double slash in middle' => 'uploads//file.txt',
|
|
'triple slash' => 'folder///nested/file.txt',
|
|
]);
|
|
|
|
it('throws exception if path contains backslashes', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path must not contain "\\".'
|
|
);
|
|
})->with([
|
|
'windows separator' => 'uploads\\file.txt',
|
|
'multiple backslash' => 'folder\\sub\\file.txt',
|
|
]);
|
|
|
|
it('throws exception if path contains directory traversal', function (string $invalidPath): void {
|
|
// GIVEN
|
|
$path = $invalidPath;
|
|
|
|
// WHEN
|
|
$action = fn () => new StorageFilePath($path);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(
|
|
UnexpectedStorageException::class,
|
|
'Path must not contain "..".'
|
|
);
|
|
})->with([
|
|
'relative parent traversal' => 'uploads/../secret.txt',
|
|
'leading traversal' => '../file.txt',
|
|
'trailing traversal' => 'folder/..',
|
|
]);
|
|
});
|
|
|
|
describe('StorageFilePath json serialization', function (): void {
|
|
it('returns the path when json serialized', function (): void {
|
|
// GIVEN
|
|
$path = 'documents/archive/2026.zip';
|
|
$vo = new StorageFilePath($path);
|
|
|
|
// WHEN
|
|
$jsonSerialized = $vo->jsonSerialize();
|
|
$jsonEncoded = json_encode($vo);
|
|
|
|
// THEN
|
|
expect($jsonSerialized)->toBe($path)
|
|
->and($jsonEncoded)->toBe('"documents\/archive\/2026.zip"');
|
|
});
|
|
});
|
|
|
|
describe('StorageFilePath string representation', function (): void {
|
|
it('returns the path when cast to string or via __toString', function (): void {
|
|
// GIVEN
|
|
$path = 'documents/archive/2026.zip';
|
|
$vo = new StorageFilePath($path);
|
|
|
|
// WHEN
|
|
$asString = (string) $vo;
|
|
$fromToStringMethod = $vo->__toString();
|
|
|
|
// THEN
|
|
expect($asString)->toBe($path)
|
|
->and($fromToStringMethod)->toBe($path);
|
|
});
|
|
});
|