testDir = sys_get_temp_dir() . '/filesystem_test_' . uniqid(); mkdir($this->testDir, 0777, true); }); afterEach(function () { // Clean up temp directory if (is_dir($this->testDir)) { $files = glob($this->testDir . '/*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } rmdir($this->testDir); } }); // Storage without Validator or Logger it('works without validator or logger', function () { $storage = new FileStorage($this->testDir); $storage->put('test.txt', 'Hello World'); $content = $storage->get('test.txt'); expect($content)->toBe('Hello World'); expect($storage->exists('test.txt'))->toBeTrue(); }); // Storage with FileValidator integration it('validates path traversal attempts with validator', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); try { $storage->get('../../../etc/passwd'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); it('validates file extensions with validator on write', function () { $validator = FileValidator::createStrict(['txt', 'json']); $storage = new FileStorage($this->testDir, validator: $validator); // Allowed extension should work $storage->put('allowed.txt', 'content'); expect($storage->exists('allowed.txt'))->toBeTrue(); // Disallowed extension should fail try { $storage->put('blocked.exe', 'malicious'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('not allowed'); } }); it('validates file size with validator on write', function () { $validator = FileValidator::forUploads(FileSize::fromKilobytes(1)); // 1KB limit $storage = new FileStorage($this->testDir, validator: $validator); // Small file should work $storage->put('small.txt', 'Small content'); expect($storage->exists('small.txt'))->toBeTrue(); // Large file should fail $largeContent = str_repeat('X', 2048); // 2KB try { $storage->put('large.txt', $largeContent); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('exceeds maximum'); } }); // Note: validateRead() does not validate extensions by design // Extensions are validated on write operations only // This is the correct behavior - when reading files, extension doesn't matter // Edge cases it('handles nonexistent files with validator', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); try { $storage->get('nonexistent.txt'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileNotFoundException $e) { expect($e)->toBeInstanceOf(FileNotFoundException::class); } }); it('validates before attempting file operations', function () { $validator = FileValidator::forUploads(); $storage = new FileStorage($this->testDir, validator: $validator); // Path traversal should fail validation before attempting filesystem operation try { $storage->put('../../../tmp/malicious.txt', 'content'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } // Verify file was NOT created (path traversal prevented) expect(file_exists($this->testDir . '/../../../tmp/malicious.txt'))->toBeFalse(); }); // Performance: Large file handling it('handles large files efficiently', function () { $validator = FileValidator::forUploads(FileSize::fromMegabytes(10)); $storage = new FileStorage($this->testDir, validator: $validator); // Create 1MB file $largeContent = str_repeat('X', 1024 * 1024); $storage->put('large.txt', $largeContent); $content = $storage->get('large.txt'); expect(strlen($content))->toBe(1024 * 1024); }); // Multiple operations workflow it('handles complete file lifecycle with validation', function () { $validator = FileValidator::createStrict(['txt', 'json']); $storage = new FileStorage($this->testDir, validator: $validator); // Create $storage->put('lifecycle.txt', 'initial content'); expect($storage->exists('lifecycle.txt'))->toBeTrue(); // Read $content = $storage->get('lifecycle.txt'); expect($content)->toBe('initial content'); // Update $storage->put('lifecycle.txt', 'updated content'); $updatedContent = $storage->get('lifecycle.txt'); expect($updatedContent)->toBe('updated content'); // Copy $storage->copy('lifecycle.txt', 'lifecycle-copy.txt'); expect($storage->exists('lifecycle-copy.txt'))->toBeTrue(); // Delete $storage->delete('lifecycle.txt'); expect($storage->exists('lifecycle.txt'))->toBeFalse(); // Copy still exists expect($storage->exists('lifecycle-copy.txt'))->toBeTrue(); }); // Validator prevents invalid operations it('prevents path traversal on read operations', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); try { $storage->get('../../sensitive/file.txt'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); it('prevents path traversal on copy operations', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); $storage->put('source.txt', 'content'); try { $storage->copy('source.txt', '../../../etc/target.txt'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); it('prevents path traversal on delete operations', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); try { $storage->delete('../../../important/file.txt'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); // URL-encoded path traversal prevention it('prevents URL-encoded path traversal attacks', function () { $validator = FileValidator::createDefault(); $storage = new FileStorage($this->testDir, validator: $validator); try { $storage->put('%2e%2e/%2e%2e/malicious.txt', 'content'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('Path traversal'); } }); // Multiple validator configurations it('enforces strict whitelist validation', function () { $validator = FileValidator::createStrict(['txt']); $storage = new FileStorage($this->testDir, validator: $validator); // Only .txt should work $storage->put('allowed.txt', 'content'); expect($storage->exists('allowed.txt'))->toBeTrue(); // Everything else should fail $blocked = ['file.json', 'file.pdf', 'file.exe', 'file.jpg']; foreach ($blocked as $filename) { try { $storage->put($filename, 'content'); expect(true)->toBeFalse("Should have blocked {$filename}"); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('not allowed'); } } }); // Image validator integration it('works with image validator', function () { $validator = FileValidator::forImages(); $storage = new FileStorage($this->testDir, validator: $validator); // Image extensions should work $allowedImages = ['photo.jpg', 'image.png', 'graphic.gif', 'icon.webp']; foreach ($allowedImages as $filename) { $storage->put($filename, 'fake image data'); expect($storage->exists($filename))->toBeTrue(); } // Non-image should fail try { $storage->put('document.pdf', 'content'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('not allowed'); } }); // Upload validator integration it('works with upload validator', function () { $validator = FileValidator::forUploads(); $storage = new FileStorage($this->testDir, validator: $validator); // Common upload types should work $allowedFiles = ['data.json', 'report.pdf', 'document.txt', 'data.csv']; foreach ($allowedFiles as $filename) { $storage->put($filename, 'content'); expect($storage->exists($filename))->toBeTrue(); } // Executable should fail try { $storage->put('malware.exe', 'malicious'); expect(true)->toBeFalse('Should have thrown exception'); } catch (FileValidationException $e) { expect($e->getMessage())->toContain('not allowed'); } }); });