actor_kind=user; * raw admin token -> actor_kind=admin-token. */ final class AuditEmissionTest extends AppTestCase { public function testManualBlockCreateEmitsAuditViaAdminToken(): void { $token = $this->createToken(TokenKind::Admin, Role::Admin); $resp = $this->request( 'POST', '/api/v1/admin/manual-blocks', [ 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json', ], (string) json_encode(['kind' => 'ip', 'ip' => '203.0.113.42', 'reason' => 'admin-token-test']), ); self::assertSame(201, $resp->getStatusCode()); $row = $this->db->fetchAssociative( "SELECT actor_kind, actor_id, action, target_type, details_json FROM audit_log WHERE action = 'manual_block.created' ORDER BY id DESC LIMIT 1" ); self::assertIsArray($row); self::assertSame('admin-token', $row['actor_kind']); self::assertSame('manual_block', $row['target_type']); $details = json_decode((string) $row['details_json'], true); self::assertIsArray($details); self::assertSame('203.0.113.42', $details['ip']); self::assertSame('admin-token-test', $details['reason']); } public function testManualBlockCreateEmitsAuditAttributedToImpersonatedUser(): void { $userId = $this->createUser(Role::Admin, isLocal: true); $service = $this->createToken(TokenKind::Service); $resp = $this->request( 'POST', '/api/v1/admin/manual-blocks', [ 'Authorization' => 'Bearer ' . $service, 'X-Acting-User-Id' => (string) $userId, 'Content-Type' => 'application/json', ], (string) json_encode(['kind' => 'ip', 'ip' => '203.0.113.43', 'reason' => 'via-ui']), ); self::assertSame(201, $resp->getStatusCode()); $row = $this->db->fetchAssociative( "SELECT actor_kind, actor_id FROM audit_log WHERE action = 'manual_block.created' ORDER BY id DESC LIMIT 1" ); self::assertIsArray($row); self::assertSame('user', $row['actor_kind']); self::assertSame((string) $userId, $row['actor_id']); } public function testTokenCreateNeverPutsRawTokenInAudit(): void { $admin = $this->createToken(TokenKind::Admin, Role::Admin); $reporterId = $this->createReporter('rep-audit'); $resp = $this->request( 'POST', '/api/v1/admin/tokens', ['Authorization' => 'Bearer ' . $admin, 'Content-Type' => 'application/json'], (string) json_encode(['kind' => 'reporter', 'reporter_id' => $reporterId]), ); self::assertSame(201, $resp->getStatusCode()); $body = $this->decode($resp); $rawToken = (string) $body['raw_token']; $row = $this->db->fetchAssociative( "SELECT details_json FROM audit_log WHERE action = 'token.created' ORDER BY id DESC LIMIT 1" ); self::assertIsArray($row); $json = (string) $row['details_json']; self::assertStringNotContainsString($rawToken, $json); // Prefix is allowed. $details = json_decode($json, true); self::assertIsArray($details); self::assertArrayHasKey('prefix', $details); self::assertArrayHasKey('kind', $details); } public function testFailedValidationDoesNotEmitAudit(): void { $admin = $this->createToken(TokenKind::Admin, Role::Admin); $resp = $this->request( 'POST', '/api/v1/admin/manual-blocks', ['Authorization' => 'Bearer ' . $admin, 'Content-Type' => 'application/json'], (string) json_encode(['kind' => 'ip', 'ip' => 'not-an-ip']), ); self::assertSame(400, $resp->getStatusCode()); $count = (int) $this->db->fetchOne("SELECT COUNT(*) FROM audit_log WHERE action = 'manual_block.created'"); self::assertSame(0, $count); } public function testCategoryCreateUpdateDeleteAudit(): void { $admin = $this->createToken(TokenKind::Admin, Role::Admin); $resp = $this->request( 'POST', '/api/v1/admin/categories', ['Authorization' => 'Bearer ' . $admin, 'Content-Type' => 'application/json'], (string) json_encode([ 'slug' => 'audit_test', 'name' => 'Audit Test', 'decay_function' => 'linear', 'decay_param' => 30.0, ]), ); self::assertSame(201, $resp->getStatusCode()); $created = $this->decode($resp); $id = (int) $created['id']; $resp = $this->request( 'PATCH', '/api/v1/admin/categories/' . $id, ['Authorization' => 'Bearer ' . $admin, 'Content-Type' => 'application/json'], (string) json_encode(['name' => 'Audit Test Renamed']), ); self::assertSame(200, $resp->getStatusCode()); $resp = $this->request( 'DELETE', '/api/v1/admin/categories/' . $id, ['Authorization' => 'Bearer ' . $admin], ); self::assertSame(204, $resp->getStatusCode()); $rows = $this->db->fetchAllAssociative( "SELECT action FROM audit_log WHERE target_type = 'category' AND target_id = ? ORDER BY id ASC", [(string) $id], ); $actions = array_map(static fn ($r) => $r['action'], $rows); self::assertSame(['category.created', 'category.updated', 'category.deleted'], $actions); } }