| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Audit;
- use App\Domain\Auth\TokenKind;
- use App\Domain\Settings\AppSettings;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * SPEC §M12 covers admin-side audit emission. C20 adds two public-endpoint
- * audit entries — `report.received` and `blocklist.requested` — gated by
- * runtime feature flags so the high-volume rows can be silenced without
- * restarting the api.
- */
- final class PublicEndpointAuditTest extends AppTestCase
- {
- public function testReportReceivedEmitsAuditWithReporterActor(): void
- {
- $reporterId = $this->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 testReportSuppressedWhenReporterAuditDisabled(): void
- {
- $reporterId = $this->createReporter('rep-silent');
- $this->db->update('reporters', ['audit_enabled' => 0], ['id' => $reporterId]);
- $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.99', 'category' => 'spam']),
- );
- 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 testBlocklistSuppressedWhenConsumerAuditDisabled(): void
- {
- $token = $this->setupConsumerToken('moderate', 'fw-silent');
- $this->db->update('consumers', ['audit_enabled' => 0], ['name' => 'fw-silent']);
- $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);
- }
- }
|