| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Admin;
- use App\Domain\Auth\Role;
- use App\Domain\Auth\TokenKind;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * `/api/v1/admin/audit-log` end-to-end. Seeds rows directly so the
- * filter coverage doesn't depend on emission timing.
- */
- final class AuditLogControllerTest extends AppTestCase
- {
- public function testListEmpty(): void
- {
- $token = $this->createToken(TokenKind::Admin, Role::Viewer);
- $resp = $this->request('GET', '/api/v1/admin/audit-log', ['Authorization' => 'Bearer ' . $token]);
- self::assertSame(200, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame([], $body['items']);
- self::assertSame(0, $body['total']);
- }
- public function testListWithFilters(): void
- {
- $now = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
- $this->seedAudit('user', '7', 'manual_block.created', 'manual_block', '1', '{"ip":"1.1.1.1"}', $now);
- $this->seedAudit('admin-token', '3', 'category.created', 'category', '2', '{"slug":"x"}', $now);
- $this->seedAudit('user', '7', 'allowlist.created', 'allowlist', '5', '{"ip":"2.2.2.2"}', $now);
- $token = $this->createToken(TokenKind::Admin, Role::Viewer);
- // Filter by actor_kind=user
- $resp = $this->request('GET', '/api/v1/admin/audit-log?actor_kind=user', ['Authorization' => 'Bearer ' . $token]);
- $body = $this->decode($resp);
- self::assertSame(2, $body['total']);
- foreach ($body['items'] as $item) {
- self::assertSame('user', $item['actor_kind']);
- }
- // Filter by action
- $resp = $this->request('GET', '/api/v1/admin/audit-log?action=category.created', ['Authorization' => 'Bearer ' . $token]);
- $body = $this->decode($resp);
- self::assertSame(1, $body['total']);
- self::assertSame('category.created', $body['items'][0]['action']);
- self::assertSame(['slug' => 'x'], $body['items'][0]['details']);
- // Filter by entity_type=manual_block
- $resp = $this->request('GET', '/api/v1/admin/audit-log?entity_type=manual_block', ['Authorization' => 'Bearer ' . $token]);
- $body = $this->decode($resp);
- self::assertSame(1, $body['total']);
- self::assertSame('manual_block', $body['items'][0]['entity_type']);
- }
- public function testSubjectFilterUnionsActorAndTarget(): void
- {
- $now = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
- // (1) admin updated reporter #5 — target=reporter, actor=user
- $this->seedAudit('user', '1', 'reporter.updated', 'reporter', '5', '{}', $now);
- // (2) reporter #5 emitted a report.received — actor=reporter, target=report
- $this->seedAudit('reporter', '5', 'report.received', 'report', '99', '{}', $now);
- // (3) different reporter — must NOT appear in subject_kind=reporter,subject_id=5
- $this->seedAudit('reporter', '6', 'report.received', 'report', '100', '{}', $now);
- // (4) unrelated row — must NOT appear
- $this->seedAudit('user', '1', 'manual_block.created', 'manual_block', '7', '{}', $now);
- $token = $this->createToken(TokenKind::Admin, Role::Viewer);
- $resp = $this->request(
- 'GET',
- '/api/v1/admin/audit-log?subject_kind=reporter&subject_id=5',
- ['Authorization' => 'Bearer ' . $token],
- );
- $body = $this->decode($resp);
- self::assertSame(2, $body['total']);
- $actions = array_map(static fn (array $r): string => $r['action'], $body['items']);
- self::assertContains('reporter.updated', $actions);
- self::assertContains('report.received', $actions);
- self::assertNotContains('manual_block.created', $actions);
- }
- public function testSubjectKindWithoutIdReturns400(): void
- {
- $token = $this->createToken(TokenKind::Admin, Role::Viewer);
- $resp = $this->request(
- 'GET',
- '/api/v1/admin/audit-log?subject_kind=reporter',
- ['Authorization' => 'Bearer ' . $token],
- );
- self::assertSame(400, $resp->getStatusCode());
- self::assertArrayHasKey('subject', $this->decode($resp)['details']);
- }
- public function testInvalidActorKindReturns400(): void
- {
- $token = $this->createToken(TokenKind::Admin, Role::Viewer);
- $resp = $this->request('GET', '/api/v1/admin/audit-log?actor_kind=potato', ['Authorization' => 'Bearer ' . $token]);
- self::assertSame(400, $resp->getStatusCode());
- }
- public function testRequiresViewer(): void
- {
- $resp = $this->request('GET', '/api/v1/admin/audit-log');
- self::assertSame(401, $resp->getStatusCode());
- }
- private function seedAudit(string $kind, ?string $actorId, string $action, string $type, string $id, string $details, string $when): void
- {
- $this->db->insert('audit_log', [
- 'actor_kind' => $kind,
- 'actor_id' => $actorId,
- 'action' => $action,
- 'target_type' => $type,
- 'target_id' => $id,
- 'details_json' => $details,
- 'ip_address' => null,
- 'created_at' => $when,
- ]);
- }
- }
|