$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); } } }); });