| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Reputation;
- use App\Domain\Ip\IpAddress;
- use App\Domain\Reputation\PairScorer;
- use App\Domain\Time\FixedClock;
- use App\Infrastructure\Category\CategoryRepository;
- use App\Infrastructure\Reputation\ReportRepository;
- use App\Tests\Integration\Support\AppTestCase;
- use Doctrine\DBAL\ParameterType;
- /**
- * The hard cutoff (default 365 days) lives in PairScorer, not in the Decay
- * function. A report older than the cutoff must be entirely excluded from
- * the score even if its decay weight would still be non-zero.
- *
- * For exponential decay, that's the only way the score reaches *exactly*
- * zero (the formula `0.5^(age/half_life)` is strictly positive).
- */
- final class PairScorerCutoffTest extends AppTestCase
- {
- public function testReportBeyondHardCutoffExcludedEntirely(): void
- {
- $clock = FixedClock::at('2026-04-29T00:00:00Z');
- $reporterId = $this->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);
- }
- }
|