run job -> * assert that the touched IPs land in `ip_enrichment`. */ final class EnrichPendingJobTest extends AppTestCase { private const COUNTRY_DB = __DIR__ . '/../../Fixtures/geoip/country.mmdb'; private const ASN_DB = __DIR__ . '/../../Fixtures/geoip/asn.mmdb'; public function testEnrichesPendingIps(): void { // Pre-condition: a single reporter and report row for 81.2.69.142 (in the fixture as GB). $reporterId = $this->createReporter('test-rep'); $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'"); $ip = IpAddress::fromString('81.2.69.142'); $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $categoryId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.0000', 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'), 'metadata_json' => null, ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]); // Replace the container's EnrichmentService with one pointed at the fixtures. $service = new MmdbEnrichmentService( countryDbPath: self::COUNTRY_DB, asnDbPath: self::ASN_DB, adapter: new MaxMindRecordAdapter(), clock: new SystemClock(), logger: new Logger('t', [new TestHandler()]), ); /** @var IpEnrichmentRepository $repo */ $repo = $this->container->get(IpEnrichmentRepository::class); $job = new EnrichPendingJob($service, $repo); $context = new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []); $result = $job->run($context); self::assertSame(1, $result->itemsProcessed); $row = $repo->findByIpBin($ip->binary()); self::assertNotNull($row); self::assertSame('GB', $row['country_code']); } public function testNoOpWhenDbsAreMissing(): void { // Use bogus paths. $service = new MmdbEnrichmentService( countryDbPath: '/no/where/country.mmdb', asnDbPath: '/no/where/asn.mmdb', adapter: new MaxMindRecordAdapter(), clock: new SystemClock(), logger: new Logger('t', [new TestHandler()]), ); // Seed something so findPending would otherwise return rows. $reporterId = $this->createReporter('test-rep'); $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'"); $ip = IpAddress::fromString('203.0.113.7'); $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $categoryId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.0000', 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'), 'metadata_json' => null, ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]); /** @var IpEnrichmentRepository $repo */ $repo = $this->container->get(IpEnrichmentRepository::class); $job = new EnrichPendingJob($service, $repo); $result = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), [])); self::assertSame(0, $result->itemsProcessed); self::assertNull($repo->findByIpBin($ip->binary())); } public function testFindPendingExcludesAlreadyEnriched(): void { $service = new MmdbEnrichmentService( countryDbPath: self::COUNTRY_DB, asnDbPath: self::ASN_DB, adapter: new MaxMindRecordAdapter(), clock: new SystemClock(), logger: new Logger('t', [new TestHandler()]), ); $reporterId = $this->createReporter('test-rep'); $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'"); $ip = IpAddress::fromString('81.2.69.142'); $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $categoryId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.0000', 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'), 'metadata_json' => null, ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]); /** @var IpEnrichmentRepository $repo */ $repo = $this->container->get(IpEnrichmentRepository::class); $job = new EnrichPendingJob($service, $repo); $first = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), [])); self::assertSame(1, $first->itemsProcessed); // Second run: nothing left to do. $second = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), [])); self::assertSame(0, $second->itemsProcessed); } public function testReenrichClearAllowsReprocessing(): void { $service = new MmdbEnrichmentService( countryDbPath: self::COUNTRY_DB, asnDbPath: self::ASN_DB, adapter: new MaxMindRecordAdapter(), clock: new SystemClock(), logger: new Logger('t', [new TestHandler()]), ); $reporterId = $this->createReporter('test-rep'); $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'"); $ip = IpAddress::fromString('81.2.69.142'); $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $categoryId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.0000', 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'), 'metadata_json' => null, ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]); /** @var IpEnrichmentRepository $repo */ $repo = $this->container->get(IpEnrichmentRepository::class); $job = new EnrichPendingJob($service, $repo); $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), [])); $cleared = $repo->clearAllEnrichedAt(); self::assertSame(1, $cleared); $reRun = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), [])); self::assertSame(1, $reRun->itemsProcessed); } }