| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- <?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;
- /**
- * Covers SPEC §M07.2: policy CRUD + preview, RBAC split (Viewer reads,
- * Admin writes), threshold replacement on PATCH, 409 on delete with
- * referencing consumers.
- */
- final class PoliciesControllerTest extends AppTestCase
- {
- public function testViewerCanListSeededPolicies(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
- $response = $this->request('GET', '/api/v1/admin/policies', [
- 'Authorization' => 'Bearer ' . $token,
- ]);
- self::assertSame(200, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertSame(3, $body['total']); // strict, moderate, paranoid
- $names = array_map(static fn (array $p): string => $p['name'], $body['items']);
- self::assertContains('strict', $names);
- self::assertContains('moderate', $names);
- self::assertContains('paranoid', $names);
- }
- public function testShowIncludesThresholdsKeyedBySlug(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
- $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);
- $response = $this->request('GET', "/api/v1/admin/policies/{$policyId}", [
- 'Authorization' => 'Bearer ' . $token,
- ]);
- self::assertSame(200, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertSame('moderate', $body['name']);
- self::assertNotEmpty($body['thresholds']);
- $slugs = array_map(static fn (array $t): string => $t['category_slug'], $body['thresholds']);
- self::assertContains('brute_force', $slugs);
- }
- public function testViewerCannotCreate(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
- $response = $this->request(
- 'POST',
- '/api/v1/admin/policies',
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode([
- 'name' => 'aggressive',
- 'thresholds' => ['brute_force' => 0.5],
- ]) ?: null,
- );
- self::assertSame(403, $response->getStatusCode());
- }
- public function testAdminCanCreatePolicyWithThresholds(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $response = $this->request(
- 'POST',
- '/api/v1/admin/policies',
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode([
- 'name' => 'aggressive',
- 'description' => 'block everything',
- 'include_manual_blocks' => true,
- 'thresholds' => [
- 'brute_force' => 0.1,
- 'spam' => 0.2,
- ],
- ]) ?: null,
- );
- self::assertSame(201, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertSame('aggressive', $body['name']);
- self::assertCount(2, $body['thresholds']);
- }
- public function testCreateRejectsDuplicateName(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $response = $this->request(
- 'POST',
- '/api/v1/admin/policies',
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode(['name' => 'moderate', 'thresholds' => ['brute_force' => 1.0]]) ?: null,
- );
- self::assertSame(400, $response->getStatusCode());
- $details = $this->decode($response)['details'];
- self::assertArrayHasKey('name', $details);
- }
- public function testCreateRejectsUnknownCategorySlug(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $response = $this->request(
- 'POST',
- '/api/v1/admin/policies',
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode(['name' => 'bogus', 'thresholds' => ['unknown_slug' => 1.0]]) ?: null,
- );
- self::assertSame(400, $response->getStatusCode());
- $details = $this->decode($response)['details'];
- self::assertStringContainsString('unknown_slug', (string) $details['thresholds']);
- }
- public function testPatchReplacesThresholdsWholesale(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);
- $response = $this->request(
- 'PATCH',
- "/api/v1/admin/policies/{$policyId}",
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode(['thresholds' => ['brute_force' => 5.0]]) ?: null,
- );
- self::assertSame(200, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertCount(1, $body['thresholds']);
- self::assertSame('brute_force', $body['thresholds'][0]['category_slug']);
- self::assertEqualsWithDelta(5.0, $body['thresholds'][0]['threshold'], 0.0001);
- }
- public function testDeleteWithReferencingConsumerReturns409(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);
- $this->db->insert('consumers', [
- 'name' => 'fw-edge',
- 'policy_id' => $policyId,
- 'is_active' => 1,
- ]);
- $response = $this->request('DELETE', "/api/v1/admin/policies/{$policyId}", [
- 'Authorization' => 'Bearer ' . $token,
- ]);
- self::assertSame(409, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertSame('policy_in_use', $body['error']);
- self::assertNotEmpty($body['consumers']);
- }
- public function testDeleteSucceedsWithoutReferences(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
- $createResp = $this->request(
- 'POST',
- '/api/v1/admin/policies',
- ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
- json_encode(['name' => 'tmp', 'thresholds' => ['brute_force' => 1.0]]) ?: null,
- );
- $newId = (int) $this->decode($createResp)['id'];
- $deleteResp = $this->request('DELETE', "/api/v1/admin/policies/{$newId}", [
- 'Authorization' => 'Bearer ' . $token,
- ]);
- self::assertSame(204, $deleteResp->getStatusCode());
- }
- public function testPreviewReturnsCountSampleAndPolicyName(): void
- {
- $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
- $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'paranoid']);
- $response = $this->request('GET', "/api/v1/admin/policies/{$policyId}/preview", [
- 'Authorization' => 'Bearer ' . $token,
- ]);
- self::assertSame(200, $response->getStatusCode());
- $body = $this->decode($response);
- self::assertArrayHasKey('count', $body);
- self::assertArrayHasKey('sample', $body);
- self::assertSame('paranoid', $body['policy']);
- self::assertIsArray($body['sample']);
- }
- }
|