dir = sys_get_temp_dir() . '/bicycle_log_' . uniqid(); Log::$directory = $this->dir; Log::$threshold = Log::DEBUG; } protected function tearDown(): void { if (is_dir($this->dir)) { foreach (glob($this->dir . '/*') as $file) { unlink($file); } rmdir($this->dir); } Log::$directory = null; Log::$threshold = null; } private function logFile(string $channel): string { return $this->dir . '/' . $channel . '-' . date('Y-m-d') . '.log'; } public function testWriteCreatesFileWithFormattedLine(): void { Log::info('Привет'); $this->assertFileExists($this->logFile('action')); $content = file_get_contents($this->logFile('action')); $this->assertMatchesRegularExpression('/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] INFO: Привет\n/', $content); } public function testChannelsSplitErrorsFromActions(): void { Log::info('действие'); Log::error('сбой'); // Действия — в action-файле, ошибки — в error-файле, не смешаны. $action = file_get_contents($this->logFile('action')); $error = file_get_contents($this->logFile('error')); $this->assertStringContainsString('INFO: действие', $action); $this->assertStringNotContainsString('сбой', $action); $this->assertStringContainsString('ERROR: сбой', $error); $this->assertStringNotContainsString('действие', $error); } public function testWarningGoesToErrorChannel(): void { Log::warning('предупреждение'); $this->assertFileDoesNotExist($this->logFile('action')); $this->assertStringContainsString('WARNING: предупреждение', file_get_contents($this->logFile('error'))); } public function testThresholdFiltersLowerLevels(): void { Log::$threshold = Log::WARNING; Log::debug('низкий'); Log::info('низкий'); $this->assertFileDoesNotExist($this->logFile('action')); Log::warning('высокий'); Log::error('высокий'); $content = file_get_contents($this->logFile('error')); $this->assertStringContainsString('WARNING: высокий', $content); $this->assertStringContainsString('ERROR: высокий', $content); } public function testShortcutsUseCorrectLevelLabel(): void { Log::debug('d'); Log::info('i'); Log::warning('w'); Log::error('e'); $action = file_get_contents($this->logFile('action')); $error = file_get_contents($this->logFile('error')); $this->assertStringContainsString('DEBUG: d', $action); $this->assertStringContainsString('INFO: i', $action); $this->assertStringContainsString('WARNING: w', $error); $this->assertStringContainsString('ERROR: e', $error); } public function testStrtrSubstitution(): void { Log::info('Пользователь :id вошёл', [':id' => 42]); $this->assertStringContainsString('Пользователь 42 вошёл', file_get_contents($this->logFile('action'))); } public function testAppendsWithoutOverwriting(): void { Log::info('первая'); Log::info('вторая'); $content = file_get_contents($this->logFile('action')); $this->assertStringContainsString('первая', $content); $this->assertStringContainsString('вторая', $content); $this->assertSame(2, substr_count($content, "\n")); } public function testRequestInfoEmptyWithoutCurrentRequest(): void { // Request::$current не установлен в юнит-окружении. $this->assertSame('', Log::requestInfo()); } public function testMaskHidesSensitiveKeys(): void { $method = new \ReflectionMethod(Log::class, 'mask'); $method->setAccessible(true); $result = $method->invoke(null, [ 'email' => 'a@b.com', 'password' => 'secret', 'csrf_token' => 'abc123', 'CSRF_TOKEN' => 'xyz', // регистр не важен ]); $this->assertSame('a@b.com', $result['email']); $this->assertSame('***', $result['password']); $this->assertSame('***', $result['csrf_token']); $this->assertSame('***', $result['CSRF_TOKEN']); } }