166 lines
5.7 KiB
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); |