the browser-facing portion of osu!
at master 9.3 kB view raw
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\Controllers; 9 10use App\Http\Middleware\ThrottleRequests; 11use App\Libraries\User\PasswordResetData; 12use App\Mail\PasswordReset; 13use App\Models\User; 14use Carbon\CarbonImmutable; 15use Illuminate\Support\Facades\Mail; 16use Tests\TestCase; 17 18class PasswordResetControllerTest extends TestCase 19{ 20 private string $origCacheDefault; 21 22 private static function randomPassword(): string 23 { 24 return str_random(10); 25 } 26 27 public function testCreate() 28 { 29 $user = User::factory()->create(); 30 31 Mail::fake(); 32 $this 33 ->post($this->path(), ['username' => $user->username]) 34 ->assertRedirect(route('password-reset.reset', ['username' => $user->username])); 35 Mail::assertQueued(PasswordReset::class); 36 } 37 38 public function testCreateWithEmail() 39 { 40 $user = User::factory()->create(); 41 42 Mail::fake(); 43 $this 44 ->post($this->path(), ['username' => $user->user_email]) 45 ->assertRedirect(route('password-reset.reset', ['username' => $user->user_email])); 46 Mail::assertQueued(PasswordReset::class); 47 } 48 49 public function testCreateInvalidUser() 50 { 51 Mail::fake(); 52 $this->post($this->path(), ['username' => '_invaliduser'])->assertStatus(422); 53 Mail::assertNothingOutgoing(); 54 } 55 56 public function testCreateSendMailOnce() 57 { 58 $user = User::factory()->create(); 59 60 Mail::fake(); 61 $this->post($this->path(), ['username' => $user->username])->assertRedirect(); 62 $this->post($this->path(), ['username' => $user->username])->assertRedirect(); 63 Mail::assertQueuedCount(1); 64 } 65 66 public function testCreateSendMailByUsernameParameter() 67 { 68 $user = User::factory()->create(); 69 70 Mail::fake(); 71 $this->post($this->path(), ['username' => $user->username])->assertRedirect(); 72 $this->post($this->path(), ['username' => $user->user_email])->assertRedirect(); 73 Mail::assertQueuedCount(2); 74 } 75 76 public function testIndex() 77 { 78 $this->get($this->path())->assertSuccessful(); 79 } 80 81 public function testResendMail() 82 { 83 $user = User::factory()->create(); 84 85 Mail::fake(); 86 $this->generateKey($user); 87 $data = PasswordResetData::find($user, $user->username); 88 $data->attrs['canResendMailAfter'] = 0; 89 $data->save(); 90 91 $this->post(route('password-reset.resend-mail', ['username' => $user->username]))->assertSuccessful(); 92 Mail::assertQueuedCount(2); 93 } 94 95 public function testResendMailDuplicate() 96 { 97 $user = User::factory()->create(); 98 99 Mail::fake(); 100 $this->generateKey($user); 101 102 $this->post(route('password-reset.resend-mail', ['username' => $user->username]))->assertSuccessful(); 103 Mail::assertQueuedCount(1); 104 } 105 106 public function testResendMailNonexistent() 107 { 108 $user = User::factory()->create(); 109 110 Mail::fake(); 111 $this->post(route('password-reset.resend-mail', ['username' => $user->username]))->assertRedirect($this->path()); 112 Mail::assertQueuedCount(0); 113 } 114 115 public function testReset() 116 { 117 $this 118 ->get(route('password-reset.reset', ['username' => 'test'])) 119 ->assertSuccessful(); 120 } 121 122 public function testResetMissingUsername() 123 { 124 $this 125 ->get(route('password-reset.reset')) 126 ->assertStatus(422); 127 } 128 129 public function testUpdate() 130 { 131 $user = User::factory()->create(); 132 133 $key = $this->generateKey($user); 134 135 $newPassword = static::randomPassword(); 136 137 $this->put($this->path(), [ 138 'key' => $key, 139 'user' => [ 140 'password' => $newPassword, 141 'password_confirmation' => $newPassword, 142 ], 143 'username' => $user->username, 144 ])->assertRedirect(route('home')); 145 146 $this->assertTrue($user->fresh()->checkPassword($newPassword)); 147 } 148 149 public function testUpdateChangedEmailExternally() 150 { 151 $password = static::randomPassword(); 152 $user = User::factory()->create(['password' => $password, 'password_confirmation' => $password]); 153 154 $key = $this->generateKey($user); 155 156 $user->update(['user_email' => "new+{$user->user_email}"]); 157 158 $tryNewPassword = static::randomPassword(); 159 160 $this->put($this->path(), [ 161 'key' => $key, 162 'user' => [ 163 'password' => $tryNewPassword, 164 'password_confirmation' => $tryNewPassword, 165 ], 166 'username' => $user->username, 167 ])->assertRedirect($this->path()) 168 ->assertSessionHas('popup', osu_trans('password_reset.error.expired')); 169 170 $this->assertTrue($user->fresh()->checkPassword($password)); 171 } 172 173 public function testUpdateChangedPasswordExternally() 174 { 175 $user = User::factory()->create(); 176 177 $key = $this->generateKey($user); 178 179 $newPassword = static::randomPassword(); 180 181 $user->update([ 182 'password' => $newPassword, 183 'password_confirmation' => $newPassword, 184 ]); 185 186 $tryNewPassword = static::randomPassword(); 187 188 $this->put($this->path(), [ 189 'key' => $key, 190 'user' => [ 191 'password' => $tryNewPassword, 192 'password_confirmation' => $tryNewPassword, 193 ], 194 ])->assertRedirect() 195 ->assertSessionHas('popup', osu_trans('password_reset.error.invalid')); 196 197 $this->assertTrue($user->fresh()->checkPassword($newPassword)); 198 } 199 200 public function testUpdateFromInactive(): void 201 { 202 $changeTime = CarbonImmutable::now()->subMinutes(1); 203 $user = User::factory()->create(['user_lastvisit' => $changeTime->subYears(10)]); 204 205 $key = $this->generateKey($user); 206 207 $newPassword = static::randomPassword(); 208 209 $this->put($this->path(), [ 210 'key' => $key, 211 'user' => [ 212 'password' => $newPassword, 213 'password_confirmation' => $newPassword, 214 ], 215 'username' => $user->username, 216 ])->assertRedirect(route('home')); 217 218 $user = $user->fresh(); 219 $this->assertTrue($user->checkPassword($newPassword)); 220 $this->assertTrue($user->user_lastvisit->greaterThan($changeTime)); 221 } 222 223 public function testUpdateInvalidUsername() 224 { 225 $user = User::factory()->create(); 226 227 $key = $this->generateKey($user); 228 229 $newPassword = static::randomPassword(); 230 231 $this->put($this->path(), [ 232 'key' => $key, 233 'user' => [ 234 'password' => $newPassword, 235 'password_confirmation' => $newPassword, 236 ], 237 'username' => "x{$user->username}", 238 ])->assertRedirect($this->path()) 239 ->assertSessionHas('popup', osu_trans('password_reset.error.invalid')); 240 241 $this->assertFalse($user->fresh()->checkPassword($newPassword)); 242 } 243 244 public function testUpdateInvalidConfirmation() 245 { 246 $user = User::factory()->create(); 247 248 $key = $this->generateKey($user); 249 250 $newPassword = static::randomPassword(); 251 252 $this->put($this->path(), [ 253 'key' => $key, 254 'user' => [ 255 'password' => $newPassword, 256 'password_confirmation' => "{$newPassword}!", 257 ], 258 'username' => $user->username, 259 ])->assertStatus(422); 260 261 $this->assertFalse($user->fresh()->checkPassword($newPassword)); 262 } 263 264 public function testUpdateInvalidKey() 265 { 266 $user = User::factory()->create(); 267 268 $this->post($this->path(), ['username' => $user->username]); 269 270 $newPassword = static::randomPassword(); 271 272 $this->put($this->path(), [ 273 'key' => '_invalidkey', 274 'user' => [ 275 'password' => $newPassword, 276 'password_confirmation' => $newPassword, 277 ], 278 'username' => $user->username, 279 ])->assertStatus(422); 280 281 $this->assertFalse($user->fresh()->checkPassword($newPassword)); 282 } 283 284 protected function setUp(): void 285 { 286 parent::setUp(); 287 $this->withoutMiddleware(ThrottleRequests::class); 288 // There's no easy way to clear data cache in redis otherwise 289 $this->origCacheDefault = $GLOBALS['cfg']['cache']['default']; 290 config_set('cache.default', 'array'); 291 } 292 293 protected function tearDown(): void 294 { 295 parent::tearDown(); 296 config_set('cache.default', $this->origCacheDefault); 297 } 298 299 private function generateKey(User $user): string 300 { 301 $username = $user->username; 302 PasswordResetData::find($user, $username)?->delete(); 303 PasswordResetData::create($user, $username); 304 305 return PasswordResetData::find($user, $username)->attrs['key']; 306 } 307 308 private function path(): string 309 { 310 static $path; 311 312 return $path ??= route('password-reset'); 313 } 314}