HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/tests/Unit/RollingRateTrackerTest.php
<?php

use App\Library\Exception\RateLimitExceeded;
use App\Library\RateLimit;
use App\Library\RollingRateTracker;
use Carbon\Carbon;

test('Rolling rate tracker counts and rollbacks by minute', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file);

    $now = Carbon::create(2026, 4, 21, 12, 30, 0);
    $oneMinuteAgo = $now->copy()->subMinute();

    $tracker->count($oneMinuteAgo);
    $tracker->count($oneMinuteAgo);
    $tracker->count($now);
    $tracker->count($now);

    expect($tracker->getCreditsUsed($oneMinuteAgo, $now))->toBe(4);
    expect(count($tracker->getRecords($oneMinuteAgo, $now)))->toBe(2);

    $tracker->rollback();
    expect($tracker->getCreditsUsed($oneMinuteAgo, $now))->toBe(3);

    $tracker->rollback();
    expect($tracker->getCreditsUsed($oneMinuteAgo, $now))->toBe(2);
    expect(count($tracker->getRecords($oneMinuteAgo, $now)))->toBe(1);
});

test('Rolling rate tracker cleanup keeps recent minutes only', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file);

    $now = Carbon::create(2026, 4, 21, 12, 30, 0);
    $twoMinutesAgo = $now->copy()->subMinutes(2);
    $oneMinuteAgo = $now->copy()->subMinute();

    Carbon::setTestNow($now);

    $tracker->count($twoMinutesAgo);
    $tracker->count($twoMinutesAgo);
    $tracker->count($oneMinuteAgo);
    $tracker->count($oneMinuteAgo);
    $tracker->count($oneMinuteAgo);
    $tracker->count($now);

    expect($tracker->getCreditsUsed())->toBe(6);

    $tracker->cleanup('2 minute');
    expect($tracker->getCreditsUsed())->toBe(6);

    $tracker->cleanup('1 minute');
    expect($tracker->getCreditsUsed())->toBe(4);

    Carbon::setTestNow();
});

test('Rolling rate tracker enforces minute limits', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [
        new RateLimit(2, 1, 'minute', '2 per minute'),
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now);
    $tracker->count($now);

    $this->expectException(RateLimitExceeded::class);
    $tracker->count($now);
});

test('Rolling rate tracker enforces hour limits across multiple minutes', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [
        new RateLimit(3, 1, 'hour', '3 per hour'),
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now->copy()->subMinutes(50));
    $tracker->count($now->copy()->subMinutes(30));
    $tracker->count($now);

    $this->expectException(RateLimitExceeded::class);
    $tracker->count($now);
});

test('Rolling rate tracker enforces day limits across multiple hours', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [
        new RateLimit(3, 1, 'day', '3 per day'),
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now->copy()->subHours(23));
    $tracker->count($now->copy()->subHours(10));
    $tracker->count($now);

    $this->expectException(RateLimitExceeded::class);
    $tracker->count($now);
});

test('Rolling rate tracker test checks limits without mutating state', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [
        new RateLimit(5, 1, 'minute', '5 per minute'),
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now);
    $tracker->count($now);
    $tracker->count($now);

    $before = file_get_contents($tracker->getLockFilePath());

    $tracker->test($now);

    expect(file_get_contents($tracker->getLockFilePath()))->toBe($before);
    expect($tracker->getCreditsUsed($now, $now))->toBe(3);
});

test('Rolling rate tracker rejects unsupported limits', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();

    $this->expectException(Exception::class);
    new RollingRateTracker($file, [
        new RateLimit(10, 1, 'month', '10 per month'),
    ]);
});

test('Rolling rate tracker prunes old buckets during count when retention is configured', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [], [
        'retention_period' => '2 minute',
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now->copy()->subMinutes(3));
    $tracker->count($now->copy()->subMinute());
    $tracker->count($now);

    $records = $tracker->getRecords($now->copy()->subMinutes(2), $now);

    expect($records)->toBe([
        ['202604211229', 1],
        ['202604211230', 1],
    ]);
});

test('Rolling rate tracker rejects retention shorter than the configured limit window', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();

    $this->expectException(Exception::class);
    $this->expectExceptionMessage('retention_period "1 hour" is shorter than the configured rate limit window "1 day"');
    new RollingRateTracker($file, [
        new RateLimit(100, 1, 'day', '100 per day'),
    ], [
        'retention_period' => '1 hour',
    ]);
});

test('Rolling rate tracker rejects queries older than the retained data window', function () {
    $file = '/tmp/test-rolling-rate-'.uniqid();
    $tracker = new RollingRateTracker($file, [], [
        'retention_period' => '2 minute',
    ]);
    $now = Carbon::create(2026, 4, 21, 12, 30, 0);

    $tracker->count($now->copy()->subMinutes(3));
    $tracker->count($now->copy()->subMinute());
    $tracker->count($now);

    $this->expectException(Exception::class);
    $this->expectExceptionMessage('query start "202604211227" is older than the retained data window');
    $tracker->getCreditsUsed($now->copy()->subMinutes(3), $now);
});