logDir = $_ENV['QDB_API_LOG_DIR'] ?? (QDB_TEST_UPLOADS . '/logs/api'); if (!is_dir($this->logDir)) { mkdir($this->logDir, 0775, true); } $this->clearLogFiles(); unset($GLOBALS['qdb_api_log_ctx']); } protected function tearDown(): void { $this->clearLogFiles(); unset($GLOBALS['qdb_api_log_ctx']); } public function testClassifyActivity(): void { $this->assertNull(qdb_api_log_classify_activity('OPTIONS', 'web', 'users')); $this->assertSame('web_change', qdb_api_log_classify_activity('POST', 'web', 'users')); $this->assertSame('app_change', qdb_api_log_classify_activity('POST', '', 'app_questionnaires')); $this->assertSame('app_sync', qdb_api_log_classify_activity('GET', '', 'app_questionnaires')); $this->assertSame('web_export', qdb_api_log_classify_activity('GET', 'web', 'export')); $this->assertSame('web_change', qdb_api_log_classify_activity('POST', 'web', 'backup')); $this->assertTrue(qdb_api_log_is_website_download('backup')); } public function testRedactSensitiveFields(): void { $redacted = qdb_api_log_redact_value([ 'username' => 'coach1', 'password' => 'secret', 'encrypted' => str_repeat('x', 40), ]); $this->assertSame('coach1', $redacted['username']); $this->assertSame('[REDACTED]', $redacted['password']); $this->assertSame('[ENCRYPTED:40 chars]', $redacted['encrypted']); } public function testRedactQueryString(): void { $qs = qdb_api_log_redact_query_string('questionnaireID=qn1&token=abc&password=pw'); parse_str((string)$qs, $params); $this->assertSame('qn1', $params['questionnaireID'] ?? null); $this->assertSame('[REDACTED]', $params['token'] ?? null); $this->assertSame('[REDACTED]', $params['password'] ?? null); } public function testTokenHintMasksMiddle(): void { $hint = qdb_api_log_token_hint('0123456789abcdef'); $this->assertStringContainsString('…', $hint); $this->assertStringNotContainsString('56789', $hint); } public function testClientIpUsesForwardedFor(): void { $_SERVER['HTTP_X_FORWARDED_FOR'] = '203.0.113.50, 198.51.100.1'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $this->assertSame('203.0.113.50', qdb_api_log_client_ip()); unset($_SERVER['HTTP_X_FORWARDED_FOR']); } public function testBeginFinishWritesWebChangeEntry(): void { $_SERVER['HTTP_X_QDB_CLIENT'] = 'web'; $_SERVER['REQUEST_URI'] = '/api/users'; $_SERVER['QUERY_STRING'] = ''; qdb_test_set_request_body(json_encode(['username' => 'new_coach'], JSON_THROW_ON_ERROR)); http_response_code(200); qdb_api_log_begin('POST', 'users'); qdb_api_log_finish(); $date = date('Y-m-d'); $read = qdb_api_log_read_entries($date, 'web_change', 10); $entries = $read['entries']; $this->assertNotEmpty($entries); $last = $entries[0]; $this->assertSame('web_change', $last['activity']); $this->assertSame('POST', $last['method']); $this->assertSame('users', $last['route']); $changes = $last['changes'] ?? []; $fields = array_column($changes, 'field'); $this->assertContains('username', $fields); qdb_test_reset_http_state(); } private function clearLogFiles(): void { foreach (glob($this->logDir . '/api-*.log') ?: [] as $path) { @unlink($path); } } }