PolicyEvaluatorTest.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Reputation;
  4. use App\Domain\Ip\IpAddress;
  5. use App\Domain\Policy\EvaluationOutcome;
  6. use App\Domain\Policy\Policy;
  7. use App\Domain\Policy\PolicyEvaluator;
  8. use App\Domain\Reputation\CidrEvaluator;
  9. use DateTimeImmutable;
  10. use DateTimeZone;
  11. use PHPUnit\Framework\TestCase;
  12. /**
  13. * Unit-level coverage for the score-vs-policy evaluator. Hand-built
  14. * `CidrEvaluator` snapshots avoid touching the DB.
  15. */
  16. final class PolicyEvaluatorTest extends TestCase
  17. {
  18. public function testAllowlistShortCircuitsEverything(): void
  19. {
  20. $ip = IpAddress::fromString('203.0.113.5');
  21. $cidr = new CidrEvaluator(
  22. manualIpBins: [$ip->binary()],
  23. manualSubnets: [],
  24. allowlistIpBins: [$ip->binary()],
  25. allowlistSubnets: [],
  26. );
  27. $policy = $this->makePolicy([1 => 0.5], includeManual: true);
  28. $evaluator = new PolicyEvaluator($policy, $cidr);
  29. $result = $evaluator->evaluate($ip, [1 => 5.0]);
  30. self::assertSame(EvaluationOutcome::ExcludedByAllowlist, $result->outcome);
  31. }
  32. public function testIncludedByScoreWhenAnyCategoryMeetsThreshold(): void
  33. {
  34. $ip = IpAddress::fromString('203.0.113.5');
  35. $cidr = new CidrEvaluator([], [], [], []);
  36. $policy = $this->makePolicy([1 => 1.0, 2 => 5.0]);
  37. $evaluator = new PolicyEvaluator($policy, $cidr);
  38. $result = $evaluator->evaluate($ip, [1 => 1.5, 2 => 0.1]);
  39. self::assertSame(EvaluationOutcome::IncludedByScore, $result->outcome);
  40. self::assertSame([1], $result->matchedCategoryIds);
  41. self::assertEqualsWithDelta(1.5, $result->maxScore, 1e-6);
  42. }
  43. public function testIncludedByScoreWithMultipleMatches(): void
  44. {
  45. $ip = IpAddress::fromString('203.0.113.5');
  46. $cidr = new CidrEvaluator([], [], [], []);
  47. $policy = $this->makePolicy([1 => 1.0, 2 => 0.5]);
  48. $evaluator = new PolicyEvaluator($policy, $cidr);
  49. $result = $evaluator->evaluate($ip, [1 => 2.0, 2 => 3.0]);
  50. self::assertSame(EvaluationOutcome::IncludedByScore, $result->outcome);
  51. self::assertSame([1, 2], $result->matchedCategoryIds);
  52. self::assertEqualsWithDelta(3.0, $result->maxScore, 1e-6);
  53. }
  54. public function testIncludedByManualBlockOnlyWhenPolicyIncludesIt(): void
  55. {
  56. $ip = IpAddress::fromString('203.0.113.5');
  57. $cidr = new CidrEvaluator(
  58. manualIpBins: [$ip->binary()],
  59. manualSubnets: [],
  60. allowlistIpBins: [],
  61. allowlistSubnets: [],
  62. );
  63. $policyIncluding = $this->makePolicy([1 => 5.0], includeManual: true);
  64. $policyExcluding = $this->makePolicy([1 => 5.0], includeManual: false);
  65. $including = (new PolicyEvaluator($policyIncluding, $cidr))->evaluate($ip, [1 => 0.0]);
  66. $excluding = (new PolicyEvaluator($policyExcluding, $cidr))->evaluate($ip, [1 => 0.0]);
  67. self::assertSame(EvaluationOutcome::IncludedByManualBlock, $including->outcome);
  68. self::assertSame(EvaluationOutcome::Excluded, $excluding->outcome);
  69. }
  70. public function testCategoryAbsentFromPolicyIsIgnored(): void
  71. {
  72. $ip = IpAddress::fromString('203.0.113.5');
  73. $cidr = new CidrEvaluator([], [], [], []);
  74. // policy only cares about category 1; even a sky-high score on
  75. // category 2 must be ignored.
  76. $policy = $this->makePolicy([1 => 1.0]);
  77. $evaluator = new PolicyEvaluator($policy, $cidr);
  78. $result = $evaluator->evaluate($ip, [2 => 999.0]);
  79. self::assertSame(EvaluationOutcome::Excluded, $result->outcome);
  80. }
  81. /**
  82. * @param array<int, float> $thresholds
  83. */
  84. private function makePolicy(array $thresholds, bool $includeManual = true): Policy
  85. {
  86. return new Policy(
  87. id: 1,
  88. name: 'test',
  89. description: null,
  90. includeManualBlocks: $includeManual,
  91. thresholds: $thresholds,
  92. createdAt: new DateTimeImmutable('now', new DateTimeZone('UTC')),
  93. );
  94. }
  95. }