| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Internal;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * End-to-end coverage for `/internal/jobs/*`. The AppTestCase boots a Slim
- * app whose ServerRequestFactory does NOT populate REMOTE_ADDR — so we
- * inject one explicitly via the request() helper override below to satisfy
- * InternalNetworkMiddleware.
- */
- final class JobsEndpointsTest extends AppTestCase
- {
- private const TOKEN = 'test-internal-token';
- protected function setUp(): void
- {
- // Ensure the container picks up our internal token.
- putenv('INTERNAL_JOB_TOKEN=' . self::TOKEN);
- $_ENV['INTERNAL_JOB_TOKEN'] = self::TOKEN;
- $_SERVER['INTERNAL_JOB_TOKEN'] = self::TOKEN;
- parent::setUp();
- // Override the internal-token middleware in the container so the
- // running Slim app validates against our test token. The base
- // AppTestCase wires a settings bundle that doesn't include the
- // token, so we patch the InternalTokenMiddleware factory directly.
- if (method_exists($this->container, 'set')) {
- /** @var \DI\Container $container */
- $container = $this->container;
- $container->set(
- \App\Infrastructure\Http\Middleware\InternalTokenMiddleware::class,
- new \App\Infrastructure\Http\Middleware\InternalTokenMiddleware(
- new \Slim\Psr7\Factory\ResponseFactory(),
- self::TOKEN,
- ),
- );
- }
- // Rebuild the app so it picks up the patched middleware.
- $this->app = \App\App\AppFactory::build($this->container);
- }
- public function testTickRequiresToken(): void
- {
- $resp = $this->internalRequest('POST', '/internal/jobs/tick', headers: []);
- self::assertSame(401, $resp->getStatusCode());
- }
- public function testTickRejectsWrongToken(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/tick',
- headers: ['Authorization' => 'Bearer wrong'],
- );
- self::assertSame(401, $resp->getStatusCode());
- }
- public function testExternalSourceReturns404OpaquePerSpec(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/tick',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- remoteAddr: '8.8.8.8',
- );
- self::assertSame(404, $resp->getStatusCode());
- }
- public function testTickSuccessProducesEnvelope(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/tick',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- );
- self::assertSame(200, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame('tick', $body['job']);
- self::assertSame('success', $body['status']);
- self::assertArrayHasKey('run_id', $body);
- self::assertArrayHasKey('duration_ms', $body);
- }
- public function testRecomputeSucceedsWithEmptyTables(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/recompute-scores',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- );
- self::assertSame(200, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame('recompute-scores', $body['job']);
- self::assertSame('success', $body['status']);
- }
- public function testConcurrentRecomputeProducesOneSuccessOneSkipped(): void
- {
- // Pre-acquire the lock to simulate "another worker is busy". The
- // second HTTP call must come back 409 with skipped_locked.
- $now = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
- $lockSql = 'INSERT INTO job_locks (job_name, acquired_by, acquired_at, expires_at) '
- . 'VALUES (:job, :owner, :acquired, :expires)';
- $this->db->executeStatement($lockSql, [
- 'job' => 'recompute-scores',
- 'owner' => 'OTHER',
- 'acquired' => $now->format('Y-m-d H:i:s'),
- 'expires' => $now->modify('+5 minutes')->format('Y-m-d H:i:s'),
- ]);
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/recompute-scores',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- body: '{"full": true}',
- );
- self::assertSame(409, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame('skipped_locked', $body['status']);
- }
- public function testRefreshGeoipReturns412(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/refresh-geoip',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- );
- self::assertSame(412, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame('not_implemented', $body['error']);
- }
- public function testStatusListsAllRegisteredJobs(): void
- {
- $resp = $this->internalRequest(
- 'GET',
- '/internal/jobs/status',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- );
- self::assertSame(200, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertArrayHasKey('jobs', $body);
- self::assertArrayHasKey('recompute-scores', $body['jobs']);
- self::assertArrayHasKey('cleanup-audit', $body['jobs']);
- self::assertArrayHasKey('enrich-pending', $body['jobs']);
- self::assertArrayHasKey('tick', $body['jobs']);
- }
- public function testCleanupAuditSucceedsAndRecordsRun(): void
- {
- $resp = $this->internalRequest(
- 'POST',
- '/internal/jobs/cleanup-audit',
- headers: ['Authorization' => 'Bearer ' . self::TOKEN],
- );
- self::assertSame(200, $resp->getStatusCode());
- $body = $this->decode($resp);
- self::assertSame('cleanup-audit', $body['job']);
- self::assertSame('success', $body['status']);
- $row = $this->db->fetchAssociative(
- "SELECT triggered_by, status FROM job_runs WHERE job_name = 'cleanup-audit'"
- );
- self::assertNotFalse($row);
- self::assertSame('schedule', $row['triggered_by']);
- self::assertSame('success', $row['status']);
- }
- /**
- * @param array<string, string> $headers
- */
- private function internalRequest(
- string $method,
- string $path,
- array $headers = [],
- ?string $body = null,
- string $remoteAddr = '127.0.0.1',
- ): \Psr\Http\Message\ResponseInterface {
- $factory = new \Slim\Psr7\Factory\ServerRequestFactory();
- $request = $factory->createServerRequest($method, $path, ['REMOTE_ADDR' => $remoteAddr]);
- foreach ($headers as $name => $value) {
- $request = $request->withHeader($name, $value);
- }
- if ($body !== null) {
- $stream = (new \Slim\Psr7\Factory\StreamFactory())->createStream($body);
- $request = $request->withBody($stream);
- $request = $request->withHeader('Content-Type', 'application/json');
- }
- return $this->app->handle($request);
- }
- }
|