Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\RateLimiter;
use Tests\TestCase;
/**
* Public PDFs are rate limited per IP (RouteServiceProvider: 'pdf').
*
* On 2026-10-07 a crawler fetched them ~11 times a second; every one was
* rendered from scratch, the server ran out of memory and the kernel killed
* MariaDB. A limited request must be refused before any PDF work starts.
*/
class PdfRateLimitTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
// Not the Redis store phpunit.xml configures: that is the dev site's
// cache, and the limiter's counters would land there.
config(['cache.default' => 'array', 'cache.limiter' => 'array']);
// The limiter was built at boot, on the Redis store, before the line
// above ran: rebuild it, or its counters still land in the dev cache
// (and survive into the next run). Its named limits live on the
// instance, so they are registered again - without them 'pdf' reads
// as a limit of 0 and every second request is refused.
$this->app->forgetInstance(\Illuminate\Cache\RateLimiter::class);
RateLimiter::clearResolvedInstances();
$this->app->getProvider(\App\Providers\RouteServiceProvider::class)->configureRateLimiting();
}
public function test_the_thirty_first_pdf_in_a_minute_is_refused(): void
{
// An id that does not exist: the limit counts requests, not
// successes, and nothing is rendered.
$url = route('call.pdf-a4-bw', ['id' => 999999999]);
for ($i = 1; $i <= 30; $i++) {
$this->assertNotSame(429, $this->get($url)->status(), "Request $i is within the limit.");
}
$refused = $this->get($url);
$refused->assertStatus(429);
$this->assertNotNull($refused->headers->get('Retry-After'));
$this->assertSame('30', (string) $refused->headers->get('X-RateLimit-Limit'));
}
public function test_every_pdf_route_shares_one_allowance(): void
{
// Switching format must not open a fresh allowance: the crawler
// fetched every variant of every call.
$routes = ['call.pdf', 'call.pdf-bw', 'call.pdf-a4', 'call.pdf-a4-bw', 'post.pdf'];
for ($i = 0; $i < 30; $i++) {
$this->get(route($routes[$i % count($routes)], ['id' => 999999999]));
}
$this->get(route('post.pdf', ['id' => 999999999]))->assertStatus(429);
$this->get(route('call.pdf-bulk'))->assertStatus(429);
$this->get(route('post.pdf-bulk'))->assertStatus(429);
}
public function test_other_ips_keep_their_own_allowance(): void
{
$url = route('post.pdf', ['id' => 999999999]);
for ($i = 0; $i < 31; $i++) {
$this->withServerVariables(['REMOTE_ADDR' => '203.0.113.1'])->get($url);
}
// The first IP is now refused...
$this->withServerVariables(['REMOTE_ADDR' => '203.0.113.1'])->get($url)->assertStatus(429);
// ...and a second one, behind its own limit, is not.
$this->assertNotSame(
429,
$this->withServerVariables(['REMOTE_ADDR' => '203.0.113.2'])->get($url)->status()
);
}
public function test_search_has_its_own_limit_of_thirty_a_minute(): void
{
$route = app('router')->getRoutes()->getByName('search.show');
$this->assertContains('throttle:search', $route->gatherMiddleware());
$limit = RateLimiter::limiter('search')(request());
$this->assertSame(30, $limit->maxAttempts);
}
}