| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Reputation;
- use App\Domain\Ip\Cidr;
- use App\Domain\Ip\IpAddress;
- use App\Domain\Reputation\CidrEvaluator;
- use App\Domain\Reputation\EffectiveStatus;
- use App\Domain\Reputation\EffectiveStatusService;
- use App\Infrastructure\Reputation\CidrEvaluatorFactory;
- use App\Infrastructure\Reputation\IpScoreRepository;
- use Doctrine\DBAL\Connection;
- use PHPUnit\Framework\TestCase;
- /**
- * Locks the SPEC §5 precedence: allowlist > manual block > scored > clean.
- * M09 wires `Scored` to "any non-zero score in `ip_scores`" via a stub
- * IpScoreRepository here; the integration tests cover the real path.
- */
- final class EffectiveStatusServiceTest extends TestCase
- {
- public function testAllowlistWinsOverManualBlock(): void
- {
- $bin = IpAddress::fromString('198.51.100.5')->binary();
- $factory = $this->factoryReturning(new CidrEvaluator(
- manualIpBins: [$bin],
- manualSubnets: [],
- allowlistIpBins: [$bin],
- allowlistSubnets: [],
- ));
- $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
- self::assertSame(
- EffectiveStatus::Allowlisted,
- $service->forIp(IpAddress::fromString('198.51.100.5'))
- );
- }
- public function testManualBlockReturnedWhenNotAllowlisted(): void
- {
- $bin = IpAddress::fromString('203.0.113.42')->binary();
- $factory = $this->factoryReturning(new CidrEvaluator([$bin], [], [], []));
- $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
- self::assertSame(
- EffectiveStatus::ManuallyBlocked,
- $service->forIp(IpAddress::fromString('203.0.113.42'))
- );
- }
- public function testIpInsideAllowlistedSubnetEvenWithSeparateManualBlockIsAllowlisted(): void
- {
- $allow = Cidr::fromString('203.0.113.0/24');
- $factory = $this->factoryReturning(new CidrEvaluator(
- manualIpBins: [IpAddress::fromString('203.0.113.42')->binary()],
- manualSubnets: [],
- allowlistIpBins: [],
- allowlistSubnets: [$allow],
- ));
- $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
- self::assertSame(
- EffectiveStatus::Allowlisted,
- $service->forIp(IpAddress::fromString('203.0.113.42'))
- );
- }
- public function testScoredWhenScoreRepoHasRowsAndNoOverrides(): void
- {
- $factory = $this->factoryReturning(new CidrEvaluator([], [], [], []));
- $service = new EffectiveStatusService($factory, $this->scoreRepoWithScores());
- self::assertSame(
- EffectiveStatus::Scored,
- $service->forIp(IpAddress::fromString('203.0.113.99'))
- );
- }
- public function testCleanWhenNothingMatches(): void
- {
- $factory = $this->factoryReturning(new CidrEvaluator([], [], [], []));
- $service = new EffectiveStatusService($factory, $this->scoreRepoWithoutScores());
- self::assertSame(
- EffectiveStatus::Clean,
- $service->forIp(IpAddress::fromString('203.0.113.99'))
- );
- }
- private function factoryReturning(CidrEvaluator $evaluator): CidrEvaluatorFactory
- {
- return new class ($evaluator) extends CidrEvaluatorFactory {
- public function __construct(private readonly CidrEvaluator $fixed)
- {
- // Deliberately not calling parent::__construct — this stub
- // never queries the DB.
- }
- public function get(): CidrEvaluator
- {
- return $this->fixed;
- }
- public function invalidate(): void
- {
- // no-op
- }
- };
- }
- private function scoreRepoWithoutScores(): IpScoreRepository
- {
- return new class ($this->createMock(Connection::class)) extends IpScoreRepository {
- public function hasAnyScore(string $ipBin): bool
- {
- return false;
- }
- };
- }
- private function scoreRepoWithScores(): IpScoreRepository
- {
- return new class ($this->createMock(Connection::class)) extends IpScoreRepository {
- public function hasAnyScore(string $ipBin): bool
- {
- return true;
- }
- };
- }
- }
|