Files
michaelschiemer/tests/Unit/Framework/Logging/Handlers/MultiFileHandlerTest.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

362 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Logging\Handlers;
use App\Framework\Core\PathProvider;
use App\Framework\Logging\Handlers\MultiFileHandler;
use App\Framework\Logging\LogConfig;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\ValueObjects\LogContext;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
// Test-spezifische LogConfig Implementation
final class TestLogConfig
{
public function __construct(private string $testDir)
{
}
public function getLogPath(string $type): string
{
return match ($type) {
'security' => $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);
}
}