Http::response('ok', 200), ]); } public function test_index_page_loads_for_new_visitors(): void { $this->get(route('verification.index')) ->assertOk() ->assertViewIs('verification.index'); } public function test_send_otp_starts_verification_flow(): void { $this->post(route('verification.send'), ['phone' => self::PHONE]) ->assertRedirect(route('verification.verify.form')); $this->assertDatabaseHas(PhoneVerification::class, [ 'phone' => self::UNMASKED_PHONE, 'verified' => false, ]); Http::assertSentCount(1); } public function test_send_otp_rejects_duplicate_phone(): void { Coupon::factory()->create(['phone' => self::UNMASKED_PHONE]); $this->from(route('verification.index')) ->post(route('verification.send'), ['phone' => self::PHONE]) ->assertRedirect(route('verification.index')) ->assertSessionHasErrors('phone'); } public function test_send_otp_rejects_invalid_phone_format(): void { $this->from(route('verification.index')) ->post(route('verification.send'), ['phone' => '+993 81 23 45 67']) ->assertRedirect(route('verification.index')) ->assertSessionHasErrors('phone'); } public function test_verify_form_requires_session_phone(): void { $this->get(route('verification.verify.form')) ->assertRedirect(route('verification.index')); } public function test_full_verification_flow_issues_coupon(): void { $this->post(route('verification.send'), ['phone' => self::PHONE]) ->assertRedirect(route('verification.verify.form')); $otp = $this->extractOtpFromLastSms(); $this->get(route('verification.verify.form'))->assertOk(); $this->post(route('verification.verify'), ['otp' => $otp]) ->assertRedirect(route('verification.congratulations')) ->assertCookie('device_registered'); $this->assertDatabaseHas(Coupon::class, [ 'phone' => self::UNMASKED_PHONE, ]); $this->get(route('verification.congratulations')) ->assertOk() ->assertViewHas('code'); } public function test_verify_otp_rejects_wrong_code(): void { PhoneVerification::factory() ->withOtp('1234') ->create(['phone' => self::UNMASKED_PHONE]); $this->withSession(['verify_phone' => self::UNMASKED_PHONE]) ->from(route('verification.verify.form')) ->post(route('verification.verify'), ['otp' => '9999']) ->assertRedirect(route('verification.verify.form')) ->assertSessionHasErrors('otp'); } public function test_verify_otp_rejects_expired_code(): void { PhoneVerification::factory() ->withOtp('1234') ->expired() ->create(['phone' => self::UNMASKED_PHONE]); $this->withSession(['verify_phone' => self::UNMASKED_PHONE]) ->from(route('verification.verify.form')) ->post(route('verification.verify'), ['otp' => '1234']) ->assertRedirect(route('verification.verify.form')) ->assertSessionHasErrors(['otp' => 'Kodyň möhleti gutardy.']); } public function test_verify_otp_requires_session_phone(): void { $this->post(route('verification.verify'), ['otp' => '1234']) ->assertRedirect(route('verification.index')); } public function test_resend_otp_sends_new_sms(): void { $this->withSession(['verify_phone' => self::UNMASKED_PHONE]) ->from(route('verification.verify.form')) ->post(route('verification.resend')) ->assertRedirect(route('verification.verify.form')) ->assertSessionHas('status'); Http::assertSentCount(1); } public function test_congratulations_recovers_coupon_from_device_cookie(): void { $coupon = Coupon::factory()->create(['phone' => self::UNMASKED_PHONE]); $this->withCookie('device_registered', encrypt(self::UNMASKED_PHONE)) ->get(route('verification.congratulations')) ->assertOk() ->assertViewIs('verification.congratulations') ->assertSee($coupon->code); } public function test_congratulations_redirects_when_session_coupon_missing_from_db(): void { $this->withSession(['coupon_code' => '9_STALE']) ->get(route('verification.congratulations')) ->assertRedirect(route('verification.index')) ->assertSessionMissing('coupon_code'); } public function test_send_otp_is_rate_limited(): void { for ($i = 0; $i < 3; $i++) { $this->post(route('verification.send'), ['phone' => self::PHONE]); } $this->post(route('verification.send'), ['phone' => self::PHONE]) ->assertStatus(429); } private function extractOtpFromLastSms(): string { $recorded = Http::recorded()->all(); $this->assertNotEmpty($recorded); [$request] = $recorded[array_key_last($recorded)]; preg_match('/(\d{4})/', (string) $request->data()['code'], $matches); $this->assertNotEmpty($matches[1]); return $matches[1]; } }