- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
284 lines
10 KiB
PHP
284 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\FileSize;
|
|
use App\Framework\Filesystem\FileStorage;
|
|
use App\Framework\Filesystem\FileValidator;
|
|
use App\Framework\Filesystem\Exceptions\FileValidationException;
|
|
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
|
|
|
|
describe('FileStorage Integration', function () {
|
|
beforeEach(function () {
|
|
// Create temp directory for tests
|
|
$this->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');
|
|
}
|
|
});
|
|
});
|