1
0

JobsAdminControllerTest.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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/jobs/{status,trigger}` — admin-only wrapper that the UI
  9. * uses to invoke jobs without needing the internal token.
  10. */
  11. final class JobsAdminControllerTest extends AppTestCase
  12. {
  13. public function testStatusRequiresViewer(): void
  14. {
  15. $resp = $this->request('GET', '/api/v1/admin/jobs/status');
  16. self::assertSame(401, $resp->getStatusCode());
  17. }
  18. public function testStatusReturnsAllJobs(): void
  19. {
  20. $token = $this->createToken(TokenKind::Admin, Role::Viewer);
  21. $resp = $this->request('GET', '/api/v1/admin/jobs/status', ['Authorization' => 'Bearer ' . $token]);
  22. self::assertSame(200, $resp->getStatusCode());
  23. $body = $this->decode($resp);
  24. self::assertArrayHasKey('jobs', $body);
  25. foreach (['recompute-scores', 'cleanup-audit', 'enrich-pending', 'refresh-geoip', 'tick'] as $name) {
  26. self::assertArrayHasKey($name, $body['jobs']);
  27. }
  28. }
  29. public function testTriggerOperatorForbidden(): void
  30. {
  31. $token = $this->createToken(TokenKind::Admin, Role::Operator);
  32. $resp = $this->request(
  33. 'POST',
  34. '/api/v1/admin/jobs/trigger/recompute-scores',
  35. ['Authorization' => 'Bearer ' . $token],
  36. );
  37. self::assertSame(403, $resp->getStatusCode());
  38. }
  39. public function testTriggerUnknownJobReturns404(): void
  40. {
  41. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  42. $resp = $this->request(
  43. 'POST',
  44. '/api/v1/admin/jobs/trigger/does-not-exist',
  45. ['Authorization' => 'Bearer ' . $token],
  46. );
  47. self::assertSame(404, $resp->getStatusCode());
  48. }
  49. public function testTriggerRecomputeRunsAndAuditsAsManual(): void
  50. {
  51. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  52. $resp = $this->request(
  53. 'POST',
  54. '/api/v1/admin/jobs/trigger/recompute-scores',
  55. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  56. '{}',
  57. );
  58. self::assertSame(200, $resp->getStatusCode());
  59. $body = $this->decode($resp);
  60. self::assertSame('recompute-scores', $body['job']);
  61. self::assertSame('success', $body['status']);
  62. // job_runs.triggered_by = manual
  63. $row = $this->db->fetchAssociative(
  64. "SELECT triggered_by FROM job_runs WHERE job_name = 'recompute-scores' ORDER BY id DESC LIMIT 1"
  65. );
  66. self::assertSame('manual', $row['triggered_by']);
  67. // Audit row attributed to the admin token
  68. $audit = $this->db->fetchAssociative(
  69. "SELECT actor_kind, action, target_id, details_json FROM audit_log WHERE action = 'job.triggered' ORDER BY id DESC LIMIT 1"
  70. );
  71. self::assertIsArray($audit);
  72. self::assertSame('admin-token', $audit['actor_kind']);
  73. self::assertSame('recompute-scores', $audit['target_id']);
  74. $details = json_decode((string) $audit['details_json'], true);
  75. self::assertSame('manual', $details['triggered_by']);
  76. }
  77. public function testRefreshGeoip412UnderMaxmindWithoutKey(): void
  78. {
  79. // Swap the downloader binding to MaxMind without a key.
  80. if (method_exists($this->container, 'set')) {
  81. /** @var \DI\Container $c */
  82. $c = $this->container;
  83. $c->set(
  84. \App\Infrastructure\Enrichment\Downloaders\GeoIpDownloader::class,
  85. new \App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader(new \GuzzleHttp\Client(), licenseKey: ''),
  86. );
  87. $c->set(
  88. \App\Application\Admin\JobsAdminController::class,
  89. $c->make(\App\Application\Admin\JobsAdminController::class),
  90. );
  91. $this->app = \App\App\AppFactory::build($this->container);
  92. }
  93. $token = $this->createToken(TokenKind::Admin, Role::Admin);
  94. $resp = $this->request(
  95. 'POST',
  96. '/api/v1/admin/jobs/trigger/refresh-geoip',
  97. ['Authorization' => 'Bearer ' . $token],
  98. );
  99. self::assertSame(412, $resp->getStatusCode());
  100. $body = $this->decode($resp);
  101. self::assertSame('no_credential', $body['error']);
  102. self::assertSame('maxmind', $body['provider']);
  103. self::assertSame('MAXMIND_LICENSE_KEY', $body['missing']);
  104. }
  105. }