163 lines
5.2 KiB
PHP
163 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Domain\Models\ValueObjects;
|
|
|
|
use Domain\Models\ValueObjects\PicturePath;
|
|
|
|
covers(PicturePath::class);
|
|
|
|
describe('picture path', function (): void {
|
|
it('creates a valid PicturePath', function (string $pathValue): void {
|
|
// WHEN
|
|
$path = new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($path)->toBeInstanceOf(PicturePath::class)
|
|
->and($path->value)->toBe($pathValue);
|
|
})->with([
|
|
'simple path' => 'coupons/logo.png',
|
|
'nested directory' => 'uploads/2026/03/avatar.jpg',
|
|
'safe punctuation' => 'images/my-photo_(1).webp',
|
|
'windows backslash' => 'coupons\sub\logo.jpeg',
|
|
]);
|
|
|
|
it('rejects an empty or whitespace-only path', function (string $pathValue): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path cannot be empty.'
|
|
);
|
|
})->with([
|
|
'empty' => '',
|
|
'spaces' => ' ',
|
|
]);
|
|
|
|
it('rejects a path that exceeds the maximum allowed length', function (): void {
|
|
// GIVEN
|
|
$pathValue = str_repeat('a', 197) . '.jpg'; // 201 chars
|
|
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path exceeds the maximum allowed length.'
|
|
);
|
|
});
|
|
|
|
it('accepts a path of exactly the maximum length', function (): void {
|
|
// GIVEN
|
|
$pathValue = str_repeat('a', 196) . '.jpg'; // 200 chars
|
|
|
|
// WHEN
|
|
$path = new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($path)->toBeInstanceOf(PicturePath::class)
|
|
->and(mb_strlen($path->value))->toBe(200);
|
|
});
|
|
|
|
it('rejects null byte characters', function (): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath("coupons/logo\0.png");
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path contains invalid null byte.'
|
|
);
|
|
});
|
|
|
|
it('rejects invalid, invisible, or forbidden filesystem characters', function (string $pathValue): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path contains invalid, non-printable, or dangerous characters.'
|
|
);
|
|
})->with([
|
|
'contains space' => 'coupons/my logo.png',
|
|
'contains newline' => "coupons/logo\n.png",
|
|
'contains asterisk' => 'coupons/*.png',
|
|
'contains question' => 'coupons/img?.png',
|
|
'contains pipe' => 'coupons/logo|1.png',
|
|
'contains colon' => 'coupons:logo.png',
|
|
'contains quotes' => 'coupons/"logo".png',
|
|
]);
|
|
|
|
it('rejects paths starting with double slashes', function (): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath('//coupons/logo.png');
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path must not start with "//".'
|
|
);
|
|
});
|
|
|
|
it('rejects drive paths and URIs', function (string $pathValue): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path must not be an absolute drive path or a URI.'
|
|
);
|
|
})->with([
|
|
'windows drive letter' => 'C:/images/logo.png',
|
|
'windows backslash' => 'D:\images\logo.png',
|
|
'http uri' => 'http://example.com/logo.png',
|
|
'https uri' => 'https://example.com/logo.png',
|
|
'custom scheme' => 's3://bucket/logo.png',
|
|
]);
|
|
|
|
it('rejects directory traversal segments', function (string $pathValue): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path must not contain relative traversal segments ("." or "..").'
|
|
);
|
|
})->with([
|
|
'parent traversal' => '../logo.png',
|
|
'nested parent traversal' => 'coupons/../../secret.png',
|
|
'current directory segment' => 'coupons/./logo.png',
|
|
]);
|
|
|
|
it('rejects an invalid or missing image extension', function (string $pathValue): void {
|
|
// WHEN
|
|
$action = fn (): PicturePath => new PicturePath($pathValue);
|
|
|
|
// THEN
|
|
expect($action)
|
|
->toThrow(
|
|
\DomainException::class,
|
|
'Picture Path must end with one of: ' .
|
|
implode(', ', PicturePath::ALLOWED_EXTENSIONS)
|
|
);
|
|
})->with([
|
|
'no extension' => 'coupons/image',
|
|
'forbidden extension' => 'coupons/image.gif',
|
|
'executable extension' => 'coupons/shell.php',
|
|
'trailing dot' => 'coupons/image.',
|
|
]);
|
|
});
|