1
0

EnrichPendingJobTest.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Enrichment;
  4. use App\Application\Jobs\EnrichPendingJob;
  5. use App\Domain\Ip\IpAddress;
  6. use App\Domain\Jobs\JobContext;
  7. use App\Domain\Time\SystemClock;
  8. use App\Infrastructure\Enrichment\MaxMindRecordAdapter;
  9. use App\Infrastructure\Enrichment\MmdbEnrichmentService;
  10. use App\Infrastructure\Reputation\IpEnrichmentRepository;
  11. use App\Tests\Integration\Support\AppTestCase;
  12. use Monolog\Handler\TestHandler;
  13. use Monolog\Logger;
  14. /**
  15. * Drives the full enrich-pending flow: seed reports -> run job ->
  16. * assert that the touched IPs land in `ip_enrichment`.
  17. */
  18. final class EnrichPendingJobTest extends AppTestCase
  19. {
  20. private const COUNTRY_DB = __DIR__ . '/../../Fixtures/geoip/country.mmdb';
  21. private const ASN_DB = __DIR__ . '/../../Fixtures/geoip/asn.mmdb';
  22. public function testEnrichesPendingIps(): void
  23. {
  24. // Pre-condition: a single reporter and report row for 81.2.69.142 (in the fixture as GB).
  25. $reporterId = $this->createReporter('test-rep');
  26. $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'");
  27. $ip = IpAddress::fromString('81.2.69.142');
  28. $this->db->insert('reports', [
  29. 'ip_bin' => $ip->binary(),
  30. 'ip_text' => $ip->text(),
  31. 'category_id' => $categoryId,
  32. 'reporter_id' => $reporterId,
  33. 'weight_at_report' => '1.0000',
  34. 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
  35. 'metadata_json' => null,
  36. ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]);
  37. // Replace the container's EnrichmentService with one pointed at the fixtures.
  38. $service = new MmdbEnrichmentService(
  39. countryDbPath: self::COUNTRY_DB,
  40. asnDbPath: self::ASN_DB,
  41. adapter: new MaxMindRecordAdapter(),
  42. clock: new SystemClock(),
  43. logger: new Logger('t', [new TestHandler()]),
  44. );
  45. /** @var IpEnrichmentRepository $repo */
  46. $repo = $this->container->get(IpEnrichmentRepository::class);
  47. $job = new EnrichPendingJob($service, $repo);
  48. $context = new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []);
  49. $result = $job->run($context);
  50. self::assertSame(1, $result->itemsProcessed);
  51. $row = $repo->findByIpBin($ip->binary());
  52. self::assertNotNull($row);
  53. self::assertSame('GB', $row['country_code']);
  54. }
  55. public function testNoOpWhenDbsAreMissing(): void
  56. {
  57. // Use bogus paths.
  58. $service = new MmdbEnrichmentService(
  59. countryDbPath: '/no/where/country.mmdb',
  60. asnDbPath: '/no/where/asn.mmdb',
  61. adapter: new MaxMindRecordAdapter(),
  62. clock: new SystemClock(),
  63. logger: new Logger('t', [new TestHandler()]),
  64. );
  65. // Seed something so findPending would otherwise return rows.
  66. $reporterId = $this->createReporter('test-rep');
  67. $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'");
  68. $ip = IpAddress::fromString('203.0.113.7');
  69. $this->db->insert('reports', [
  70. 'ip_bin' => $ip->binary(),
  71. 'ip_text' => $ip->text(),
  72. 'category_id' => $categoryId,
  73. 'reporter_id' => $reporterId,
  74. 'weight_at_report' => '1.0000',
  75. 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
  76. 'metadata_json' => null,
  77. ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]);
  78. /** @var IpEnrichmentRepository $repo */
  79. $repo = $this->container->get(IpEnrichmentRepository::class);
  80. $job = new EnrichPendingJob($service, $repo);
  81. $result = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []));
  82. self::assertSame(0, $result->itemsProcessed);
  83. self::assertNull($repo->findByIpBin($ip->binary()));
  84. }
  85. public function testFindPendingExcludesAlreadyEnriched(): void
  86. {
  87. $service = new MmdbEnrichmentService(
  88. countryDbPath: self::COUNTRY_DB,
  89. asnDbPath: self::ASN_DB,
  90. adapter: new MaxMindRecordAdapter(),
  91. clock: new SystemClock(),
  92. logger: new Logger('t', [new TestHandler()]),
  93. );
  94. $reporterId = $this->createReporter('test-rep');
  95. $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'");
  96. $ip = IpAddress::fromString('81.2.69.142');
  97. $this->db->insert('reports', [
  98. 'ip_bin' => $ip->binary(),
  99. 'ip_text' => $ip->text(),
  100. 'category_id' => $categoryId,
  101. 'reporter_id' => $reporterId,
  102. 'weight_at_report' => '1.0000',
  103. 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
  104. 'metadata_json' => null,
  105. ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]);
  106. /** @var IpEnrichmentRepository $repo */
  107. $repo = $this->container->get(IpEnrichmentRepository::class);
  108. $job = new EnrichPendingJob($service, $repo);
  109. $first = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []));
  110. self::assertSame(1, $first->itemsProcessed);
  111. // Second run: nothing left to do.
  112. $second = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []));
  113. self::assertSame(0, $second->itemsProcessed);
  114. }
  115. public function testReenrichClearAllowsReprocessing(): void
  116. {
  117. $service = new MmdbEnrichmentService(
  118. countryDbPath: self::COUNTRY_DB,
  119. asnDbPath: self::ASN_DB,
  120. adapter: new MaxMindRecordAdapter(),
  121. clock: new SystemClock(),
  122. logger: new Logger('t', [new TestHandler()]),
  123. );
  124. $reporterId = $this->createReporter('test-rep');
  125. $categoryId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug = 'brute_force'");
  126. $ip = IpAddress::fromString('81.2.69.142');
  127. $this->db->insert('reports', [
  128. 'ip_bin' => $ip->binary(),
  129. 'ip_text' => $ip->text(),
  130. 'category_id' => $categoryId,
  131. 'reporter_id' => $reporterId,
  132. 'weight_at_report' => '1.0000',
  133. 'received_at' => (new \DateTimeImmutable('-1 minute', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
  134. 'metadata_json' => null,
  135. ], ['ip_bin' => \Doctrine\DBAL\ParameterType::LARGE_OBJECT]);
  136. /** @var IpEnrichmentRepository $repo */
  137. $repo = $this->container->get(IpEnrichmentRepository::class);
  138. $job = new EnrichPendingJob($service, $repo);
  139. $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []));
  140. $cleared = $repo->clearAllEnrichedAt();
  141. self::assertSame(1, $cleared);
  142. $reRun = $job->run(new JobContext(new SystemClock(), new Logger('t', [new TestHandler()]), []));
  143. self::assertSame(1, $reRun->itemsProcessed);
  144. }
  145. }