createReporter('cutoff-test'); $ip = IpAddress::fromString('203.0.113.99'); /** @var int $catId */ $catId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug='brute_force'"); // 400 days ago — beyond the 365-day default cutoff. $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $catId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.00', 'received_at' => $clock->now()->modify('-400 days')->format('Y-m-d H:i:s'), ], ['ip_bin' => ParameterType::LARGE_OBJECT]); $scorer = new PairScorer( new ReportRepository($this->db), new CategoryRepository($this->db), $clock, 365, ); self::assertSame(0.0, $scorer->score($ip->binary(), $catId, $clock->now())); } public function testReportInsideCutoffIsCounted(): void { $clock = FixedClock::at('2026-04-29T00:00:00Z'); $reporterId = $this->createReporter('inside-cutoff-test'); $ip = IpAddress::fromString('203.0.113.123'); /** @var int $catId */ $catId = (int) $this->db->fetchOne("SELECT id FROM categories WHERE slug='brute_force'"); // 364 days ago — barely inside the cutoff. With a 14-day half-life, // its weight is essentially zero (0.5^26 ≈ 1.5e-8) but still > 0. $this->db->insert('reports', [ 'ip_bin' => $ip->binary(), 'ip_text' => $ip->text(), 'category_id' => $catId, 'reporter_id' => $reporterId, 'weight_at_report' => '1.00', 'received_at' => $clock->now()->modify('-364 days')->format('Y-m-d H:i:s'), ], ['ip_bin' => ParameterType::LARGE_OBJECT]); $scorer = new PairScorer( new ReportRepository($this->db), new CategoryRepository($this->db), $clock, 365, ); $score = $scorer->score($ip->binary(), $catId, $clock->now()); self::assertGreaterThan(0.0, $score); self::assertLessThan(1e-6, $score); } }