getAllowedExtensions())->toBeNull(); expect($validator->getBlockedExtensions())->toBe(['exe', 'bat', 'sh', 'cmd', 'com']); expect($validator->getMaxFileSize())->toBeInstanceOf(FileSize::class); expect($validator->getMaxFileSize()->toBytes())->toBe(100 * 1024 * 1024); // 100MB }); it('creates strict validator with allowed extensions only', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); expect($validator->getAllowedExtensions())->toBe(['txt', 'pdf']); expect($validator->getBlockedExtensions())->toBeNull(); expect($validator->getMaxFileSize()->toBytes())->toBe(50 * 1024 * 1024); // 50MB }); it('creates upload validator with secure defaults', function () { $validator = FileValidator::forUploads(); expect($validator->getAllowedExtensions())->toBe(['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'csv', 'json']); expect($validator->getBlockedExtensions())->toBe(['exe', 'bat', 'sh', 'cmd', 'com', 'php', 'phtml']); expect($validator->getMaxFileSize()->toBytes())->toBe(10 * 1024 * 1024); // 10MB }); it('creates image validator with image extensions only', function () { $validator = FileValidator::forImages(); expect($validator->getAllowedExtensions())->toBe(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']); expect($validator->getBlockedExtensions())->toBeNull(); expect($validator->getMaxFileSize()->toBytes())->toBe(5 * 1024 * 1024); // 5MB }); it('allows custom max size for uploads', function () { $customSize = FileSize::fromMegabytes(20); $validator = FileValidator::forUploads($customSize); expect($validator->getMaxFileSize()->toBytes())->toBe(20 * 1024 * 1024); }); // Path Validation Tests it('validates normal file paths', function () { $validator = FileValidator::createDefault(); $validator->validatePath('/var/www/html/test.txt'); $validator->validatePath('relative/path/file.json'); expect(true)->toBeTrue(); // No exception thrown }); it('throws exception for empty path', function () { $validator = FileValidator::createDefault(); try { $validator->validatePath(''); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('File path cannot be empty'); } }); it('throws exception for path with null bytes', function () { $validator = FileValidator::createDefault(); try { $validator->validatePath("/path/to/file\0.txt"); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('null bytes'); } }); it('detects path traversal with ../', function () { $validator = FileValidator::createDefault(); try { $validator->validatePath('../../../etc/passwd'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal attempt'); } }); it('detects path traversal with backslash notation', function () { $validator = FileValidator::createDefault(); try { $validator->validatePath('..\\..\\windows\\system32'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal attempt'); } }); it('detects URL-encoded path traversal', function () { $validator = FileValidator::createDefault(); try { $validator->validatePath('/path/%2e%2e/etc/passwd'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal attempt'); } }); // Extension Validation Tests it('validates allowed extensions', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); $validator->validateExtension('/path/to/file.txt'); $validator->validateExtension('/path/to/document.pdf'); expect(true)->toBeTrue(); // No exception thrown }); it('throws exception for disallowed extension', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); try { $validator->validateExtension('/path/to/file.exe'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('not allowed'); } }); it('throws exception for blocked extension', function () { $validator = FileValidator::createDefault(); try { $validator->validateExtension('/path/to/malware.exe'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('is blocked'); } }); it('allows files without extension when no whitelist defined', function () { $validator = FileValidator::createDefault(); $validator->validateExtension('/path/to/Makefile'); expect(true)->toBeTrue(); // No exception thrown }); it('throws exception for files without extension when whitelist defined', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); try { $validator->validateExtension('/path/to/Makefile'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('no extension'); } }); it('handles case-insensitive extension validation', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); $validator->validateExtension('/path/to/file.TXT'); $validator->validateExtension('/path/to/document.PDF'); expect(true)->toBeTrue(); // No exception thrown }); // FileSize Validation Tests it('validates file size within limit', function () { $validator = FileValidator::forUploads(FileSize::fromMegabytes(10)); $validator->validateFileSize(FileSize::fromMegabytes(5)); $validator->validateFileSize(FileSize::fromMegabytes(9)); expect(true)->toBeTrue(); // No exception thrown }); it('throws exception when file size exceeds limit', function () { $validator = FileValidator::forUploads(FileSize::fromMegabytes(10)); try { $validator->validateFileSize(FileSize::fromMegabytes(15)); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('exceeds maximum allowed size'); } }); it('allows any file size when no limit defined', function () { $validator = new FileValidator( allowedExtensions: null, blockedExtensions: null, maxFileSize: null, baseDirectory: null ); $validator->validateFileSize(FileSize::fromGigabytes(10)); expect(true)->toBeTrue(); // No exception thrown }); // Extension Check Helper Tests it('checks if extension is allowed', function () { $validator = FileValidator::createStrict(['txt', 'pdf']); expect($validator->isExtensionAllowed('txt'))->toBeTrue(); expect($validator->isExtensionAllowed('.pdf'))->toBeTrue(); // With dot expect($validator->isExtensionAllowed('TXT'))->toBeTrue(); // Case insensitive expect($validator->isExtensionAllowed('exe'))->toBeFalse(); }); it('checks blocked extensions correctly', function () { $validator = FileValidator::createDefault(); expect($validator->isExtensionAllowed('exe'))->toBeFalse(); expect($validator->isExtensionAllowed('txt'))->toBeTrue(); expect($validator->isExtensionAllowed('pdf'))->toBeTrue(); }); it('allows all extensions when no restrictions', function () { $validator = new FileValidator(); expect($validator->isExtensionAllowed('exe'))->toBeTrue(); expect($validator->isExtensionAllowed('anything'))->toBeTrue(); }); // Composite Validation Tests it('validates upload with all checks', function () { $validator = FileValidator::forUploads(); $validator->validateUpload('/path/to/image.jpg', FileSize::fromMegabytes(2)); expect(true)->toBeTrue(); // No exception thrown }); it('composite upload validation catches path traversal', function () { $validator = FileValidator::forUploads(); try { $validator->validateUpload('../../../etc/passwd.jpg', FileSize::fromMegabytes(1)); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); it('composite upload validation catches disallowed extension', function () { $validator = FileValidator::forUploads(); try { $validator->validateUpload('/path/to/script.php', FileSize::fromMegabytes(1)); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('is not allowed'); } }); it('composite upload validation catches oversized file', function () { $validator = FileValidator::forUploads(); try { $validator->validateUpload('/path/to/image.jpg', FileSize::fromMegabytes(50)); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('exceeds maximum'); } }); // Getter Tests it('provides access to configuration', function () { $allowedExtensions = ['txt', 'pdf']; $blockedExtensions = ['exe', 'bat']; $maxSize = FileSize::fromMegabytes(20); $validator = new FileValidator( allowedExtensions: $allowedExtensions, blockedExtensions: $blockedExtensions, maxFileSize: $maxSize ); expect($validator->getAllowedExtensions())->toBe($allowedExtensions); expect($validator->getBlockedExtensions())->toBe($blockedExtensions); expect($validator->getMaxFileSize())->toBe($maxSize); }); });