RateLimiterTest.php 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Http;
  4. use App\Domain\Time\FixedClock;
  5. use App\Infrastructure\Http\RateLimiter;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * Token-bucket invariants:
  9. * - capacity = 2 × refill, so 6 immediate consumes succeed at refill=3.
  10. * - the 7th fails, then advancing 1s restores 3 tokens.
  11. * - per-key isolation: two keys do not share a bucket.
  12. */
  13. final class RateLimiterTest extends TestCase
  14. {
  15. public function testBurstUpToCapacitySucceeds(): void
  16. {
  17. $clock = FixedClock::at('2026-04-29T00:00:00Z');
  18. $limiter = new RateLimiter($clock, refillPerSecond: 3.0, capacity: 6.0);
  19. for ($i = 0; $i < 6; $i++) {
  20. self::assertTrue($limiter->tryConsume(1), "consume #{$i} should succeed");
  21. }
  22. self::assertFalse($limiter->tryConsume(1), 'consume #7 should be denied');
  23. }
  24. public function testRefillRestoresTokensOverTime(): void
  25. {
  26. $clock = FixedClock::at('2026-04-29T00:00:00Z');
  27. $limiter = new RateLimiter($clock, refillPerSecond: 3.0, capacity: 6.0);
  28. for ($i = 0; $i < 6; $i++) {
  29. $limiter->tryConsume(1);
  30. }
  31. self::assertFalse($limiter->tryConsume(1));
  32. $clock->advance('+1 second');
  33. // After 1s @ 3/s we should have 3 fresh tokens.
  34. self::assertTrue($limiter->tryConsume(1));
  35. self::assertTrue($limiter->tryConsume(1));
  36. self::assertTrue($limiter->tryConsume(1));
  37. self::assertFalse($limiter->tryConsume(1));
  38. }
  39. public function testKeysAreIsolated(): void
  40. {
  41. $clock = FixedClock::at('2026-04-29T00:00:00Z');
  42. $limiter = new RateLimiter($clock, refillPerSecond: 1.0, capacity: 2.0);
  43. self::assertTrue($limiter->tryConsume(1));
  44. self::assertTrue($limiter->tryConsume(1));
  45. self::assertFalse($limiter->tryConsume(1));
  46. // Token 2's bucket is untouched.
  47. self::assertTrue($limiter->tryConsume(2));
  48. self::assertTrue($limiter->tryConsume(2));
  49. }
  50. }