File: /home/xedaptot/ai.naniguide.com/tests/Unit/Plans/RateLimitsServiceTest.php
<?php
/**
* RateLimitsServiceTest — wraps existing RateTracker file-based infrastructure.
*
* See docs/payment-order-plan-subscription-saas/SUBSCRIPTION-COMPREHENSIVE-DESIGN.md §6.4.
*/
use App\Model\Customer;
use App\Model\Plan;
use App\Model\PlanRateLimit;
use App\Model\Subscription;
use App\Services\Plans\RateLimits\RateLimitKey;
use App\Services\Plans\RateLimits\RateLimitsService;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
uses(TestCase::class);
beforeEach(function () {
$this->customer = Customer::forceCreate([
'uid' => uniqid('test_c_'),
'timezone' => 'UTC',
'status' => 'active',
]);
$this->plan = Plan::forceCreate([
'uid' => uniqid('test_plan_'),
'name' => 'Test Plan',
'currency_id' => 1,
'frequency_amount' => 1,
'frequency_unit' => 'month',
'price' => 0,
'status' => 'active',
]);
$this->sub = Subscription::create([
'uid' => (string) \Illuminate\Support\Str::uuid(),
'customer_id' => $this->customer->id,
'plan_id' => $this->plan->id,
'status' => 'active',
'current_period_ends_at' => now()->addMonth(),
]);
$this->svc = app(RateLimitsService::class);
});
afterEach(function () {
// Remove tracker files
$patterns = [
storage_path("app/quota/subscription-rate-{$this->sub->uid}-*"),
storage_path("app/quota/subscription-rate-{$this->sub->uid}-*-lock"),
];
foreach ($patterns as $pattern) {
foreach (glob($pattern) ?: [] as $f) {
@unlink($f);
}
}
DB::table('plan_rate_limits')->where('plan_id', $this->plan->id)->delete();
$this->sub->delete();
$this->plan->delete();
$this->customer->delete();
});
test('check returns true when no rate limit configured', function () {
expect($this->svc->check($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBeTrue();
});
test('check returns true within window, false when exceeded', function () {
PlanRateLimit::factory()->for($this->plan, 'plan')
->forKey(RateLimitKey::SEND_EMAIL_RATE)
->state(['limit_value' => 3, 'time_amount' => 1, 'time_unit' => 'minute'])
->create();
expect($this->svc->check($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBeTrue();
$this->svc->hit($this->sub, RateLimitKey::SEND_EMAIL_RATE, 3);
// Over limit now
expect($this->svc->check($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBeFalse();
});
test('remaining reflects configured limit minus hits', function () {
PlanRateLimit::factory()->for($this->plan, 'plan')
->forKey(RateLimitKey::SEND_EMAIL_RATE)
->state(['limit_value' => 10, 'time_amount' => 1, 'time_unit' => 'minute'])
->create();
expect($this->svc->remaining($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBe(10);
$this->svc->hit($this->sub, RateLimitKey::SEND_EMAIL_RATE, 4);
expect($this->svc->remaining($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBe(6);
});
test('remaining returns null when no config', function () {
expect($this->svc->remaining($this->sub, RateLimitKey::SEND_EMAIL_RATE))->toBeNull();
});