1
0

ReportControllerTest.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Public;
  4. use App\Domain\Auth\Role;
  5. use App\Domain\Auth\TokenKind;
  6. use App\Tests\Integration\Support\AppTestCase;
  7. final class ReportControllerTest extends AppTestCase
  8. {
  9. public function testValidReportInsertedAndScoreUpdated(): void
  10. {
  11. $reporterId = $this->createReporter('web-prod');
  12. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  13. $resp = $this->request(
  14. 'POST',
  15. '/api/v1/report',
  16. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  17. json_encode(['ip' => '203.0.113.42', 'category' => 'brute_force']) ?: null,
  18. );
  19. self::assertSame(202, $resp->getStatusCode());
  20. $body = $this->decode($resp);
  21. self::assertArrayHasKey('report_id', $body);
  22. self::assertSame('203.0.113.42', $body['ip']);
  23. self::assertArrayHasKey('received_at', $body);
  24. // ip_scores must have a row > 0.
  25. $score = $this->db->fetchOne(
  26. "SELECT score FROM ip_scores WHERE ip_text = '203.0.113.42'"
  27. );
  28. self::assertNotFalse($score);
  29. self::assertGreaterThan(0.0, (float) $score);
  30. }
  31. public function testManyReportsAccumulateMonotonically(): void
  32. {
  33. $reporterId = $this->createReporter('web-many');
  34. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  35. for ($i = 0; $i < 5; $i++) {
  36. $this->request(
  37. 'POST',
  38. '/api/v1/report',
  39. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  40. json_encode(['ip' => '198.51.100.7', 'category' => 'scanner']) ?: null,
  41. );
  42. }
  43. $count = (int) $this->db->fetchOne(
  44. "SELECT COUNT(*) FROM reports WHERE ip_text = '198.51.100.7'"
  45. );
  46. self::assertSame(5, $count);
  47. $score = (float) $this->db->fetchOne(
  48. "SELECT score FROM ip_scores WHERE ip_text = '198.51.100.7'"
  49. );
  50. // 5 fresh reports × weight 1.0 × decay~1.0 should be ~5.0.
  51. self::assertEqualsWithDelta(5.0, $score, 0.05);
  52. }
  53. public function testWrongKindTokenRejected(): void
  54. {
  55. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  56. $resp = $this->request(
  57. 'POST',
  58. '/api/v1/report',
  59. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  60. json_encode(['ip' => '1.2.3.4', 'category' => 'spam']) ?: null,
  61. );
  62. self::assertSame(401, $resp->getStatusCode());
  63. }
  64. public function testInvalidIpReturns400(): void
  65. {
  66. $reporterId = $this->createReporter('web-bad-ip');
  67. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  68. $resp = $this->request(
  69. 'POST',
  70. '/api/v1/report',
  71. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  72. json_encode(['ip' => 'not-an-ip', 'category' => 'spam']) ?: null,
  73. );
  74. self::assertSame(400, $resp->getStatusCode());
  75. $body = $this->decode($resp);
  76. self::assertSame('validation_failed', $body['error']);
  77. self::assertArrayHasKey('ip', $body['details']);
  78. }
  79. public function testUnknownCategoryReturns400(): void
  80. {
  81. $reporterId = $this->createReporter('web-bad-cat');
  82. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  83. $resp = $this->request(
  84. 'POST',
  85. '/api/v1/report',
  86. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  87. json_encode(['ip' => '1.2.3.4', 'category' => 'no-such']) ?: null,
  88. );
  89. self::assertSame(400, $resp->getStatusCode());
  90. self::assertArrayHasKey('category', $this->decode($resp)['details']);
  91. }
  92. public function testMetadataMustBeObject(): void
  93. {
  94. $reporterId = $this->createReporter('web-bad-meta');
  95. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  96. $resp = $this->request(
  97. 'POST',
  98. '/api/v1/report',
  99. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  100. json_encode(['ip' => '1.2.3.4', 'category' => 'spam', 'metadata' => [1, 2, 3]]) ?: null,
  101. );
  102. self::assertSame(400, $resp->getStatusCode());
  103. }
  104. public function testMetadataExceedingLimitRejected(): void
  105. {
  106. $reporterId = $this->createReporter('web-big-meta');
  107. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  108. $big = ['blob' => str_repeat('A', 5000)];
  109. $resp = $this->request(
  110. 'POST',
  111. '/api/v1/report',
  112. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  113. json_encode(['ip' => '1.2.3.4', 'category' => 'spam', 'metadata' => $big]) ?: null,
  114. );
  115. self::assertSame(400, $resp->getStatusCode());
  116. }
  117. public function testInactiveReporterTokenRejected(): void
  118. {
  119. $reporterId = $this->createReporter('web-disabled');
  120. $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);
  121. $this->db->update('reporters', ['is_active' => 0], ['id' => $reporterId]);
  122. $resp = $this->request(
  123. 'POST',
  124. '/api/v1/report',
  125. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  126. json_encode(['ip' => '1.2.3.4', 'category' => 'spam']) ?: null,
  127. );
  128. self::assertSame(401, $resp->getStatusCode());
  129. }
  130. }