| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Audit;
- use App\Domain\Auth\Role;
- use App\Domain\Auth\TokenKind;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * Confirms that successful state-changing admin endpoints emit exactly
- * one row in `audit_log` and that the actor attribution honours the
- * SPEC §8 invariant: service-token + impersonation -> 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);
- }
- }
|