1
0

MaintenanceControllerTest.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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\Tests\Integration\Support\AppTestCase;
  7. /**
  8. * `/api/v1/admin/maintenance/{purge,seed-demo}` — admin-only one-shot
  9. * data tools used by the Settings page for demos and resets.
  10. */
  11. final class MaintenanceControllerTest extends AppTestCase
  12. {
  13. public function testPurgeRequiresAdmin(): void
  14. {
  15. $token = $this->createToken(TokenKind::Admin, Role::Operator);
  16. $resp = $this->request(
  17. 'POST',
  18. '/api/v1/admin/maintenance/purge',
  19. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  20. '{"confirm": "PURGE"}',
  21. );
  22. self::assertSame(403, $resp->getStatusCode());
  23. }
  24. public function testPurgeRequiresLiteralConfirmString(): void
  25. {
  26. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  27. $resp = $this->request(
  28. 'POST',
  29. '/api/v1/admin/maintenance/purge',
  30. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  31. '{"confirm": "yes"}',
  32. );
  33. self::assertSame(400, $resp->getStatusCode());
  34. self::assertSame('validation_failed', $this->decode($resp)['error']);
  35. }
  36. public function testPurgeWipesOperationalDataButPreservesServiceToken(): void
  37. {
  38. // Seed a service token (bypassing the controller; the API protects it).
  39. $this->db->insert('api_tokens', [
  40. 'kind' => TokenKind::Service->value,
  41. 'token_hash' => str_repeat('a', 64),
  42. 'token_prefix' => 'irdb_svc',
  43. ]);
  44. // And one operational token, plus a reporter and a stray report.
  45. $reporterId = $this->createReporter('purge-victim');
  46. $catRow = $this->db->fetchAssociative('SELECT id FROM categories LIMIT 1');
  47. self::assertNotFalse($catRow);
  48. $this->db->insert('reports', [
  49. 'ip_bin' => str_repeat("\x00", 12) . "\x00\x00\x00\x01",
  50. 'ip_text' => '0.0.0.1',
  51. 'category_id' => (int) $catRow['id'],
  52. 'reporter_id' => $reporterId,
  53. 'weight_at_report' => '1.00',
  54. ]);
  55. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  56. $resp = $this->request(
  57. 'POST',
  58. '/api/v1/admin/maintenance/purge',
  59. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  60. '{"confirm": "PURGE"}',
  61. );
  62. self::assertSame(200, $resp->getStatusCode());
  63. $body = $this->decode($resp);
  64. self::assertSame('purged', $body['status']);
  65. self::assertGreaterThanOrEqual(1, (int) $body['deleted']['reports']);
  66. // Service token survives, the admin token does not.
  67. $svcCount = (int) $this->db->fetchOne(
  68. 'SELECT COUNT(*) FROM api_tokens WHERE kind = :k',
  69. ['k' => TokenKind::Service->value],
  70. );
  71. self::assertSame(1, $svcCount);
  72. $allTokens = (int) $this->db->fetchOne('SELECT COUNT(*) FROM api_tokens');
  73. self::assertSame(1, $allTokens, 'only the service token should remain');
  74. // Reporters / reports gone.
  75. self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM reporters'));
  76. self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM reports'));
  77. // Categories preserved.
  78. self::assertGreaterThan(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM categories'));
  79. }
  80. public function testSeedDemoPopulatesDataAndIsIdempotent(): void
  81. {
  82. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  83. $first = $this->request(
  84. 'POST',
  85. '/api/v1/admin/maintenance/seed-demo',
  86. ['Authorization' => 'Bearer ' . $token],
  87. );
  88. self::assertSame(200, $first->getStatusCode());
  89. $body = $this->decode($first);
  90. self::assertSame('seeded', $body['status']);
  91. self::assertGreaterThan(0, (int) $body['summary']['reporters']);
  92. self::assertGreaterThan(0, (int) $body['summary']['ips']);
  93. self::assertGreaterThan(0, (int) $body['summary']['reports']);
  94. // Real rows landed.
  95. self::assertGreaterThan(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM reporters'));
  96. self::assertGreaterThan(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM reports'));
  97. // Second call short-circuits with 409.
  98. $second = $this->request(
  99. 'POST',
  100. '/api/v1/admin/maintenance/seed-demo',
  101. ['Authorization' => 'Bearer ' . $token],
  102. );
  103. self::assertSame(409, $second->getStatusCode());
  104. self::assertSame('already_seeded', $this->decode($second)['error']);
  105. }
  106. public function testSeedDemoForbiddenForViewer(): void
  107. {
  108. $token = $this->createToken(TokenKind::Admin, Role::Viewer);
  109. $resp = $this->request(
  110. 'POST',
  111. '/api/v1/admin/maintenance/seed-demo',
  112. ['Authorization' => 'Bearer ' . $token],
  113. );
  114. self::assertSame(403, $resp->getStatusCode());
  115. }
  116. }