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\Models;
9
10use App\Libraries\Session\Store;
11use App\Models\OAuth\Token;
12use App\Models\User;
13use App\Models\UsernameChangeHistory;
14use Carbon\CarbonImmutable;
15use Database\Factories\OAuth\RefreshTokenFactory;
16use Tests\TestCase;
17
18class UserTest extends TestCase
19{
20 public static function dataProviderForAttributeTwitter(): array
21 {
22 return [
23 ['@hello', 'hello'],
24 ['hello', 'hello'],
25 ['@', null],
26 ['', null],
27 [null, null],
28 ];
29 }
30
31 public static function dataProviderForUsernameChangeCost()
32 {
33 return [
34 [0, 0],
35 [1, 8],
36 [2, 16],
37 [3, 32],
38 [4, 64],
39 [5, 100],
40 [6, 100],
41 [10, 100],
42 ];
43 }
44
45 public static function dataProviderForUsernameChangeCostType()
46 {
47 return [
48 ['admin', 0],
49 ['inactive', 0],
50 ['paid', 8],
51 ['revert', 0],
52 ['support', 8],
53 ];
54 }
55
56 public static function dataProviderForUsernameChangeCostWindow()
57 {
58 // years with no name changes, cost
59 // test is setup with name change every 6 months.
60 return [
61 [0, 100],
62 [1, 32],
63 [2, 8],
64 [3, 8],
65 [4, 8],
66 ];
67 }
68
69 public static function dataProviderValidDiscordUsername(): array
70 {
71 return [
72 ['username', true],
73 ['user_name', true],
74 ['user.name', true],
75 ['user2name', true],
76 ['u_sernam.e1337', true],
77 ['username#', false],
78 ['u', false],
79 ['morethan32characterinthisusername', false], // 33 characters
80
81 // old format
82 ['username#1337', true],
83 ['ユーザー名#1337', true],
84 ['username#1', false],
85 ['username#13bb', false],
86 ['username#abcd', false],
87 ['user@name#1337', false],
88 ['user#name#1337', false],
89 ['user:name#1337', false],
90 ];
91 }
92
93 /**
94 * @dataProvider dataProviderForAttributeTwitter
95 */
96 public function testAttributeTwitter($setValue, $getValue)
97 {
98 $user = new User(['user_twitter' => $setValue]);
99
100 $this->assertSame($getValue, $user->user_twitter);
101 }
102
103 public function testEmailLoginDisabled()
104 {
105 config_set('osu.user.allow_email_login', false);
106 User::factory()->create([
107 'username' => 'test',
108 'user_email' => 'test@example.org',
109 ]);
110
111 $this->assertNull(User::findForLogin('test@example.org'));
112 }
113
114 public function testEmailLoginEnabled()
115 {
116 config_set('osu.user.allow_email_login', true);
117 $user = User::factory()->create([
118 'username' => 'test',
119 'user_email' => 'test@example.org',
120 ]);
121
122 $this->assertTrue($user->is(User::findForLogin('test@example.org')));
123 }
124
125 public function testResetSessions(): void
126 {
127 $user = User::factory()->create();
128
129 // create session
130 $this->post(route('login'), ['username' => $user->username, 'password' => User::factory()::DEFAULT_PASSWORD]);
131 // sanity check
132 $this->assertNotEmpty(Store::ids($user->getKey()));
133
134 // create token
135 $token = Token::factory()->create(['user_id' => $user, 'revoked' => false]);
136 $refreshToken = (new RefreshTokenFactory())->create(['access_token_id' => $token, 'revoked' => false]);
137
138 $user->resetSessions();
139
140 $this->assertEmpty(Store::ids($user->getKey()));
141 $this->assertTrue($token->fresh()->revoked);
142 $this->assertTrue($refreshToken->fresh()->revoked);
143 }
144
145 public function testUsernameAvailableAtForDefaultGroup()
146 {
147 config_set('osu.user.allowed_rename_groups', ['default']);
148 $allowedAtUpTo = now()->addYears(5);
149 $user = User::factory()->withGroup('default')->create();
150
151 $this->assertLessThanOrEqual($allowedAtUpTo, $user->getUsernameAvailableAt());
152 }
153
154 public function testUsernameAvailableAtForNonDefaultGroup()
155 {
156 config_set('osu.user.allowed_rename_groups', ['default']);
157 $allowedAt = now()->addYears(10);
158 $user = User::factory()->withGroup('gmt')->create(['group_id' => app('groups')->byIdentifier('default')]);
159
160 $this->assertGreaterThanOrEqual($allowedAt, $user->getUsernameAvailableAt());
161 }
162
163 /**
164 * @dataProvider dataProviderForUsernameChangeCost
165 */
166 public function testUsernameChangeCost(int $changes, int $cost)
167 {
168 $user = User::factory()
169 ->has(UsernameChangeHistory::factory()->count($changes))
170 ->create();
171
172 $this->assertSame($cost, $user->usernameChangeCost());
173 }
174
175 public function testUsernameChangeCostMultiple()
176 {
177 $user = User::factory()->create();
178
179 $this->assertSame(0, $user->usernameChangeCost());
180
181 $user->usernameChangeHistory()->create([
182 'timestamp' => CarbonImmutable::now(),
183 'type' => 'paid',
184 'username' => 'marty',
185 ]);
186
187 // 1 change in last 3 years
188 $this->travelTo(CarbonImmutable::now()->addYears(3));
189 $this->assertSame(8, $user->usernameChangeCost());
190
191 // 0 changes in last 3 years
192 $this->travelTo(CarbonImmutable::now()->addYears(1));
193 $this->assertSame(8, $user->usernameChangeCost());
194
195 $user->usernameChangeHistory()->create([
196 'timestamp' => CarbonImmutable::now(),
197 'type' => 'paid',
198 'username' => 'mcfly',
199 ]);
200
201 // 1 change in last 3 years
202 $this->assertSame(8, $user->usernameChangeCost());
203
204 $user->usernameChangeHistory()->create([
205 'timestamp' => CarbonImmutable::now(),
206 'type' => 'paid',
207 'username' => 'futuremarty',
208 ]);
209
210 // 2 changes in last 3 years
211 $this->assertSame(16, $user->usernameChangeCost());
212
213 // 1 changes in last 3 years
214 $this->travelTo(CarbonImmutable::now()->addYears(3));
215 $this->assertSame(8, $user->usernameChangeCost());
216 // 0 changes in last 3 years
217 $this->travelTo(CarbonImmutable::now()->addYears(1));
218 $this->assertSame(8, $user->usernameChangeCost());
219 }
220
221 /**
222 * @dataProvider dataProviderForUsernameChangeCostType
223 */
224 public function testUsernameChangeCostType(string $type, int $cost)
225 {
226 $user = User::factory()
227 ->has(UsernameChangeHistory::factory()->state(['type' => $type]))
228 ->create();
229
230 $this->assertSame($cost, $user->usernameChangeCost());
231 }
232
233 /**
234 * @dataProvider dataProviderForUsernameChangeCostWindow
235 */
236 public function testUsernameChangeCostWindow(int $years, int $cost)
237 {
238 $now = CarbonImmutable::now();
239 $this->travelTo(CarbonImmutable::now()->subYears(3));
240
241 $user = User::factory()->create();
242 while (CarbonImmutable::now()->isBefore($now)) {
243 $user->usernameChangeHistory()->create([
244 'timestamp' => CarbonImmutable::now(),
245 'type' => 'paid',
246 'username' => 'marty',
247 ]);
248
249 $this->travelTo(CarbonImmutable::now()->addMonths(6));
250 }
251
252 $this->travelTo($now->addYears($years));
253 $this->assertSame($cost, $user->usernameChangeCost());
254 }
255
256 /**
257 * @dataProvider dataProviderValidDiscordUsername
258 */
259 public function testValidDiscordUsername(string $username, bool $valid)
260 {
261 $user = User::factory()->make();
262 $user->user_discord = $username;
263
264 $this->assertSame($valid, $user->isValid());
265
266 if (!$valid) {
267 $this->assertArrayHasKey('user_discord', $user->validationErrors()->all());
268 }
269 }
270}