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
6namespace Tests\Middleware;
7
8use App\Http\Middleware\RequireScopes;
9use App\Libraries\RouteScopesHelper;
10use App\Models\Build;
11use App\Models\Changelog;
12use App\Models\Comment;
13use App\Models\UpdateStream;
14use Route;
15use Tests\TestCase;
16
17class RouteScopesTest extends TestCase
18{
19 private static $expectations;
20
21 /**
22 * @dataProvider routeScopesDataProvider
23 */
24 public function testApiRouteScopes($route)
25 {
26 $this->importExpectations();
27
28 $key = RouteScopesHelper::keyForMethods($route['methods']).'@'.$route['uri'];
29
30 $this->assertSame(static::$expectations[$key], $route, $key);
31 }
32
33 /**
34 * @dataProvider routesDataProvider
35 */
36 public function testUnscopedRequestsRequireAuthentication(string $url, string $method, array $middlewares)
37 {
38 // factory some objects so unauthed endpoints don't 404.
39 // FIXME: only create objects as necessary instead of every run;
40 // This can't be simply setup in setUpBeforeClass() because then we'd need to initialize the app container there,
41 // but the container is overriden on each test and also destroyed before the teardown
42 // making rolling back or cleaning up problematic.
43 $stream = UpdateStream::factory()->create([
44 'name' => '1',
45 'stream_id' => 1, // Changelog stream_id is tinyint, autoincrement makes test fail too soon.
46 ]);
47
48 Changelog::factory()->create([
49 'stream_id' => $stream->getKey(),
50 'user_id' => 1, // user doesn't need to exist and not having to create a user makes the test much faster
51 ]);
52
53 $build = Build::factory()->create([
54 'version' => '1',
55 'stream_id' => $stream->getKey(),
56 ]);
57
58 Comment::factory()->create([
59 'commentable_id' => $build->getKey(),
60 'commentable_type' => 'build',
61 'id' => 1,
62 ]);
63
64 $status = $this->call($method, $url)->getStatusCode();
65
66 if ($method === 'GET' && starts_with(ltrim($url, '/').'/', RequireScopes::NO_TOKEN_REQUIRED)) {
67 $this->assertTrue(in_array($status, [200, 302, 404], true));
68 } elseif (in_array('require-scopes', $middlewares, true)) {
69 $this->assertSame(401, $status);
70 } else {
71 $this->assertNotSame(401, $status);
72 }
73 }
74
75 public static function routesDataProvider()
76 {
77 static::createApp();
78
79 $data = [];
80
81 foreach (Route::getRoutes() as $route) {
82 if (!starts_with($route->uri, 'api/')) {
83 continue;
84 }
85
86 foreach ($route->methods() as $method) {
87 // Only need the url to be valid so they can be routed.
88 $parameters = [];
89 foreach ($route->parameterNames() as $parameterName) {
90 $parameters[$parameterName] = '1';
91 }
92
93 $url = app('url')->toRoute($route, $parameters, false);
94 $middlewares = $route->gatherMiddleware();
95
96 $key = "{$method}@{$route->uri}"; // give data set a name.
97 $data[$key] = [$url, $method, $middlewares];
98 }
99 }
100
101 return $data;
102 }
103
104 public static function routeScopesDataProvider()
105 {
106 static::createApp();
107
108 return array_map(
109 fn ($route) => [$route],
110 (new RouteScopesHelper())->toArray(),
111 );
112 }
113
114 private function importExpectations()
115 {
116 if (static::$expectations !== null) {
117 return;
118 }
119
120 static::$expectations = [];
121
122 $helper = new RouteScopesHelper();
123 $helper->fromJson('tests/api_routes.json');
124 $routes = $helper->routes;
125 foreach ($routes as $route) {
126 $key = RouteScopesHelper::keyForMethods($route['methods']).'@'.$route['uri'];
127 static::$expectations[$key] = $route;
128 }
129 }
130}