Files
SkinbaseNova/tests/Unit/ForumRateLimitMiddlewareTest.php

166 lines
5.7 KiB
PHP

<?php
use App\Http\Middleware\ForumRateLimitMiddleware;
use cPad\Plugins\Forum\Services\Security\BotProtectionService;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Symfony\Component\HttpFoundation\Response;
uses(Tests\TestCase::class);
it('reports forum throttle violations to bot protection before rethrowing', function () {
$request = Request::create('/forum/topic/example-topic/reply', 'POST');
$request->setRouteResolver(static fn (): object => new class {
public function getName(): string
{
return 'forum.topic.reply';
}
});
$throttle = \Mockery::mock(ThrottleRequests::class);
$botProtection = \Mockery::mock(BotProtectionService::class);
$exception = new ThrottleRequestsException('Too Many Attempts.', null, [
'Retry-After' => '42',
'X-RateLimit-Limit' => '3',
'X-RateLimit-Remaining' => '0',
]);
$throttle->shouldReceive('handle')
->once()
->with($request, \Mockery::type(Closure::class), 'forum-post-write')
->andThrow($exception);
$botProtection->shouldReceive('recordRateLimitViolation')
->once()
->with(
$request,
'forum_reply_create',
\Mockery::on(static function (array $context): bool {
return $context['limiter'] === 'forum-post-write'
&& $context['bucket'] === 'minute'
&& $context['max_attempts'] === 3
&& $context['retry_after'] === 42;
})
);
$middleware = new ForumRateLimitMiddleware($throttle, $botProtection);
$next = static fn (): Response => response('ok');
$middleware->handle($request, $next);
})->throws(ThrottleRequestsException::class);
it('classifies forum hourly limiter violations using the actual limit bucket', function () {
$request = Request::create('/forum/topic/example-topic/reply', 'POST');
$request->setRouteResolver(static fn (): object => new class {
public function getName(): string
{
return 'forum.topic.reply';
}
});
$throttle = \Mockery::mock(ThrottleRequests::class);
$botProtection = \Mockery::mock(BotProtectionService::class);
$exception = new ThrottleRequestsException('Too Many Attempts.', null, [
'Retry-After' => '120',
'X-RateLimit-Limit' => '10',
'X-RateLimit-Remaining' => '0',
]);
$throttle->shouldReceive('handle')
->once()
->with($request, \Mockery::type(Closure::class), 'forum-post-write')
->andThrow($exception);
$botProtection->shouldReceive('recordRateLimitViolation')
->once()
->with(
$request,
'forum_reply_create',
\Mockery::on(static function (array $context): bool {
return $context['bucket'] === 'hour'
&& $context['max_attempts'] === 10
&& $context['retry_after'] === 120;
})
);
$middleware = new ForumRateLimitMiddleware($throttle, $botProtection);
$next = static fn (): Response => response('ok');
$middleware->handle($request, $next);
})->throws(ThrottleRequestsException::class);
it('classifies thread creation minute and hour limiter buckets correctly', function () {
$request = Request::create('/forum/example-board/new', 'POST');
$request->setRouteResolver(static fn (): object => new class {
public function getName(): string
{
return 'forum.topic.store';
}
});
$throttle = \Mockery::mock(ThrottleRequests::class);
$botProtection = \Mockery::mock(BotProtectionService::class);
$minuteException = new ThrottleRequestsException('Too Many Attempts.', null, [
'Retry-After' => '30',
'X-RateLimit-Limit' => '3',
'X-RateLimit-Remaining' => '0',
]);
$hourException = new ThrottleRequestsException('Too Many Attempts.', null, [
'Retry-After' => '120',
'X-RateLimit-Limit' => '10',
'X-RateLimit-Remaining' => '0',
]);
$throttle->shouldReceive('handle')
->once()
->with($request, \Mockery::type(Closure::class), 'forum-thread-create')
->andThrow($minuteException);
$throttle->shouldReceive('handle')
->once()
->with($request, \Mockery::type(Closure::class), 'forum-thread-create')
->andThrow($hourException);
$botProtection->shouldReceive('recordRateLimitViolation')
->once()
->with(
$request,
'forum_topic_create',
\Mockery::on(static function (array $context): bool {
return $context['limiter'] === 'forum-thread-create'
&& $context['bucket'] === 'minute'
&& $context['max_attempts'] === 3
&& $context['retry_after'] === 30;
})
);
$botProtection->shouldReceive('recordRateLimitViolation')
->once()
->with(
$request,
'forum_topic_create',
\Mockery::on(static function (array $context): bool {
return $context['limiter'] === 'forum-thread-create'
&& $context['bucket'] === 'hour'
&& $context['max_attempts'] === 10
&& $context['retry_after'] === 120;
})
);
$middleware = new ForumRateLimitMiddleware($throttle, $botProtection);
$next = static fn (): Response => response('ok');
try {
$middleware->handle($request, $next);
} catch (ThrottleRequestsException) {
}
$middleware->handle($request, $next);
})->throws(ThrottleRequestsException::class);