366 lines
13 KiB
PHP
366 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Uwil\Storage\Tests\Unit\Lib;
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
use Uwil\Storage\Config\StorageServiceConfig;
|
|
use Uwil\Storage\Contracts\MimeTypeChecker;
|
|
use Uwil\Storage\Exceptions\BadUploadedFile;
|
|
use Uwil\Storage\Lib\FileSanitizerImpl;
|
|
use Uwil\Storage\Lib\ValueObjects\SanitizedFile;
|
|
|
|
covers(FileSanitizerImpl::class);
|
|
|
|
/**
|
|
* @param array<string> $allowedExtensions
|
|
*/
|
|
function createConfig(array $allowedExtensions = ['jpg', 'png', 'pdf', 'txt']): StorageServiceConfig
|
|
{
|
|
return new StorageServiceConfig(
|
|
defaultDisk: 'local',
|
|
eligibleDisks: ['local'],
|
|
laravelExistingDisks: [(object) ['disk' => 'local', 'driver' => 'local']],
|
|
allowedExtensions: $allowedExtensions
|
|
);
|
|
}
|
|
|
|
describe('FileSanitizerImpl sanitizeFileName', function (): void {
|
|
it('cleans directory paths and replaces invalid characters with underscore', function (): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$cleaned = $sanitizer->sanitizeFileName('path/to/my special @file #1.pdf');
|
|
|
|
// THEN
|
|
expect($cleaned)->toBe('my_special_file_1.pdf');
|
|
});
|
|
|
|
it('strips leading dots and dashes from the filename', function (string $input, string $expected): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$cleaned = $sanitizer->sanitizeFileName($input);
|
|
|
|
// THEN
|
|
expect($cleaned)->toBe($expected);
|
|
})->with([
|
|
'leading dots' => ['...hidden.txt', 'hidden.txt'],
|
|
'leading dashes' => ['---my-file.png', 'my-file.png'],
|
|
'mixed leading' => ['.-.-avatar.jpg', 'avatar.jpg'],
|
|
]);
|
|
|
|
it('compresses multiple consecutive underscores and multiple dots', function (): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$cleaned = $sanitizer->sanitizeFileName('my___file....name.pdf');
|
|
|
|
// THEN
|
|
expect($cleaned)->toBe('my_file.name.pdf');
|
|
});
|
|
|
|
it('truncates filename exceeding maximum 255 characters while preserving extension', function (): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$longBaseName = str_repeat('a', 300);
|
|
$originalName = $longBaseName . '.pdf';
|
|
|
|
// WHEN
|
|
$cleaned = $sanitizer->sanitizeFileName($originalName);
|
|
|
|
// THEN
|
|
expect(strlen($cleaned))->toBeLessThanOrEqual(255)
|
|
->and($cleaned)->toEndWith('.pdf');
|
|
});
|
|
});
|
|
|
|
describe('FileSanitizerImpl sanitizeFile', function (): void {
|
|
it('successfully sanitizes an SplFileObject', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_spl_') . '.txt';
|
|
file_put_contents($tempFile, 'Sample content');
|
|
|
|
$config = createConfig(['txt']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$mimeChecker->shouldReceive('detectMimeType')->with($tempFile)->andReturn('text/plain');
|
|
$mimeChecker->shouldReceive('isMimeAllowedForExtension')->with('txt', 'text/plain')->andReturn(true);
|
|
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$splFile = new \SplFileObject($tempFile);
|
|
|
|
try {
|
|
// WHEN
|
|
$result = $sanitizer->sanitizeFile($splFile);
|
|
|
|
// THEN
|
|
expect($result)->toBeInstanceOf(SanitizedFile::class)
|
|
->and($result->mimeType)->toBe('text/plain')
|
|
->and($result->size)->toBe(strlen('Sample content'));
|
|
} finally {
|
|
unset($splFile);
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('throws BadUploadedFile if file is not allowed by extension', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_bad_ext_') . '.exe';
|
|
file_put_contents($tempFile, 'content');
|
|
|
|
$config = createConfig(['pdf']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$splFile = new \SplFileObject($tempFile);
|
|
|
|
try {
|
|
// WHEN
|
|
$action = fn () => $sanitizer->sanitizeFile($splFile);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(BadUploadedFile::class, 'is not allowed for file');
|
|
} finally {
|
|
unset($splFile);
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('throws BadUploadedFile if detected MIME is disallowed for extension', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_bad_mime_') . '.txt';
|
|
file_put_contents($tempFile, 'content');
|
|
|
|
$config = createConfig(['txt']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$mimeChecker->shouldReceive('detectMimeType')->with($tempFile)->andReturn('application/x-executable');
|
|
$mimeChecker->shouldReceive('isMimeAllowedForExtension')->andReturn(false);
|
|
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$splFile = new \SplFileObject($tempFile);
|
|
|
|
try {
|
|
// WHEN
|
|
$action = fn () => $sanitizer->sanitizeFile($splFile);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(BadUploadedFile::class, 'is forbidden or invalid for extension');
|
|
} finally {
|
|
unset($splFile);
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('throws BadUploadedFile if file cannot be read', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_deleted_') . '.txt';
|
|
file_put_contents($tempFile, 'data');
|
|
$splFile = new \SplFileObject($tempFile);
|
|
unlink($tempFile);
|
|
|
|
$config = createConfig(['txt']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$action = fn () => $sanitizer->sanitizeFile($splFile);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(BadUploadedFile::class, 'Unable to read file:');
|
|
});
|
|
});
|
|
|
|
describe('FileSanitizerImpl sanitizeUploaded', function (): void {
|
|
it('successfully sanitizes a valid UploadedFile', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_upload_');
|
|
file_put_contents($tempFile, 'Uploaded document content');
|
|
|
|
$config = createConfig(['pdf']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$mimeChecker->shouldReceive('detectMimeType')->with($tempFile)->andReturn('application/pdf');
|
|
$mimeChecker->shouldReceive('isMimeAllowedForExtension')->with('pdf', 'application/pdf')->andReturn(true);
|
|
|
|
$uploadedFile = new UploadedFile($tempFile, 'Invoice #100.pdf', 'application/pdf', null, true);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
try {
|
|
// WHEN
|
|
$result = $sanitizer->sanitizeUploaded($uploadedFile);
|
|
|
|
// THEN
|
|
expect($result)->toBeInstanceOf(SanitizedFile::class)
|
|
->and($result->originalName)->toBe('Invoice #100.pdf')
|
|
->and($result->sanitizedName)->toBe('Invoice_100.pdf')
|
|
->and($result->mimeType)->toBe('application/pdf');
|
|
} finally {
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('throws BadUploadedFile if uploaded file is unreadable', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_del_up_') . '.pdf';
|
|
file_put_contents($tempFile, 'data');
|
|
$uploadedFile = new UploadedFile($tempFile, 'test.pdf', null, null, true);
|
|
unlink($tempFile);
|
|
|
|
$config = createConfig(['pdf']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$action = fn () => $sanitizer->sanitizeUploaded($uploadedFile);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(BadUploadedFile::class, 'Unable to read uploaded file:');
|
|
});
|
|
|
|
it('throws BadUploadedFile if UploadedFile reports error', function (): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$uploadedFile = new UploadedFile('/tmp/fake.txt', 'fake.txt', null, UPLOAD_ERR_INI_SIZE, false);
|
|
|
|
// WHEN
|
|
$action = fn () => $sanitizer->sanitizeUploaded($uploadedFile);
|
|
|
|
// THEN
|
|
expect($action)->toThrow(BadUploadedFile::class, 'Uploaded file is not valid.');
|
|
});
|
|
});
|
|
|
|
describe('FileSanitizerImpl fileIsValid', function (): void {
|
|
it('returns false when uploaded file path does not exist or is unreadable', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_del_valid_') . '.txt';
|
|
file_put_contents($tempFile, 'data');
|
|
$uploadedFile = new UploadedFile($tempFile, 'test.txt', null, null, true);
|
|
unlink($tempFile);
|
|
|
|
$config = createConfig(['txt']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
// WHEN
|
|
$isValid = $sanitizer->fileIsValid($uploadedFile);
|
|
|
|
// THEN
|
|
expect($isValid)->toBeFalse();
|
|
});
|
|
it('returns true when UploadedFile has allowed extension and allowed MIME', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_valid_');
|
|
file_put_contents($tempFile, 'Valid file');
|
|
|
|
$config = createConfig(['txt']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$mimeChecker->shouldReceive('detectMimeType')->with($tempFile)->andReturn('text/plain');
|
|
$mimeChecker->shouldReceive('isMimeAllowedForExtension')->with('txt', 'text/plain')->andReturn(true);
|
|
|
|
$uploadedFile = new UploadedFile($tempFile, 'notes.txt', 'text/plain', null, true);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
try {
|
|
// WHEN
|
|
$isValid = $sanitizer->fileIsValid($uploadedFile);
|
|
|
|
// THEN
|
|
expect($isValid)->toBeTrue();
|
|
} finally {
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns false when UploadedFile reports upload error', function (): void {
|
|
// GIVEN
|
|
$config = createConfig();
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
$uploadedFile = new UploadedFile('/tmp/fake.txt', 'fake.txt', null, UPLOAD_ERR_NO_FILE, false);
|
|
|
|
// WHEN
|
|
$isValid = $sanitizer->fileIsValid($uploadedFile);
|
|
|
|
// THEN
|
|
expect($isValid)->toBeFalse();
|
|
});
|
|
|
|
it('returns false when extension is not allowed', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_not_allowed_ext_');
|
|
file_put_contents($tempFile, 'Executable');
|
|
|
|
$config = createConfig(['pdf']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$uploadedFile = new UploadedFile($tempFile, 'app.exe', null, null, true);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
try {
|
|
// WHEN
|
|
$isValid = $sanitizer->fileIsValid($uploadedFile);
|
|
|
|
// THEN
|
|
expect($isValid)->toBeFalse();
|
|
} finally {
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns false when MIME type is not allowed for extension', function (): void {
|
|
// GIVEN
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_mime_mismatch_');
|
|
file_put_contents($tempFile, 'Fake PDF');
|
|
|
|
$config = createConfig(['pdf']);
|
|
$mimeChecker = \Mockery::mock(MimeTypeChecker::class);
|
|
|
|
$mimeChecker->shouldReceive('detectMimeType')->with($tempFile)->andReturn('application/x-php');
|
|
$mimeChecker->shouldReceive('isMimeAllowedForExtension')->with('pdf', 'application/x-php')->andReturn(false);
|
|
|
|
$uploadedFile = new UploadedFile($tempFile, 'fake.pdf', null, null, true);
|
|
$sanitizer = new FileSanitizerImpl($config, $mimeChecker);
|
|
|
|
try {
|
|
// WHEN
|
|
$isValid = $sanitizer->fileIsValid($uploadedFile);
|
|
|
|
// THEN
|
|
expect($isValid)->toBeFalse();
|
|
} finally {
|
|
if (file_exists($tempFile)) {
|
|
unlink($tempFile);
|
|
}
|
|
}
|
|
});
|
|
});
|