createReporter('rep-audit'); $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId); $resp = $this->request( 'POST', '/api/v1/report', ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'], (string) json_encode(['ip' => '203.0.113.42', 'category' => 'brute_force', 'metadata' => ['ua' => 'curl']]), ); self::assertSame(202, $resp->getStatusCode()); $row = $this->db->fetchAssociative( "SELECT actor_kind, actor_id, action, target_type, target_label, details_json FROM audit_log WHERE action = 'report.received' ORDER BY id DESC LIMIT 1" ); self::assertIsArray($row); self::assertSame('reporter', $row['actor_kind']); self::assertSame((string) $reporterId, $row['actor_id']); self::assertSame('report', $row['target_type']); self::assertSame('203.0.113.42', $row['target_label']); $details = json_decode((string) $row['details_json'], true); self::assertIsArray($details); self::assertSame('203.0.113.42', $details['ip']); self::assertSame('brute_force', $details['category']); self::assertSame($reporterId, $details['reporter_id']); self::assertSame('rep-audit', $details['reporter_name']); self::assertTrue($details['has_metadata']); } public function testReportReceivedSuppressedWhenToggleDisabled(): void { /** @var AppSettings $settings */ $settings = $this->container->get(AppSettings::class); $settings->setBool(AppSettings::KEY_AUDIT_REPORT_RECEIVED_ENABLED, false); $reporterId = $this->createReporter('rep-quiet'); $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId); $resp = $this->request( 'POST', '/api/v1/report', ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'], (string) json_encode(['ip' => '198.51.100.7', 'category' => 'scanner']), ); self::assertSame(202, $resp->getStatusCode()); $count = (int) $this->db->fetchOne( "SELECT COUNT(*) FROM audit_log WHERE action = 'report.received'" ); self::assertSame(0, $count); } public function testBlocklistRequestedEmitsAuditWithConsumerActor(): void { $token = $this->setupConsumerToken('moderate', 'fw-audit'); $resp = $this->request('GET', '/api/v1/blocklist', [ 'Authorization' => 'Bearer ' . $token, ]); self::assertSame(200, $resp->getStatusCode()); $row = $this->db->fetchAssociative( "SELECT actor_kind, actor_id, action, target_type, target_label, details_json FROM audit_log WHERE action = 'blocklist.requested' ORDER BY id DESC LIMIT 1" ); self::assertIsArray($row); self::assertSame('consumer', $row['actor_kind']); self::assertNotNull($row['actor_id']); self::assertSame('blocklist', $row['target_type']); self::assertSame('moderate', $row['target_label']); $details = json_decode((string) $row['details_json'], true); self::assertIsArray($details); self::assertSame('moderate', $details['policy_name']); self::assertSame('text', $details['format']); self::assertSame(200, $details['status']); self::assertArrayHasKey('entries', $details); } public function testBlocklist304StillEmitsAuditWithStatus304(): void { $token = $this->setupConsumerToken('moderate', 'fw-etag'); $first = $this->request('GET', '/api/v1/blocklist', [ 'Authorization' => 'Bearer ' . $token, ]); $etag = $first->getHeaderLine('ETag'); $second = $this->request('GET', '/api/v1/blocklist', [ 'Authorization' => 'Bearer ' . $token, 'If-None-Match' => $etag, ]); self::assertSame(304, $second->getStatusCode()); $statuses = $this->db->fetchAllAssociative( "SELECT details_json FROM audit_log WHERE action = 'blocklist.requested' ORDER BY id ASC" ); self::assertCount(2, $statuses); $second = json_decode((string) $statuses[1]['details_json'], true); self::assertIsArray($second); self::assertSame(304, $second['status']); } public function testBlocklistRequestedSuppressedWhenToggleDisabled(): void { /** @var AppSettings $settings */ $settings = $this->container->get(AppSettings::class); $settings->setBool(AppSettings::KEY_AUDIT_BLOCKLIST_REQUEST_ENABLED, false); $token = $this->setupConsumerToken('moderate', 'fw-quiet'); $resp = $this->request('GET', '/api/v1/blocklist', [ 'Authorization' => 'Bearer ' . $token, ]); self::assertSame(200, $resp->getStatusCode()); $count = (int) $this->db->fetchOne( "SELECT COUNT(*) FROM audit_log WHERE action = 'blocklist.requested'" ); self::assertSame(0, $count); } public function testFailedReportDoesNotEmitAudit(): void { $reporterId = $this->createReporter('rep-bad'); $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId); $resp = $this->request( 'POST', '/api/v1/report', ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'], (string) json_encode(['ip' => 'not-an-ip', 'category' => 'spam']), ); self::assertSame(400, $resp->getStatusCode()); $count = (int) $this->db->fetchOne( "SELECT COUNT(*) FROM audit_log WHERE action = 'report.received'" ); self::assertSame(0, $count); } private function setupConsumerToken(string $policyName, string $consumerName): string { $policyId = (int) $this->db->fetchOne( 'SELECT id FROM policies WHERE name = :n', ['n' => $policyName], ); $this->db->insert('consumers', [ 'name' => $consumerName, 'policy_id' => $policyId, 'is_active' => 1, ]); $consumerId = (int) $this->db->lastInsertId(); return $this->createToken(TokenKind::Consumer, consumerId: $consumerId); } }