diff --git a/src/Limit.php b/src/Limit.php index 3d4bfe9..7d82c85 100644 --- a/src/Limit.php +++ b/src/Limit.php @@ -130,8 +130,10 @@ public function exceeded(?int $releaseInSeconds = null): void $this->hits = $this->allow; - if (isset($releaseInSeconds)) { - $interval = DateInterval::createFromDateString($releaseInSeconds . ' seconds'); + $seconds = $releaseInSeconds ?? $this->releaseInSeconds; + + if ($seconds > 0) { + $interval = DateInterval::createFromDateString($seconds . ' seconds'); if ($interval === false) { return; diff --git a/tests/Feature/HasRateLimitsTest.php b/tests/Feature/HasRateLimitsTest.php index 97f3081..3a6c28f 100644 --- a/tests/Feature/HasRateLimitsTest.php +++ b/tests/Feature/HasRateLimitsTest.php @@ -51,12 +51,12 @@ expect($storeData)->toHaveKey('TestConnector:3_every_60'); expect($storeData)->toHaveKey('TestConnector:too_many_attempts_limit'); - expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, @@ -69,12 +69,12 @@ $storeData = $store->getStore(); - expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toLookLike([ 'hits' => 2, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, @@ -87,12 +87,12 @@ $storeData = $store->getStore(); - expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toLookLike([ 'hits' => 3, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, @@ -145,7 +145,7 @@ expect($storeData)->toHaveKey('TestConnector:3_every_5'); expect($storeData)->toHaveKey('TestConnector:too_many_attempts_limit'); - expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusFive, ]); @@ -157,7 +157,7 @@ $storeData = $store->getStore(); - expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toLookLike([ 'hits' => 2, 'timestamp' => $currentTimestampPlusFive, ]); @@ -169,7 +169,7 @@ $storeData = $store->getStore(); - expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_5']))->toLookLike([ 'hits' => 3, 'timestamp' => $currentTimestampPlusFive, ]); @@ -344,12 +344,12 @@ expect($storeData)->toHaveKey('LimitedRequest:60_every_60'); expect($storeData)->toHaveKey('LimitedRequest:too_many_attempts_limit'); - expect(parseRawLimit($storeData['LimitedRequest:60_every_60']))->toEqual([ + expect(parseRawLimit($storeData['LimitedRequest:60_every_60']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['LimitedRequest:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['LimitedRequest:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, @@ -390,23 +390,23 @@ expect($storeData)->toHaveKey('TestConnector:3_every_60'); expect($storeData)->toHaveKey('TestConnector:too_many_attempts_limit'); - expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:3_every_60']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['TestConnector:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['LimitedRequest:60_every_60']))->toEqual([ + expect(parseRawLimit($storeData['LimitedRequest:60_every_60']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['LimitedRequest:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['LimitedRequest:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, @@ -443,12 +443,12 @@ expect($storeData)->toHaveKey('LimitedSoloRequest:60_every_60'); expect($storeData)->toHaveKey('LimitedSoloRequest:too_many_attempts_limit'); - expect(parseRawLimit($storeData['LimitedSoloRequest:60_every_60']))->toEqual([ + expect(parseRawLimit($storeData['LimitedSoloRequest:60_every_60']))->toLookLike([ 'hits' => 1, 'timestamp' => $currentTimestampPlusSixty, ]); - expect(parseRawLimit($storeData['LimitedSoloRequest:too_many_attempts_limit']))->toEqual([ + expect(parseRawLimit($storeData['LimitedSoloRequest:too_many_attempts_limit']))->toLookLike([ 'hits' => 0, 'allow' => 1, 'timestamp' => $currentTimestampPlusSixty, diff --git a/tests/Pest.php b/tests/Pest.php index 78c84f2..d24fc35 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -44,6 +44,22 @@ function parseRawLimit(?string $data): ?array return ! empty($data) ? json_decode($data, true) : null; } +expect()->extend('toLookLike', function (array $expected) { + $actual = $this->value; + + $expectedTimestamp = $expected['timestamp']; + unset($expected['timestamp']); + + $actualTimestamp = $actual['timestamp']; + unset($actual['timestamp']); + + expect($actual)->toEqual($expected); + expect($actualTimestamp)->toBeGreaterThanOrEqual($expectedTimestamp); + expect($actualTimestamp)->toBeLessThanOrEqual($expectedTimestamp + 1); + + return $this; +}); + /** * Reset the testing directory */ diff --git a/tests/Unit/LimitTest.php b/tests/Unit/LimitTest.php index e77bd59..871c365 100644 --- a/tests/Unit/LimitTest.php +++ b/tests/Unit/LimitTest.php @@ -100,3 +100,32 @@ expect($limit->getReleaseInSeconds())->toEqual($seconds); }); + +test('exceeded without releaseInSeconds falls back to the configured interval', function () { + $limit = Limit::allow(10)->everySeconds(120); + + $limit->exceeded(); + + expect($limit->wasManuallyExceeded())->toBeTrue() + ->and($limit->getHits())->toBe(10) + ->and($limit->getRemainingSeconds())->toBe(120); +}); + +test('exceeded with explicit releaseInSeconds uses the provided value', function () { + $limit = Limit::allow(10)->everySeconds(120); + + $limit->exceeded(releaseInSeconds: 300); + + expect($limit->wasManuallyExceeded())->toBeTrue() + ->and($limit->getRemainingSeconds())->toBe(300); +}); + +test('custom limiter exceeded without releaseInSeconds falls back to default 60 seconds', function () { + $limit = Limit::custom(function () { + }); + + $limit->exceeded(); + + expect($limit->wasManuallyExceeded())->toBeTrue() + ->and($limit->getRemainingSeconds())->toBe(60); +});