| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Http;
- use App\Domain\Time\FixedClock;
- use App\Infrastructure\Http\RateLimiter;
- use PHPUnit\Framework\TestCase;
- /**
- * Token-bucket invariants:
- * - capacity = 2 × refill, so 6 immediate consumes succeed at refill=3.
- * - the 7th fails, then advancing 1s restores 3 tokens.
- * - per-key isolation: two keys do not share a bucket.
- */
- final class RateLimiterTest extends TestCase
- {
- public function testBurstUpToCapacitySucceeds(): void
- {
- $clock = FixedClock::at('2026-04-29T00:00:00Z');
- $limiter = new RateLimiter($clock, refillPerSecond: 3.0, capacity: 6.0);
- for ($i = 0; $i < 6; $i++) {
- self::assertTrue($limiter->tryConsume(1), "consume #{$i} should succeed");
- }
- self::assertFalse($limiter->tryConsume(1), 'consume #7 should be denied');
- }
- public function testRefillRestoresTokensOverTime(): void
- {
- $clock = FixedClock::at('2026-04-29T00:00:00Z');
- $limiter = new RateLimiter($clock, refillPerSecond: 3.0, capacity: 6.0);
- for ($i = 0; $i < 6; $i++) {
- $limiter->tryConsume(1);
- }
- self::assertFalse($limiter->tryConsume(1));
- $clock->advance('+1 second');
- // After 1s @ 3/s we should have 3 fresh tokens.
- self::assertTrue($limiter->tryConsume(1));
- self::assertTrue($limiter->tryConsume(1));
- self::assertTrue($limiter->tryConsume(1));
- self::assertFalse($limiter->tryConsume(1));
- }
- public function testKeysAreIsolated(): void
- {
- $clock = FixedClock::at('2026-04-29T00:00:00Z');
- $limiter = new RateLimiter($clock, refillPerSecond: 1.0, capacity: 2.0);
- self::assertTrue($limiter->tryConsume(1));
- self::assertTrue($limiter->tryConsume(1));
- self::assertFalse($limiter->tryConsume(1));
- // Token 2's bucket is untouched.
- self::assertTrue($limiter->tryConsume(2));
- self::assertTrue($limiter->tryConsume(2));
- }
- }
|