182 lines
5.8 KiB
PHP
182 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Coupon;
|
|
use App\Models\PhoneVerification;
|
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\TestCase;
|
|
|
|
class VerificationFlowTest extends TestCase
|
|
{
|
|
use LazilyRefreshDatabase;
|
|
|
|
private const PHONE = '+993 61 92 92 48';
|
|
|
|
private const UNMASKED_PHONE = '61929248';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Http::fake([
|
|
config('services.sms.url') => 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];
|
|
}
|
|
}
|