seed(RoleSeeder::class); $this->app->instance(SmsService::class, new FakeSmsService); } public function test_forgot_password_returns_ok_for_existing_phone(): void { $user = User::factory()->create(['phone' => '+639170000020']); $response = $this->postJson('/api/v1/auth/forgot-password', [ 'phone' => $user->phone, ]); $response->assertOk(); $this->assertDatabaseHas('otp_codes', [ 'destination' => $user->phone, 'purpose' => OtpCode::PURPOSE_PASSWORD_RESET, ]); } public function test_forgot_password_does_not_leak_unknown_phone(): void { $response = $this->postJson('/api/v1/auth/forgot-password', [ 'phone' => '+639170000099', ]); $response->assertOk(); $this->assertDatabaseMissing('otp_codes', [ 'destination' => '+639170000099', 'purpose' => OtpCode::PURPOSE_PASSWORD_RESET, ]); } public function test_reset_password_succeeds_with_valid_otp(): void { $user = User::factory()->create([ 'phone' => '+639170000021', 'password' => Hash::make('OldPassword1'), ]); $user->createToken('old-device'); $issue = app(OtpService::class)->issue( $user->phone, OtpCode::PURPOSE_PASSWORD_RESET, $user, ); $response = $this->postJson('/api/v1/auth/reset-password', [ 'phone' => $user->phone, 'code' => $issue->plainCode, 'password' => 'NewPassword1', 'password_confirmation' => 'NewPassword1', ]); $response->assertOk(); $this->assertTrue(Hash::check('NewPassword1', $user->fresh()->password)); $this->assertDatabaseMissing('personal_access_tokens', [ 'tokenable_id' => $user->id, ]); } public function test_reset_password_rejects_invalid_otp(): void { $user = User::factory()->create(['phone' => '+639170000022']); app(OtpService::class)->issue($user->phone, OtpCode::PURPOSE_PASSWORD_RESET, $user); $response = $this->postJson('/api/v1/auth/reset-password', [ 'phone' => $user->phone, 'code' => '000000', 'password' => 'NewPassword1', 'password_confirmation' => 'NewPassword1', ]); $response->assertStatus(422) ->assertJsonPath('success', false); } }