Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests/DTO/RateLimitStatusDTOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function testConstructorAndToArray(): void
'blocked' => true,
'backoff_seconds' => 15,
'next_allowed_at' => '2025-01-01 12:00:00',
'source' => null,
];

$this->assertSame($expected, $dto->toArray());
Expand All @@ -50,6 +51,7 @@ public function testNullableFields(): void
'blocked' => false,
'backoff_seconds' => null,
'next_allowed_at' => null,
'source' => null,
];

$this->assertSame($expected, $dto->toArray());
Expand Down
71 changes: 8 additions & 63 deletions tests/MiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
<?php

/**
* Created by Maatify.dev
* User: Maatify.dev
* Date: 2025-11-07
* Time: 02:54
* Project: rate-limiter
* IDE: PhpStorm
* https://www.Maatify.dev
*/

declare(strict_types=1);

namespace Maatify\RateLimiter\Tests;
Expand All @@ -18,69 +8,24 @@
use Maatify\RateLimiter\Resolver\RateLimiterResolver;
use Maatify\RateLimiter\Enums\RateLimitActionEnum;
use Maatify\RateLimiter\Enums\PlatformEnum;
use Maatify\RateLimiter\Contracts\RateLimiterInterface;
use Maatify\RateLimiter\Resolver\EnforcingRateLimiter;

/**
* 🧩 Class MiddlewareTest
*
* 🎯 Purpose:
* Validates key middleware dependencies and the resolver’s ability
* to correctly instantiate rate limiter drivers and enum behaviors.
*
* ⚙️ Focus:
* - Ensures `RateLimiterResolver` creates a valid driver instance.
* - Verifies enum `value()` method consistency.
*
* ✅ Example execution:
* ```bash
* ./vendor/bin/phpunit --filter MiddlewareTest
* ```
*
* @package Maatify\RateLimiter\Tests
*/
final class MiddlewareTest extends TestCase
{
/**
* 🧠 Test that the resolver creates a valid Redis driver instance.
*
* 🎯 Ensures:
* - The resolver successfully returns an object implementing {@see \Maatify\RateLimiter\Contracts\RateLimiterInterface}.
* - Default behavior with `driver => redis` works as expected.
*
* ✅ Example:
* ```php
* $resolver = new RateLimiterResolver(['driver' => 'redis']);
* $driver = $resolver->resolve();
* ```
*/
public function testResolverCreatesRedisDriver(): void
public function testResolverSelectsDriver(): void
{
// ⚙️ Load Redis configuration dynamically
$redisHost = getenv('REDIS_HOST') ?: ($_ENV['REDIS_HOST'] ?? '127.0.0.1');
$redisPort = getenv('REDIS_PORT') ?: ($_ENV['REDIS_PORT'] ?? '6379');
$redisPassword = getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? '');
$mockDriver = $this->createMock(RateLimiterInterface::class);

// 🧩 Initialize Redis driver via resolver
$resolver = new RateLimiterResolver([
'driver' => 'redis',
'redis_host' => $redisHost,
'redis_port' => $redisPort,
'redis_password' => $redisPassword,
]);
'redis' => $mockDriver,
], 'redis');

$driver = $resolver->resolve();

$this->assertInstanceOf(
\Maatify\RateLimiter\Contracts\RateLimiterInterface::class,
$driver,
'Resolver must return a valid RateLimiterInterface implementation for Redis.'
);
$this->assertInstanceOf(EnforcingRateLimiter::class, $driver);
}

/**
* 🔍 Test that enums return correct string values.
*
* 🎯 Verifies that `RateLimitActionEnum` and `PlatformEnum`
* correctly return their string identifiers through `value()`.
*/
public function testActionEnums(): void
{
$this->assertSame(
Expand Down
154 changes: 154 additions & 0 deletions tests/Phase5/ActionLimiterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

namespace Maatify\RateLimiter\Tests\Phase5;

use Maatify\RateLimiter\Contracts\BackoffPolicyInterface;
use Maatify\RateLimiter\Contracts\PlatformInterface;
use Maatify\RateLimiter\Contracts\RateLimitActionInterface;
use Maatify\RateLimiter\Contracts\RateLimiterInterface;
use Maatify\RateLimiter\DTO\RateLimitStatusDTO;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use Maatify\RateLimiter\Resolver\EnforcingRateLimiter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

final class ActionLimiterTest extends TestCase
{
/** @var RateLimiterInterface&MockObject */
private RateLimiterInterface $driver;

/** @var BackoffPolicyInterface&MockObject */
private BackoffPolicyInterface $backoffPolicy;

private EnforcingRateLimiter $resolver;

protected function setUp(): void
{
$this->driver = $this->createMock(RateLimiterInterface::class);
$this->backoffPolicy = $this->createMock(BackoffPolicyInterface::class);
$this->resolver = new EnforcingRateLimiter($this->driver, $this->backoffPolicy);
}

public function testActionLimiterExecutesIfGlobalPasses(): void
{
$action = $this->createMock(RateLimitActionInterface::class);
$platform = $this->createMock(PlatformInterface::class);

$callCount = 0;

$this->driver->expects($this->exactly(2))
->method('attempt')
->willReturnCallback(function (string $key, RateLimitActionInterface $act, PlatformInterface $plt) use (&$callCount) {
$callCount++;
if ($callCount === 1) {
// Global passes
return new RateLimitStatusDTO(100, 99, 60, null, false, null, null, 'global');
}

// Action passes
return new RateLimitStatusDTO(10, 5, 60, null, false, null, null, 'action');
});

$status = $this->resolver->attempt('key', $action, $platform);

$this->assertSame('action', $status->source);
$this->assertFalse($status->blocked);
}

public function testActionLimiterBlock(): void
{
$action = $this->createMock(RateLimitActionInterface::class);
$platform = $this->createMock(PlatformInterface::class);

$callCount = 0;

$this->driver->expects($this->exactly(2))
->method('attempt')
->willReturnCallback(function (string $key, RateLimitActionInterface $act, PlatformInterface $plt) use (&$callCount) {
$callCount++;
if ($callCount === 1) {
// Global passes
return new RateLimitStatusDTO(100, 99, 60, null, false, null, null, 'global');
}

// Action blocks
$dto = new RateLimitStatusDTO(10, 0, 60, null, true, null, null, 'action');
throw new TooManyRequestsException('Action limit', 429, $dto);
});

$this->backoffPolicy->expects($this->once())
->method('calculateDelay')
->willReturn(15);

try {
$this->resolver->attempt('key', $action, $platform);
$this->fail('Expected TooManyRequestsException was not thrown');
} catch (TooManyRequestsException $e) {
$status = $e->status;
$this->assertNotNull($status);
$this->assertSame('action', $status->source);
$this->assertSame(15, $status->retryAfter);
$this->assertTrue($status->blocked);
$this->assertNotNull($status->nextAllowedAt);
}
}

public function testSourceIsSetIfNotProvidedByDriver(): void
{
$action = $this->createMock(RateLimitActionInterface::class);
$platform = $this->createMock(PlatformInterface::class);

$callCount = 0;

$this->driver->expects($this->exactly(2))
->method('attempt')
->willReturnCallback(function (string $key, RateLimitActionInterface $act, PlatformInterface $plt) use (&$callCount) {
$callCount++;
if ($callCount === 1) {
return new RateLimitStatusDTO(100, 99, 60); // No source
}
return new RateLimitStatusDTO(10, 5, 60); // No source
});

$status = $this->resolver->attempt('key', $action, $platform);

$this->assertSame('action', $status->source);
}

public function testResetResetsGlobalAndAction(): void
{
$action = $this->createMock(RateLimitActionInterface::class);
$platform = $this->createMock(PlatformInterface::class);

$callCount = 0;

$this->driver->expects($this->exactly(2))
->method('reset')
->willReturnCallback(function (string $key, RateLimitActionInterface $act, PlatformInterface $plt) use (&$callCount) {
$callCount++;
if ($callCount === 1) {
$this->assertSame('global', $act->value());
return true;
}
return true;
});

$this->assertTrue($this->resolver->reset('key', $action, $platform));
}

public function testStatusProxiesDriverAndEnrichesSource(): void
{
$action = $this->createMock(RateLimitActionInterface::class);
$platform = $this->createMock(PlatformInterface::class);

$this->driver->expects($this->once())
->method('status')
->willReturn(new RateLimitStatusDTO(10, 5, 60));

$status = $this->resolver->status('key', $action, $platform);

$this->assertSame('action', $status->source);
}
}
64 changes: 64 additions & 0 deletions tests/Phase5/BackoffPolicyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Maatify\RateLimiter\Tests\Phase5;

use Maatify\RateLimiter\DTO\RateLimitStatusDTO;
use Maatify\RateLimiter\Resolver\ExponentialBackoffPolicy;
use PHPUnit\Framework\TestCase;

final class BackoffPolicyTest extends TestCase
{
public function testBackoffGrowsExponentially(): void
{
$policy = new ExponentialBackoffPolicy(base: 2);

// Limit 10, remaining -1 -> over 1 -> 2^1 = 2
$status1 = new RateLimitStatusDTO(10, -1, 60);
$this->assertSame(2, $policy->calculateDelay($status1));

// Limit 10, remaining -2 -> over 2 -> 2^2 = 4
$status2 = new RateLimitStatusDTO(10, -2, 60);
$this->assertSame(4, $policy->calculateDelay($status2));

// Limit 10, remaining -3 -> over 3 -> 2^3 = 8
$status3 = new RateLimitStatusDTO(10, -3, 60);
$this->assertSame(8, $policy->calculateDelay($status3));
}

public function testBackoffCappedByResetWindow(): void
{
$policy = new ExponentialBackoffPolicy(base: 2);

// Limit 10, remaining -10 -> over 10 -> 2^10 = 1024
// Reset after 60
// Should be capped at 60
$status = new RateLimitStatusDTO(10, -10, 60);
$this->assertSame(60, $policy->calculateDelay($status));
}

public function testBackoffCappedByMaxDelay(): void
{
$policy = new ExponentialBackoffPolicy(base: 2, maxDelay: 100);

// Limit 10, remaining -10 -> over 10 -> 2^10 = 1024
// Reset after 2000
// Should be capped at 100
$status = new RateLimitStatusDTO(10, -10, 2000);
$this->assertSame(100, $policy->calculateDelay($status));
}

public function testZeroDelayIfNotOverLimit(): void
{
$policy = new ExponentialBackoffPolicy(base: 2);

// Remaining 0 -> over 0 -> delay 0
$status = new RateLimitStatusDTO(10, 0, 60);
$this->assertSame(0, $policy->calculateDelay($status));

// Remaining 5 -> over 0 -> delay 0
$status2 = new RateLimitStatusDTO(10, 5, 60);
$this->assertSame(0, $policy->calculateDelay($status2));
}
}
37 changes: 37 additions & 0 deletions tests/Phase5/ExceptionPropagationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Maatify\RateLimiter\Tests\Phase5;

use Maatify\RateLimiter\DTO\RateLimitStatusDTO;
use Maatify\RateLimiter\Exceptions\TooManyRequestsException;
use PHPUnit\Framework\TestCase;

final class ExceptionPropagationTest extends TestCase
{
public function testExceptionCarriesDTO(): void
{
$dto = new RateLimitStatusDTO(10, 0, 60);
$exception = new TooManyRequestsException('Error', 429, $dto);

$this->assertSame($dto, $exception->status);
}

public function testExceptionExposesRetryAfter(): void
{
$dto = new RateLimitStatusDTO(10, 0, 60, 30);
$exception = new TooManyRequestsException('Error', 429, $dto);

$this->assertSame(30, $exception->getRetryAfter());
}

public function testExceptionExposesNextAllowedAt(): void
{
$timestamp = '2025-01-01 12:00:00';
$dto = new RateLimitStatusDTO(10, 0, 60, 30, true, null, $timestamp);
$exception = new TooManyRequestsException('Error', 429, $dto);

$this->assertSame($timestamp, $exception->getNextAllowedAt());
}
}
Loading