- 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.
401 lines
14 KiB
PHP
401 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Filesystem\CachedFileStorage;
|
|
use App\Framework\Filesystem\FileStorage;
|
|
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
|
|
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
|
|
|
|
// Helper function for recursive directory deletion
|
|
function deleteDirectoryRecursive(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$files = array_diff(scandir($dir), ['.', '..']);
|
|
|
|
foreach ($files as $file) {
|
|
$path = $dir . '/' . $file;
|
|
if (is_dir($path)) {
|
|
deleteDirectoryRecursive($path);
|
|
} else {
|
|
unlink($path);
|
|
}
|
|
}
|
|
|
|
rmdir($dir);
|
|
}
|
|
|
|
describe('CachedFileStorage', function () {
|
|
beforeEach(function () {
|
|
// Create temporary test directory
|
|
$this->testDir = sys_get_temp_dir() . '/cached_storage_test_' . uniqid();
|
|
mkdir($this->testDir, 0777, true);
|
|
|
|
// Create wrapped storage
|
|
$this->storage = new FileStorage(basePath: $this->testDir);
|
|
$this->cachedStorage = new CachedFileStorage($this->storage, basePath: $this->testDir);
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Cleanup test directory
|
|
if (is_dir($this->testDir)) {
|
|
deleteDirectoryRecursive($this->testDir);
|
|
}
|
|
});
|
|
|
|
|
|
describe('Directory Caching', function () {
|
|
it('caches directory on first put', function () {
|
|
$path = 'subdir/file.txt';
|
|
$content = 'test content';
|
|
|
|
// First put - directory will be created and cached
|
|
$this->cachedStorage->put($path, $content);
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
expect($stats['cached_directories'])->toBeGreaterThan(0);
|
|
|
|
// Verify directory exists
|
|
$dir = $this->testDir . '/subdir';
|
|
expect(is_dir($dir))->toBeTrue();
|
|
});
|
|
|
|
it('reuses cached directory on subsequent puts', function () {
|
|
$dir = 'nested/deep/directory';
|
|
|
|
// First put - creates and caches directory
|
|
$this->cachedStorage->put($dir . '/file1.txt', 'content1');
|
|
|
|
$statsBefore = $this->cachedStorage->getCacheStats();
|
|
$cacheSizeBefore = $statsBefore['cached_directories'];
|
|
|
|
// Second put - reuses cached directory (no new is_dir call)
|
|
$this->cachedStorage->put($dir . '/file2.txt', 'content2');
|
|
|
|
$statsAfter = $this->cachedStorage->getCacheStats();
|
|
$cacheSizeAfter = $statsAfter['cached_directories'];
|
|
|
|
// Cache size should be same (directory already cached)
|
|
expect($cacheSizeAfter)->toBe($cacheSizeBefore);
|
|
|
|
// Both files should exist
|
|
expect(file_exists($this->testDir . '/' . $dir . '/file1.txt'))->toBeTrue();
|
|
expect(file_exists($this->testDir . '/' . $dir . '/file2.txt'))->toBeTrue();
|
|
});
|
|
|
|
it('caches parent directories recursively', function () {
|
|
$path = 'level1/level2/level3/file.txt';
|
|
|
|
$this->cachedStorage->put($path, 'test');
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
|
|
// Should cache level1, level1/level2, and level1/level2/level3
|
|
expect($stats['cached_directories'])->toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('handles absolute paths correctly', function () {
|
|
$absolutePath = $this->testDir . '/absolute/file.txt';
|
|
|
|
$this->cachedStorage->put($absolutePath, 'test');
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
expect($stats['cached_directories'])->toBeGreaterThan(0);
|
|
|
|
expect(file_exists($absolutePath))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Read Operations', function () {
|
|
it('delegates get to wrapped storage', function () {
|
|
$path = 'test.txt';
|
|
$content = 'test content';
|
|
|
|
// Write file directly via wrapped storage
|
|
$this->storage->put($path, $content);
|
|
|
|
// Read via cached storage
|
|
$result = $this->cachedStorage->get($path);
|
|
|
|
expect($result)->toBe($content);
|
|
});
|
|
|
|
it('throws FileNotFoundException for non-existent files', function () {
|
|
expect(fn() => $this->cachedStorage->get('nonexistent.txt'))
|
|
->toThrow(FileNotFoundException::class);
|
|
});
|
|
|
|
it('checks file existence correctly', function () {
|
|
$path = 'exists.txt';
|
|
|
|
expect($this->cachedStorage->exists($path))->toBeFalse();
|
|
|
|
$this->cachedStorage->put($path, 'content');
|
|
|
|
expect($this->cachedStorage->exists($path))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Write Operations', function () {
|
|
it('creates nested directories automatically', function () {
|
|
$path = 'deep/nested/structure/file.txt';
|
|
$content = 'nested content';
|
|
|
|
$this->cachedStorage->put($path, $content);
|
|
|
|
expect(file_exists($this->testDir . '/' . $path))->toBeTrue();
|
|
expect(file_get_contents($this->testDir . '/' . $path))->toBe($content);
|
|
});
|
|
|
|
it('overwrites existing files', function () {
|
|
$path = 'overwrite.txt';
|
|
|
|
$this->cachedStorage->put($path, 'original');
|
|
expect($this->cachedStorage->get($path))->toBe('original');
|
|
|
|
$this->cachedStorage->put($path, 'updated');
|
|
expect($this->cachedStorage->get($path))->toBe('updated');
|
|
});
|
|
|
|
it('handles empty content', function () {
|
|
$path = 'empty.txt';
|
|
|
|
$this->cachedStorage->put($path, '');
|
|
|
|
expect($this->cachedStorage->exists($path))->toBeTrue();
|
|
expect($this->cachedStorage->get($path))->toBe('');
|
|
});
|
|
});
|
|
|
|
describe('Copy Operations', function () {
|
|
it('copies files with directory cache optimization', function () {
|
|
$source = 'source.txt';
|
|
$destination = 'subdir/destination.txt';
|
|
|
|
// Create source file
|
|
$this->cachedStorage->put($source, 'copy this');
|
|
|
|
// Copy to nested directory (should cache destination directory)
|
|
$this->cachedStorage->copy($source, $destination);
|
|
|
|
expect($this->cachedStorage->exists($destination))->toBeTrue();
|
|
expect($this->cachedStorage->get($destination))->toBe('copy this');
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
expect($stats['cached_directories'])->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('throws exception when copying non-existent file', function () {
|
|
expect(fn() => $this->cachedStorage->copy('nonexistent.txt', 'destination.txt'))
|
|
->toThrow(FileNotFoundException::class);
|
|
});
|
|
});
|
|
|
|
|
|
describe('Delete Operations', function () {
|
|
it('deletes files', function () {
|
|
$path = 'delete-me.txt';
|
|
|
|
$this->cachedStorage->put($path, 'content');
|
|
expect($this->cachedStorage->exists($path))->toBeTrue();
|
|
|
|
$this->cachedStorage->delete($path);
|
|
expect($this->cachedStorage->exists($path))->toBeFalse();
|
|
});
|
|
|
|
it('throws exception when deleting non-existent file', function () {
|
|
expect(fn() => $this->cachedStorage->delete('nonexistent.txt'))
|
|
->toThrow(FileNotFoundException::class);
|
|
});
|
|
|
|
it('keeps directory in cache after file deletion', function () {
|
|
$path = 'subdir/file.txt';
|
|
|
|
$this->cachedStorage->put($path, 'content');
|
|
|
|
$statsBefore = $this->cachedStorage->getCacheStats();
|
|
$cacheSizeBefore = $statsBefore['cached_directories'];
|
|
|
|
$this->cachedStorage->delete($path);
|
|
|
|
$statsAfter = $this->cachedStorage->getCacheStats();
|
|
$cacheSizeAfter = $statsAfter['cached_directories'];
|
|
|
|
// Directory should still be cached (still exists, just empty)
|
|
expect($cacheSizeAfter)->toBe($cacheSizeBefore);
|
|
});
|
|
});
|
|
|
|
describe('Metadata Operations', function () {
|
|
it('gets file size', function () {
|
|
$path = 'sized.txt';
|
|
$content = 'test content with size';
|
|
|
|
$this->cachedStorage->put($path, $content);
|
|
|
|
$size = $this->cachedStorage->size($path);
|
|
|
|
expect($size)->toBe(strlen($content));
|
|
});
|
|
|
|
it('gets last modified timestamp', function () {
|
|
$path = 'timestamped.txt';
|
|
|
|
$this->cachedStorage->put($path, 'content');
|
|
|
|
$lastModified = $this->cachedStorage->lastModified($path);
|
|
|
|
expect($lastModified)->toBeInt();
|
|
expect($lastModified)->toBeLessThanOrEqual(time());
|
|
});
|
|
|
|
it('gets MIME type', function () {
|
|
$path = 'file.txt';
|
|
|
|
$this->cachedStorage->put($path, 'text content');
|
|
|
|
$mimeType = $this->cachedStorage->getMimeType($path);
|
|
|
|
expect($mimeType)->toContain('text');
|
|
});
|
|
|
|
});
|
|
|
|
describe('Batch Operations', function () {
|
|
it('puts multiple files with directory cache optimization', function () {
|
|
$files = [
|
|
'batch1/file1.txt' => 'content1',
|
|
'batch1/file2.txt' => 'content2',
|
|
'batch2/file3.txt' => 'content3',
|
|
];
|
|
|
|
$this->cachedStorage->putMultiple($files);
|
|
|
|
foreach ($files as $path => $content) {
|
|
expect($this->cachedStorage->exists($path))->toBeTrue();
|
|
expect($this->cachedStorage->get($path))->toBe($content);
|
|
}
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
expect($stats['cached_directories'])->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('gets multiple files', function () {
|
|
$files = [
|
|
'multi1.txt' => 'content1',
|
|
'multi2.txt' => 'content2',
|
|
'multi3.txt' => 'content3',
|
|
];
|
|
|
|
// Write files
|
|
$this->cachedStorage->putMultiple($files);
|
|
|
|
// Read in batch
|
|
$results = $this->cachedStorage->getMultiple(array_keys($files));
|
|
|
|
expect($results)->toBe($files);
|
|
});
|
|
|
|
it('gets metadata for multiple files', function () {
|
|
$files = [
|
|
'meta1.txt' => 'content1',
|
|
'meta2.txt' => 'content2',
|
|
];
|
|
|
|
$this->cachedStorage->putMultiple($files);
|
|
|
|
$metadata = $this->cachedStorage->getMetadataMultiple(array_keys($files));
|
|
|
|
expect($metadata)->toHaveCount(2);
|
|
expect($metadata['meta1.txt']->size)->toBe(strlen('content1'));
|
|
expect($metadata['meta2.txt']->size)->toBe(strlen('content2'));
|
|
});
|
|
});
|
|
|
|
describe('Directory Listing', function () {
|
|
it('lists directory contents', function () {
|
|
$dir = 'listable';
|
|
|
|
$this->cachedStorage->put($dir . '/file1.txt', 'content1');
|
|
$this->cachedStorage->put($dir . '/file2.txt', 'content2');
|
|
$this->cachedStorage->put($dir . '/file3.txt', 'content3');
|
|
|
|
$contents = $this->cachedStorage->listDirectory($dir);
|
|
|
|
// listDirectory returns array with numeric keys and relative path values (including directory prefix)
|
|
expect($contents)->toHaveCount(3);
|
|
|
|
// Convert to simple array values for comparison
|
|
$files = array_values($contents);
|
|
expect($files)->toContain('listable/file1.txt');
|
|
expect($files)->toContain('listable/file2.txt');
|
|
expect($files)->toContain('listable/file3.txt');
|
|
});
|
|
});
|
|
|
|
describe('Cache Management', function () {
|
|
it('can clear directory cache', function () {
|
|
$this->cachedStorage->put('cached/file.txt', 'content');
|
|
|
|
$statsBefore = $this->cachedStorage->getCacheStats();
|
|
expect($statsBefore['cached_directories'])->toBeGreaterThan(0);
|
|
|
|
$this->cachedStorage->clearDirectoryCache();
|
|
|
|
$statsAfter = $this->cachedStorage->getCacheStats();
|
|
expect($statsAfter['cached_directories'])->toBe(0);
|
|
});
|
|
|
|
it('provides accurate cache statistics', function () {
|
|
$this->cachedStorage->put('level1/level2/file.txt', 'content');
|
|
|
|
$stats = $this->cachedStorage->getCacheStats();
|
|
|
|
expect($stats)->toHaveKeys(['cached_directories', 'cache_entries']);
|
|
expect($stats['cached_directories'])->toBeGreaterThan(0);
|
|
expect($stats['cache_entries'])->toBeArray();
|
|
});
|
|
|
|
it('checks if directory is cached', function () {
|
|
// Use relative path that will be resolved
|
|
$relativePath = 'cached_check/file.txt';
|
|
|
|
// Get the directory that will actually be cached
|
|
$cachedDir = $this->testDir . '/cached_check';
|
|
|
|
expect($this->cachedStorage->isDirectoryCached($cachedDir))->toBeFalse();
|
|
|
|
$this->cachedStorage->put($relativePath, 'content');
|
|
|
|
// After put, the directory should be cached
|
|
expect($this->cachedStorage->isDirectoryCached($cachedDir))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Performance Characteristics', function () {
|
|
it('reduces directory checks for repeated writes', function () {
|
|
$dir = 'performance/test';
|
|
|
|
// First write creates and caches directory
|
|
$this->cachedStorage->put($dir . '/file1.txt', 'content1');
|
|
|
|
$statsBefore = $this->cachedStorage->getCacheStats();
|
|
|
|
// Subsequent writes reuse cached directory
|
|
for ($i = 2; $i <= 10; $i++) {
|
|
$this->cachedStorage->put($dir . "/file{$i}.txt", "content{$i}");
|
|
}
|
|
|
|
$statsAfter = $this->cachedStorage->getCacheStats();
|
|
|
|
// Cache size should not grow significantly (same directory)
|
|
expect($statsAfter['cached_directories'])->toBeLessThanOrEqual(
|
|
$statsBefore['cached_directories'] + 1
|
|
);
|
|
});
|
|
});
|
|
});
|