1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace Tests\Middleware;
9
10use App\Http\Middleware\RequestCost;
11use App\Models\OAuth\Token;
12use App\Models\User;
13use Closure;
14use LaravelRedis;
15use Route;
16use Tests\TestCase;
17
18class ThrottleRequestsTest extends TestCase
19{
20 const LIMIT = 60;
21
22 protected Token $token;
23
24 /**
25 * @dataProvider throttleDataProvider
26 */
27 public function testThrottle(array $middlewares, int $remaining, ?Closure $action = null)
28 {
29 $action ??= (fn () => []);
30
31 Route::get('api/test-throttle', $action)->middleware(['api', 'require-scopes'])->middleware($middlewares);
32
33 $this->getJson('api/test-throttle')
34 ->assertHeader('X-Ratelimit-Limit', static::LIMIT)
35 ->assertHeader('X-Ratelimit-Remaining', $remaining);
36 }
37
38 public function testThrottleMultipleRequests()
39 {
40 Route::get('api/test-throttle', fn () => [])->middleware(['api', 'require-scopes'])->middleware('throttle:60,10');
41
42 $this->getJson('api/test-throttle');
43 $this->getJson('api/test-throttle')
44 ->assertHeader('X-Ratelimit-Limit', static::LIMIT)
45 ->assertHeader('X-Ratelimit-Remaining', 58);
46 }
47
48 public static function throttleDataProvider()
49 {
50 return [
51 'throttle' => [['throttle:60,10'], 59],
52 'request-cost specified' => [['request-cost:5', 'throttle:60,10'], 55],
53 'request-cost after throttle order does not matter' => [['throttle:60,10', 'request-cost:5'], 55],
54 'setCost' => [['throttle:60,10'], 58, fn () => RequestCost::setCost(2)],
55 'setCost overrides default' => [['throttle:60,10', 'request-cost:5'], 58, fn () => RequestCost::setCost(2)],
56 ];
57 }
58
59 protected function setUp(): void
60 {
61 parent::setUp();
62
63 // Using token so we can get the key name and remove the keys from redis on cleanup.
64 $this->token = $this->createToken(User::factory()->create(), ['*']);
65 $this->actingWithToken($this->token);
66 }
67
68 protected function tearDown(): void
69 {
70 if ($GLOBALS['cfg']['cache']['default'] === 'redis') {
71 $key = $GLOBALS['cfg']['cache']['prefix'].':'.sha1($this->token->getKey());
72 LaravelRedis::del($key);
73 LaravelRedis::del("{$key}:timer");
74 }
75
76 parent::tearDown();
77 }
78}