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\Models\OAuth;
7
8use App\Events\UserSessionEvent;
9use App\Exceptions\InvalidScopeException;
10use App\Models\OAuth\Client;
11use App\Models\OAuth\Token;
12use App\Models\User;
13use Database\Factories\OAuth\RefreshTokenFactory;
14use Illuminate\Support\Facades\Event;
15use Laravel\Passport\Passport;
16use Tests\TestCase;
17
18class TokenTest extends TestCase
19{
20 public function testAuthCodeChatWriteAllowsSelf()
21 {
22 $user = User::factory()->create();
23 $client = Client::factory()->create(['user_id' => $user]);
24
25 $token = $this->createToken($user, ['chat.write'], $client);
26 $this->actAsUserWithToken($token);
27
28 $this->assertTrue($user->is($token->getResourceOwner()));
29 $this->assertTrue($user->is(auth()->user()));
30 }
31
32 /**
33 * @dataProvider authCodeChatWriteRequiresBotGroupDataProvider
34 */
35 public function testAuthCodeChatWriteRequiresBotGroup(?string $group, ?string $expectedException)
36 {
37 $user = User::factory()->withGroup($group)->create();
38 $client = Client::factory()->create(['user_id' => $user]);
39 $tokenUser = User::factory()->create();
40
41 if ($expectedException !== null) {
42 $this->expectException($expectedException);
43 } else {
44 $this->expectNotToPerformAssertions();
45 }
46
47 $this->createToken($tokenUser, ['chat.write'], $client);
48 }
49
50 public function testClientCredentialsRequiredForDelegation()
51 {
52 $user = User::factory()->create();
53 $client = Client::factory()->create(['user_id' => $user]);
54
55 $this->expectException(InvalidScopeException::class);
56 $this->createToken($user, ['delegate'], $client);
57 }
58
59 public function testClientCredentialResourceOwnerBot()
60 {
61 $user = User::factory()->withGroup('bot')->create();
62 $client = Client::factory()->create(['user_id' => $user]);
63 $token = $this->createToken(null, ['delegate'], $client);
64
65 $this->actAsUserWithToken($token);
66
67 $this->assertTrue($token->isClientCredentials());
68 $this->assertTrue($user->is($token->getResourceOwner()));
69 $this->assertTrue($user->is(auth()->user()));
70 }
71
72 public function testClientCredentialResourceOwnerPublic()
73 {
74 $user = User::factory()->withGroup('bot')->create();
75 $client = Client::factory()->create(['user_id' => $user]);
76 $token = $this->createToken(null, ['public'], $client);
77
78 $this->actAsUserWithToken($token);
79
80 $this->assertTrue($token->isClientCredentials());
81 $this->assertNull($token->getResourceOwner());
82 $this->assertNull(auth()->user());
83 }
84
85 /**
86 * @dataProvider delegationNotAllowedScopesDataProvider
87 */
88 public function testDelegationNotAllowedScopes(array $scopes)
89 {
90 $user = User::factory()->create();
91 $client = Client::factory()->create(['user_id' => $user]);
92
93 $this->expectException(InvalidScopeException::class);
94 $this->createToken(null, $scopes, $client);
95 }
96
97 /**
98 * @dataProvider delegationRequiredScopesDataProvider
99 */
100 public function testDelegationRequiredScopes(array $scopes, ?string $expectedException)
101 {
102 $user = User::factory()->withGroup('bot')->create();
103 $client = Client::factory()->create(['user_id' => $user]);
104
105 if ($expectedException !== null) {
106 $this->expectException($expectedException);
107 } else {
108 $this->expectNotToPerformAssertions();
109 }
110
111 $this->createToken(null, $scopes, $client);
112 }
113
114 /**
115 * @dataProvider delegationRequiresChatBotDataProvider
116 */
117 public function testDelegationRequiresChatBot(?string $group, ?string $expectedException)
118 {
119 $user = User::factory()->withGroup($group)->create();
120 $client = Client::factory()->create(['user_id' => $user]);
121 $tokenUser = User::factory()->create();
122
123 if ($expectedException !== null) {
124 $this->expectException($expectedException);
125 } else {
126 $this->expectNotToPerformAssertions();
127 }
128
129 $this->createToken(null, ['delegate'], $client);
130 }
131
132 /**
133 * @dataProvider scopesDataProvider
134 *
135 * @return void
136 */
137 public function testScopes($scopes, $expectedException)
138 {
139 $user = User::factory()->create();
140 $client = Client::factory()->create(['user_id' => $user]);
141
142 if ($expectedException !== null) {
143 $this->expectException($expectedException);
144 } else {
145 $this->expectNotToPerformAssertions();
146 }
147
148 $this->createToken($user, $scopes, $client);
149 }
150
151 public function testScopesAreSorted()
152 {
153 $token = new Token();
154 $token->scopes = ['i', 'am', 'a', 'scope'];
155
156 $this->assertSame(['a', 'am', 'i', 'scope'], $token->scopes);
157 }
158
159 /**
160 * @dataProvider scopesClientCredentialsDataProvider
161 *
162 * @return void
163 */
164 public function testScopesClientCredentials($scopes, $expectedException)
165 {
166 $user = User::factory()->create();
167 $client = Client::factory()->create(['user_id' => $user]);
168
169 if ($expectedException !== null) {
170 $this->expectException($expectedException);
171 } else {
172 $this->expectNotToPerformAssertions();
173 }
174
175 $this->createToken(null, $scopes, $client);
176 }
177
178 public function testRevokeRecursive()
179 {
180 Event::fake();
181
182 $refreshToken = (new RefreshTokenFactory())->create();
183 $token = $refreshToken->accessToken;
184
185 $this->assertFalse($refreshToken->revoked);
186 $this->assertFalse($token->revoked);
187
188 $token->revokeRecursive();
189
190 $this->assertTrue($refreshToken->fresh()->revoked);
191 $this->assertTrue($token->fresh()->revoked);
192 Event::assertDispatched(UserSessionEvent::class, fn (UserSessionEvent $event) => $event->action === 'logout');
193 }
194
195 public static function authCodeChatWriteRequiresBotGroupDataProvider()
196 {
197 return [
198 [null, InvalidScopeException::class],
199 ['admin', InvalidScopeException::class],
200 ['bng', InvalidScopeException::class],
201 ['bot', null],
202 ['gmt', InvalidScopeException::class],
203 ['nat', InvalidScopeException::class],
204 ];
205 }
206
207 public static function delegationNotAllowedScopesDataProvider()
208 {
209 return Passport::scopes()
210 ->pluck('id')
211 ->filter(fn ($id) => !in_array($id, ['chat.write', 'delegate'], true))
212 ->map(fn ($id) => [['delegate', $id]])
213 ->values();
214 }
215
216 public static function delegationRequiredScopesDataProvider()
217 {
218 return [
219 'chat.write requires delegation' => [['chat.write'], InvalidScopeException::class],
220 'chat.write delegation' => [['chat.write', 'delegate'], null],
221 ];
222 }
223
224 public static function delegationRequiresChatBotDataProvider()
225 {
226 return [
227 [null, InvalidScopeException::class],
228 ['admin', InvalidScopeException::class],
229 ['bng', InvalidScopeException::class],
230 ['bot', null],
231 ['gmt', InvalidScopeException::class],
232 ['nat', InvalidScopeException::class],
233 ];
234 }
235
236 public static function scopesDataProvider()
237 {
238 return [
239 'null is not a valid scope' => [null, InvalidScopeException::class],
240 'empty scope should fail' => [[], InvalidScopeException::class],
241 'all scope is allowed' => [['*'], null],
242 ];
243 }
244
245 public static function scopesClientCredentialsDataProvider()
246 {
247 return [
248 'null is not a valid scope' => [null, InvalidScopeException::class],
249 'empty scope should fail' => [[], InvalidScopeException::class],
250 'all scope is not allowed' => [['*'], InvalidScopeException::class],
251 ];
252 }
253}