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\Models\User;
10use Illuminate\Routing\Route;
11use Laravel\Passport\Exceptions\MissingScopeException;
12use Request;
13use Tests\TestCase;
14
15class RequireScopesTest extends TestCase
16{
17 protected $next;
18 protected $request;
19 protected $user;
20
21 /**
22 * @dataProvider clientCredentialsTestDataProvider
23 */
24 public function testClientCredentials($scopes, $expectedException)
25 {
26 $this->setRequest(['public']);
27 $this->setUser(null, ['public']);
28
29 app(RequireScopes::class)->handle($this->request, $this->next);
30 $this->assertTrue(oauth_token()->isClientCredentials());
31 }
32
33 public function testClientCredentialsIsGuest()
34 {
35 $this->setRequest(['public']);
36 $this->setUser(null, ['public']);
37
38 app(RequireScopes::class)->handle($this->request, $this->next);
39 $this->assertNull(auth()->user());
40 }
41
42 public function testClientCredentialsWhenAllScopeRequired()
43 {
44 $this->setRequest();
45 $this->setUser(null, ['public']);
46
47 $this->expectException(MissingScopeException::class);
48
49 app(RequireScopes::class)->handle($this->request, $this->next);
50 $this->assertTrue(oauth_token()->isClientCredentials());
51 }
52
53 public function testRequireScopesLayered()
54 {
55 $userScopes = ['identify'];
56 $requireScopes = ['identify'];
57
58 $this->setRequest($requireScopes);
59 $this->setUser($this->user, $userScopes);
60
61 app(RequireScopes::class)->handle($this->request, function () use ($requireScopes) {
62 app(RequireScopes::class)->handle($this->request, $this->next, ...$requireScopes);
63 });
64
65 $this->assertTrue(!oauth_token()->isClientCredentials());
66 }
67
68 public function testRequireScopesLayeredNoPermission()
69 {
70 $userScopes = ['somethingelse'];
71 $requireScopes = ['identify'];
72
73 $this->setRequest($requireScopes);
74 $this->setUser($this->user, $userScopes);
75
76 $this->expectException(MissingScopeException::class);
77 app(RequireScopes::class)->handle($this->request, function () use ($requireScopes) {
78 app(RequireScopes::class)->handle($this->request, $this->next, ...$requireScopes);
79 });
80 }
81
82 public function testRequireScopesSkipped()
83 {
84 $userScopes = ['somethingelse'];
85 $requireScopes = ['identify'];
86
87 $this->setRequest($requireScopes, Request::create('/api/v2/changelog', 'GET'));
88 $this->setUser($this->user, $userScopes);
89
90 app(RequireScopes::class)->handle($this->request, $this->next, ...$requireScopes);
91 $this->assertTrue(!oauth_token()->isClientCredentials());
92 }
93
94 /**
95 * @dataProvider userScopesTestDataProvider
96 */
97 public function testUserScopes($requiredScopes, $userScopes, $expectedException)
98 {
99 $this->setRequest($requiredScopes);
100 $this->setUser($this->user, $userScopes);
101
102 if ($expectedException !== null) {
103 $this->expectException($expectedException);
104 }
105
106 if ($requiredScopes === null) {
107 app(RequireScopes::class)->handle($this->request, $this->next);
108 } else {
109 app(RequireScopes::class)->handle($this->request, $this->next, ...$requiredScopes);
110 }
111
112 $this->assertTrue(!oauth_token()->isClientCredentials());
113 }
114
115 public static function clientCredentialsTestDataProvider()
116 {
117 return [
118 'null is not a valid scope' => [null, MissingScopeException::class],
119 'empty scope should fail' => [[], MissingScopeException::class],
120 'public' => [['public'], null],
121 'all scope is not allowed' => [['*'], MissingScopeException::class],
122 ];
123 }
124
125 public function clientCredentialsTestWhenAllScopeRequiredDataProvider()
126 {
127 return [
128 'null is not a valid scope' => [null, MissingScopeException::class],
129 'empty scope should fail' => [[], MissingScopeException::class],
130 'public' => [['public'], MissingScopeException::class],
131 'all scope is not allowed' => [['*'], MissingScopeException::class],
132 ];
133 }
134
135 public static function userScopesTestDataProvider()
136 {
137 return [
138 'All scopes' => [null, ['*'], null],
139 'Has the required scope' => [['identify'], ['identify'], null],
140 'Does not have the required scope' => [['identify'], ['somethingelse'], MissingScopeException::class],
141 'Requires specific scope and all scope' => [['identify'], ['*'], null],
142 'Requires specific scope and multiple non-matching scopes' => [['identify'], ['somethingelse', 'alsonotright', 'nope'], MissingScopeException::class],
143 'Requires specific scope and multiple scopes' => [['identify'], ['somethingelse', 'identify', 'nope'], null],
144 'Blank require should deny regular scopes' => [null, ['identify'], MissingScopeException::class],
145 ];
146 }
147
148 protected function setRequest(?array $scopes = null, $request = null)
149 {
150 $this->request = $request ?? Request::create('/api/_fake', 'GET');
151
152 $this->next = static function () {
153 // just an empty closure.
154 };
155
156 // so request() works
157 \Request::swap($this->request);
158
159 // set a fake route resolver
160 $this->request->setRouteResolver(function () use ($scopes) {
161 $route = new Route(['GET'], '/api/_fake', null);
162 $route->middleware('require-scopes');
163
164 if ($scopes !== null) {
165 $route->middleware('require-scopes:'.implode(',', $scopes));
166 }
167
168 return $route;
169 });
170 }
171
172 protected function setUp(): void
173 {
174 parent::setUp();
175
176 // nearly all the tests in the class need a user, so might as well set it up here.
177 $this->user = User::factory()->create();
178 }
179
180 protected function setUser(?User $user, ?array $scopes = null, $client = null)
181 {
182 $token = $this->createToken($user, $scopes, $client);
183 $this->actAsUserWithToken($token);
184 }
185}