seed(RoleSeeder::class); $this->fakeSms = new FakeSmsService; $this->app->instance(SmsService::class, $this->fakeSms); } public function test_register_otp_can_be_verified_and_activates_user(): void { $user = User::factory()->create([ 'phone' => '+639170000010', 'status' => User::STATUS_PENDING, 'phone_verified_at' => null, ]); $issue = app(OtpService::class)->issue( $user->phone, OtpCode::PURPOSE_REGISTER, $user, ); $response = $this->postJson('/api/v1/auth/otp/verify', [ 'phone' => $user->phone, 'code' => $issue->plainCode, 'purpose' => OtpCode::PURPOSE_REGISTER, ]); $response->assertOk() ->assertJsonPath('data.verified', true) ->assertJsonPath('data.user.status', User::STATUS_ACTIVE); $this->assertNotNull($user->fresh()->phone_verified_at); } public function test_invalid_otp_rejected(): void { $user = User::factory()->create(['phone' => '+639170000011']); app(OtpService::class)->issue($user->phone, OtpCode::PURPOSE_REGISTER, $user); $response = $this->postJson('/api/v1/auth/otp/verify', [ 'phone' => $user->phone, 'code' => '000000', 'purpose' => OtpCode::PURPOSE_REGISTER, ]); $response->assertStatus(422) ->assertJsonPath('success', false); } public function test_otp_locks_after_max_attempts(): void { $user = User::factory()->create(['phone' => '+639170000012']); app(OtpService::class)->issue($user->phone, OtpCode::PURPOSE_REGISTER, $user); for ($i = 0; $i < OtpCode::MAX_ATTEMPTS; $i++) { $this->postJson('/api/v1/auth/otp/verify', [ 'phone' => $user->phone, 'code' => '999999', 'purpose' => OtpCode::PURPOSE_REGISTER, ]); } $response = $this->postJson('/api/v1/auth/otp/verify', [ 'phone' => $user->phone, 'code' => '111111', 'purpose' => OtpCode::PURPOSE_REGISTER, ]); $response->assertStatus(422) ->assertJsonPath('errors.code.0', 'exhausted'); } public function test_resend_otp_throttled_within_cooldown(): void { $user = User::factory()->create(['phone' => '+639170000013']); app(OtpService::class)->issue($user->phone, OtpCode::PURPOSE_REGISTER, $user); $response = $this->postJson('/api/v1/auth/otp/resend', [ 'phone' => $user->phone, 'purpose' => OtpCode::PURPOSE_REGISTER, ]); $response->assertStatus(429) ->assertJsonPath('success', false); } }