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\Libraries\SessionVerification;
9
10use App\Libraries\Session\Store as SessionStore;
11use App\Libraries\SessionVerification;
12use App\Mail\UserVerification as UserVerificationMail;
13use App\Models\LoginAttempt;
14use App\Models\OAuth\Client;
15use App\Models\OAuth\Token;
16use App\Models\User;
17use Tests\TestCase;
18
19class ControllerTest extends TestCase
20{
21 public function testIssue(): void
22 {
23 \Mail::fake();
24 $user = User::factory()->create();
25
26 $this
27 ->be($user)
28 ->get(route('account.edit'))
29 ->assertStatus(401)
30 ->assertViewIs('users.verify');
31
32 $record = LoginAttempt::find('127.0.0.1');
33
34 \Mail::assertQueued(UserVerificationMail::class, 1);
35 $this->assertTrue($record->containsUser($user, 'verify'));
36 $this->assertFalse(\Session::isVerified());
37 }
38
39 public function testReissue(): void
40 {
41 \Mail::fake();
42 $user = User::factory()->create();
43 $session = \Session::instance();
44
45 $this
46 ->be($user)
47 ->withPersistentSession($session)
48 ->get(route('account.edit'));
49
50 $state = SessionVerification\State::fromSession($session);
51
52 $this
53 ->withPersistentSession($session)
54 ->post(route('account.reissue-code'))
55 ->assertSuccessful();
56
57 \Mail::assertQueued(UserVerificationMail::class, 2);
58 $this->assertNotSame($state->key, SessionVerification\State::fromSession($session)->key);
59 }
60
61 public function testReissueOAuthVerified(): void
62 {
63 \Mail::fake();
64 $token = Token::factory()->create(['verified' => true]);
65
66 $this
67 ->actingWithToken($token)
68 ->post(route('api.verify.reissue'))
69 ->assertStatus(422);
70
71 \Mail::assertNotQueued(UserVerificationMail::class);
72 $this->assertNull(SessionVerification\State::fromSession($token));
73 }
74
75 public function testReissueVerified(): void
76 {
77 \Mail::fake();
78 $user = User::factory()->create();
79 $session = \Session::instance();
80 $session->markVerified();
81
82 $this
83 ->be($user)
84 ->withPersistentSession($session)
85 ->post(route('account.reissue-code'))
86 ->assertStatus(422);
87
88 \Mail::assertNotQueued(UserVerificationMail::class);
89 $this->assertNull(SessionVerification\State::fromSession($session));
90 }
91
92 public function testVerify(): void
93 {
94 $user = User::factory()->create();
95 $session = \Session::instance();
96
97 $this
98 ->be($user)
99 ->withPersistentSession($session)
100 ->get(route('account.edit'))
101 ->assertStatus(401)
102 ->assertViewIs('users.verify');
103
104 $key = SessionVerification\State::fromSession($session)->key;
105
106 $this
107 ->withPersistentSession($session)
108 ->post(route('account.verify'), ['verification_key' => $key])
109 ->assertSuccessful();
110
111 $record = LoginAttempt::find('127.0.0.1');
112
113 $this->assertFalse($record->containsUser($user, 'verify-mismatch:'));
114 $this->assertTrue($session->isVerified());
115 $this->assertNull(SessionVerification\State::fromSession($session));
116 }
117
118 public function testVerifyLink(): void
119 {
120 $user = User::factory()->create();
121 $session = \Session::instance();
122 $sessionId = $session->getId();
123
124 $this
125 ->be($user)
126 ->withPersistentSession($session)
127 ->get(route('account.edit'))
128 ->assertStatus(401)
129 ->assertViewIs('users.verify');
130
131 $linkKey = SessionVerification\State::fromSession($session)->linkKey;
132
133 $guestSession = SessionStore::findOrNew();
134 $this
135 ->withPersistentSession($guestSession)
136 ->get(route('account.verify', ['key' => $linkKey]))
137 ->assertSuccessful();
138
139 $record = LoginAttempt::find('127.0.0.1');
140
141 $this->assertFalse($record->containsUser($user, 'verify-mismatch:'));
142 $this->assertTrue(SessionStore::findOrNew($sessionId)->isVerified());
143 }
144
145 public function testVerifyLinkMismatch(): void
146 {
147 $user = User::factory()->create();
148 $session = \Session::instance();
149 $sessionId = $session->getId();
150
151 $this
152 ->be($user)
153 ->withPersistentSession($session)
154 ->get(route('account.edit'))
155 ->assertStatus(401)
156 ->assertViewIs('users.verify');
157
158 $guestSession = SessionStore::findOrNew();
159 $this
160 ->withPersistentSession($guestSession)
161 ->get(route('account.verify', ['key' => 'invalid']))
162 ->assertStatus(404);
163
164 $this->assertFalse(SessionStore::findOrNew($sessionId)->isVerified());
165 }
166
167 public function testVerifyLinkOAuth(): void
168 {
169 $token = Token::factory()->create([
170 'client_id' => Client::factory()->create(['password_client' => true]),
171 'verified' => false,
172 ]);
173
174 $this
175 ->actingWithToken($token)
176 ->get(route('api.me'))
177 ->assertSuccessful();
178
179 $linkKey = SessionVerification\State::fromSession($token)->linkKey;
180
181 \Auth::logout();
182 $this
183 ->withPersistentSession(SessionStore::findOrNew())
184 ->get(route('account.verify', ['key' => $linkKey]))
185 ->assertSuccessful();
186
187 $record = LoginAttempt::find('127.0.0.1');
188
189 $this->assertFalse($record->containsUser($token->user, 'verify-mismatch:'));
190 $this->assertTrue($token->fresh()->isVerified());
191 }
192
193 public function testVerifyMismatch(): void
194 {
195 $user = User::factory()->create();
196 $session = \Session::instance();
197
198 $this
199 ->be($user)
200 ->withPersistentSession($session)
201 ->get(route('account.edit'))
202 ->assertStatus(401)
203 ->assertViewIs('users.verify');
204
205 $record = LoginAttempt::find('127.0.0.1');
206 $this->assertFalse($record->containsUser($user, 'verify-mismatch:'));
207
208 $this
209 ->withPersistentSession($session)
210 ->post(route('account.verify'), ['verification_key' => 'invalid'])
211 ->assertStatus(422);
212
213 $record = LoginAttempt::find('127.0.0.1');
214
215 $this->assertTrue($record->containsUser($user, 'verify-mismatch:'));
216 $this->assertFalse($session->isVerified());
217 }
218
219 public function testVerifyOAuth(): void
220 {
221 $token = Token::factory()->create([
222 'client_id' => Client::factory()->create(['password_client' => true]),
223 'verified' => false,
224 ]);
225
226 $this
227 ->actingWithToken($token)
228 ->get(route('api.me'))
229 ->assertSuccessful();
230
231 $key = SessionVerification\State::fromSession($token)->key;
232
233 $this
234 ->actingWithToken($token)
235 ->post(route('api.verify', ['verification_key' => $key]))
236 ->assertSuccessful();
237
238 $record = LoginAttempt::find('127.0.0.1');
239
240 $this->assertFalse($record->containsUser($token->user, 'verify-mismatch:'));
241 $this->assertTrue($token->fresh()->isVerified());
242 }
243
244 public function testVerifyOAuthVerified(): void
245 {
246 \Mail::fake();
247 $token = Token::factory()->create(['verified' => true]);
248
249 $this
250 ->actingWithToken($token)
251 ->post(route('api.verify', ['verification_key' => 'invalid']))
252 ->assertSuccessful();
253
254 $this->assertNull(SessionVerification\State::fromSession($token));
255 \Mail::assertNotQueued(UserVerificationMail::class);
256 }
257
258 public function testVerifyVerified(): void
259 {
260 \Mail::fake();
261 $user = User::factory()->create();
262 $session = \Session::instance();
263 $session->markVerified();
264
265 $this
266 ->be($user)
267 ->withPersistentSession($session)
268 ->post(route('account.verify'), ['verification_key' => 'invalid'])
269 ->assertSuccessful();
270
271 $this->assertNull(SessionVerification\State::fromSession($session));
272 \Mail::assertNotQueued(UserVerificationMail::class);
273 }
274}