1
0

AllowlistControllerTest.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Admin;
  4. use App\Domain\Auth\Role;
  5. use App\Domain\Auth\TokenKind;
  6. use App\Domain\Ip\IpAddress;
  7. use App\Infrastructure\Reputation\CidrEvaluatorFactory;
  8. use App\Tests\Integration\Support\AppTestCase;
  9. use Monolog\Handler\TestHandler;
  10. use Monolog\Level;
  11. use Monolog\Logger;
  12. use Psr\Log\LoggerInterface;
  13. /**
  14. * Covers SPEC §6 allowlist CRUD plus the allowlist-precedence warning
  15. * logged when an IP appears on both lists. Mirrors the manual-blocks
  16. * suite minus `expires_at`.
  17. */
  18. final class AllowlistControllerTest extends AppTestCase
  19. {
  20. public function testOperatorCanCreateIpAllowlistEntry(): void
  21. {
  22. $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
  23. $response = $this->request(
  24. 'POST',
  25. '/api/v1/admin/allowlist',
  26. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  27. json_encode(['kind' => 'ip', 'ip' => '198.51.100.5', 'reason' => 'monitor']) ?: null,
  28. );
  29. self::assertSame(201, $response->getStatusCode());
  30. $body = $this->decode($response);
  31. self::assertSame('198.51.100.5', $body['ip']);
  32. }
  33. public function testOperatorCanCreateSubnetAllowlistEntry(): void
  34. {
  35. $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
  36. $response = $this->request(
  37. 'POST',
  38. '/api/v1/admin/allowlist',
  39. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  40. json_encode(['kind' => 'subnet', 'cidr' => '10.0.0.0/8', 'reason' => 'private']) ?: null,
  41. );
  42. self::assertSame(201, $response->getStatusCode());
  43. $body = $this->decode($response);
  44. self::assertSame('10.0.0.0/8', $body['cidr']);
  45. self::assertSame(8, $body['prefix_length']);
  46. }
  47. public function testViewerCannotCreate(): void
  48. {
  49. $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
  50. $response = $this->request(
  51. 'POST',
  52. '/api/v1/admin/allowlist',
  53. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  54. json_encode(['kind' => 'ip', 'ip' => '1.2.3.4']) ?: null,
  55. );
  56. self::assertSame(403, $response->getStatusCode());
  57. }
  58. public function testAllowlistedIpIsAllowlistedRegardlessOfManualBlock(): void
  59. {
  60. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  61. // Same IP on both lists — log should warn; evaluator reports both
  62. // memberships independently; SPEC precedence is the
  63. // EffectiveStatusService's job.
  64. $this->request(
  65. 'POST',
  66. '/api/v1/admin/manual-blocks',
  67. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  68. json_encode(['kind' => 'ip', 'ip' => '198.51.100.5']) ?: null,
  69. );
  70. $this->request(
  71. 'POST',
  72. '/api/v1/admin/allowlist',
  73. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  74. json_encode(['kind' => 'ip', 'ip' => '198.51.100.5']) ?: null,
  75. );
  76. // Inject a TestHandler-backed logger before the evaluator builds.
  77. $logger = new Logger('test');
  78. $logger->pushHandler($handler = new TestHandler(Level::Warning));
  79. /** @var \DI\Container $container */
  80. $container = $this->container;
  81. $container->set(LoggerInterface::class, $logger);
  82. // Rebuild the factory with the new logger so the warning is captured.
  83. $container->set(CidrEvaluatorFactory::class, new CidrEvaluatorFactory(
  84. $container->get(\App\Infrastructure\ManualBlock\ManualBlockRepository::class),
  85. $container->get(\App\Infrastructure\Allowlist\AllowlistRepository::class),
  86. $container->get(\App\Domain\Time\Clock::class),
  87. $logger,
  88. 0,
  89. ));
  90. /** @var CidrEvaluatorFactory $factory */
  91. $factory = $this->container->get(CidrEvaluatorFactory::class);
  92. $evaluator = $factory->get();
  93. $ip = IpAddress::fromString('198.51.100.5');
  94. self::assertTrue($evaluator->isAllowlisted($ip));
  95. self::assertTrue($evaluator->isManuallyBlocked($ip));
  96. $found = false;
  97. foreach ($handler->getRecords() as $record) {
  98. if (str_contains($record->message, 'allowlist takes precedence')) {
  99. $found = true;
  100. break;
  101. }
  102. }
  103. self::assertTrue($found, 'expected overlap warning to be logged');
  104. }
  105. public function testDeleteInvalidatesEvaluator(): void
  106. {
  107. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  108. $created = $this->request(
  109. 'POST',
  110. '/api/v1/admin/allowlist',
  111. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  112. json_encode(['kind' => 'subnet', 'cidr' => '10.0.0.0/8']) ?: null,
  113. );
  114. $id = (int) $this->decode($created)['id'];
  115. /** @var CidrEvaluatorFactory $factory */
  116. $factory = $this->container->get(CidrEvaluatorFactory::class);
  117. self::assertTrue($factory->get()->isAllowlisted(IpAddress::fromString('10.0.0.1')));
  118. $delete = $this->request(
  119. 'DELETE',
  120. "/api/v1/admin/allowlist/{$id}",
  121. ['Authorization' => 'Bearer ' . $token],
  122. );
  123. self::assertSame(204, $delete->getStatusCode());
  124. self::assertFalse($factory->get()->isAllowlisted(IpAddress::fromString('10.0.0.1')));
  125. }
  126. }