Files
michaelschiemer/tests/Unit/Framework/Filesystem/CachedFileStorageTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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