$this->testDir . '/security/security.log', 'cache' => $this->testDir . '/debug/cache.log', 'database' => $this->testDir . '/debug/database.log', 'framework' => $this->testDir . '/debug/framework.log', 'error' => $this->testDir . '/app/error.log', default => $this->testDir . '/app/app.log' }; } public function getAllLogPaths(): array { return [ 'security' => $this->getLogPath('security'), 'cache' => $this->getLogPath('cache'), 'database' => $this->getLogPath('database'), 'framework' => $this->getLogPath('framework'), 'error' => $this->getLogPath('error'), 'app' => $this->getLogPath('app'), ]; } public function getBaseLogPath(): string { return $this->testDir; } public function ensureLogDirectoriesExist(): void { // No-op for testing } } // Test-spezifische PathProvider Implementation final class TestPathProvider { public function resolvePath(string $path): string { return $path; // Return path as-is for testing } } final class MultiFileHandlerTest extends TestCase { private string $testLogDir; private TestLogConfig $mockLogConfig; private TestPathProvider $mockPathProvider; private MultiFileHandler $handler; protected function setUp(): void { // Temporäres Test-Verzeichnis erstellen $this->testLogDir = sys_get_temp_dir() . '/test_logs_' . uniqid(); mkdir($this->testLogDir, 0755, true); // Test-spezifische LogConfig mit direkten Pfaden $this->mockLogConfig = $this->createLogConfigWithTestPaths(); $this->mockPathProvider = new TestPathProvider(); $this->handler = new MultiFileHandler( $this->mockLogConfig, $this->mockPathProvider, LogLevel::DEBUG, '[{timestamp}] [{level_name}] [{channel}] {message}' ); } protected function tearDown(): void { // Cleanup: Test-Verzeichnis löschen if (is_dir($this->testLogDir)) { $this->deleteDirectory($this->testLogDir); } } private function deleteDirectory(string $dir): void { if (! is_dir($dir)) { return; } $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; is_dir($path) ? $this->deleteDirectory($path) : unlink($path); } rmdir($dir); } public function test_is_handling_respects_min_level(): void { $debugRecord = new LogRecord( message: 'Debug message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: new DateTimeImmutable() ); $errorRecord = new LogRecord( message: 'Error message', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: new DateTimeImmutable() ); expect($this->handler->isHandling($debugRecord))->toBeTrue(); expect($this->handler->isHandling($errorRecord))->toBeTrue(); // Handler mit höherem Min-Level $errorHandler = new MultiFileHandler( $this->mockLogConfig, $this->mockPathProvider, LogLevel::ERROR ); expect($errorHandler->isHandling($debugRecord))->toBeFalse(); expect($errorHandler->isHandling($errorRecord))->toBeTrue(); } public function test_handles_security_channel_logs(): void { $record = new LogRecord( message: 'Security alert', context: LogContext::withData(['ip' => '192.168.1.1']), level: LogLevel::WARNING, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'security' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/security/security.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [WARNING] [security] Security alert'); expect($content)->toContain('{"ip":"192.168.1.1"}'); } public function test_handles_cache_channel_logs(): void { $record = new LogRecord( message: 'Cache miss', context: LogContext::withData(['key' => 'user_123']), level: LogLevel::DEBUG, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'cache' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/debug/cache.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [DEBUG] [cache] Cache miss'); expect($content)->toContain('{"key":"user_123"}'); } public function test_handles_database_channel_logs(): void { $record = new LogRecord( message: 'Query failed', context: LogContext::withData(['query' => 'SELECT * FROM users']), level: LogLevel::ERROR, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'database' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/debug/database.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [ERROR] [database] Query failed'); expect($content)->toContain('{"query":"SELECT * FROM users"}'); } public function test_handles_framework_channel_logs(): void { $record = new LogRecord( message: 'Route registered', context: LogContext::withData(['route' => '/api/users']), level: LogLevel::INFO, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'framework' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/debug/framework.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [INFO] [framework] Route registered'); expect($content)->toContain('{"route":"/api/users"}'); } public function test_handles_error_channel_logs(): void { $record = new LogRecord( message: 'System failure', context: LogContext::withData(['component' => 'payment']), level: LogLevel::CRITICAL, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'error' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/app/error.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [CRITICAL] [error] System failure'); expect($content)->toContain('{"component":"payment"}'); } public function test_handles_app_channel_fallback(): void { $record = new LogRecord( message: 'Default message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'unknown_channel' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/app/app.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [INFO] [unknown_channel] Default message'); } public function test_handles_null_channel(): void { $record = new LogRecord( message: 'No channel message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: null ); $this->handler->handle($record); $logFile = $this->testLogDir . '/app/app.log'; expect(file_exists($logFile))->toBeTrue(); $content = file_get_contents($logFile); expect($content)->toContain('[2023-12-25 10:30:45] [INFO] [app] No channel message'); } public function test_creates_directories_automatically(): void { $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable(), channel: 'security' ); // Verzeichnis sollte nicht existieren expect(is_dir($this->testLogDir . '/security'))->toBeFalse(); $this->handler->handle($record); // Verzeichnis sollte automatisch erstellt worden sein expect(is_dir($this->testLogDir . '/security'))->toBeTrue(); expect(file_exists($this->testLogDir . '/security/security.log'))->toBeTrue(); } public function test_appends_to_existing_files(): void { $record1 = new LogRecord( message: 'First message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'security' ); $record2 = new LogRecord( message: 'Second message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: new DateTimeImmutable('2023-12-25 10:31:45'), channel: 'security' ); $this->handler->handle($record1); $this->handler->handle($record2); $logFile = $this->testLogDir . '/security/security.log'; $content = file_get_contents($logFile); expect($content)->toContain('First message'); expect($content)->toContain('Second message'); // Zwei Zeilen sollten vorhanden sein $lines = explode("\n", trim($content)); expect($lines)->toHaveCount(2); } public function test_handles_empty_context(): void { $record = new LogRecord( message: 'Message without context', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable('2023-12-25 10:30:45'), channel: 'security' ); $this->handler->handle($record); $logFile = $this->testLogDir . '/security/security.log'; $content = file_get_contents($logFile); // Sollte keine JSON-Context-Info enthalten expect($content)->toContain('Message without context'); expect($content)->not->toContain('{}'); expect($content)->not->toContain('[]'); } private function createLogConfigWithTestPaths(): TestLogConfig { return new TestLogConfig($this->testLogDir); } }