true, 'services.sms.sms_poh.server' => 'https://sms.example.test/send', 'services.sms.sms_poh.token' => 'test-token', 'services.sms.sms_poh.sender' => 'App', ]); }); test('requesting a code for a new email sends a mail and creates a pending verification', function () { Mail::fake(); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => 'new@example.com']) ->assertSuccessful(); Mail::assertSent(RegistrationCodeMail::class); $verification = RegistrationVerification::where('identifier', 'new@example.com')->sole(); expect($verification->type)->toBe('email') ->and($verification->verified_at)->toBeNull(); }); test('requesting a code for a new phone number sends an sms', function () { Http::fake(['sms.example.test/*' => Http::response('OK', 200)]); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => '+959123456789']) ->assertSuccessful(); Http::assertSent(fn ($request) => $request->url() === 'https://sms.example.test/send' && $request['to'] === '+959123456789'); $verification = RegistrationVerification::where('identifier', '+959123456789')->sole(); expect($verification->type)->toBe('phone'); }); test('requesting a code rejects an already registered email', function () { Mail::fake(); User::factory()->create(['email' => 'taken@example.com']); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => 'taken@example.com']) ->assertUnprocessable() ->assertJsonValidationErrors('identifier'); Mail::assertNothingSent(); }); test('requesting a code rejects an already registered phone', function () { User::factory()->create(['phone' => '+959123456789']); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => '+959123456789']) ->assertUnprocessable() ->assertJsonValidationErrors('identifier'); }); test('verifying with the correct code returns a verification token', function () { Mail::fake(); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => 'new@example.com']); $verification = RegistrationVerification::where('identifier', 'new@example.com')->sole(); // The plaintext code isn't returned by the API by design, so reach // into the model the same way the real code was generated to recover // it for the test — simplest is to reissue with a known code via the // factory instead of parsing outbound mail content. $verification->forceFill(['code' => Hash::make('654321')])->save(); $this->postJson('/api/v1/auth/registration/verify-code', [ 'identifier' => 'new@example.com', 'code' => '654321', ]) ->assertSuccessful() ->assertJsonStructure(['verification_token']); expect($verification->fresh()->verified_at)->not->toBeNull(); }); test('verifying with the wrong code fails and increments attempts', function () { $verification = RegistrationVerification::factory()->create(); $this->postJson('/api/v1/auth/registration/verify-code', [ 'identifier' => $verification->identifier, 'code' => '000000', ])->assertUnprocessable(); expect($verification->fresh()->attempts)->toBe(1); }); test('verifying locks out after too many wrong attempts', function () { $verification = RegistrationVerification::factory()->create(['attempts' => 5]); $this->postJson('/api/v1/auth/registration/verify-code', [ 'identifier' => $verification->identifier, 'code' => '000000', ])->assertUnprocessable(); }); test('verifying an expired code fails', function () { $verification = RegistrationVerification::factory()->expired()->create(); $this->postJson('/api/v1/auth/registration/verify-code', [ 'identifier' => $verification->identifier, 'code' => '000000', ])->assertUnprocessable(); }); test('registering with a valid verification token creates a user and returns a token', function () { $verification = RegistrationVerification::factory()->verified()->create(['identifier' => 'new@example.com']); $response = $this->postJson('/api/v1/auth/register', [ 'verification_token' => $verification->verification_token, 'name' => 'Jane Doe', 'password' => 'super-secret-password', 'password_confirmation' => 'super-secret-password', 'device_name' => 'iphone', ]); $response->assertSuccessful()->assertJsonStructure(['token']); $user = User::where('email', 'new@example.com')->sole(); expect($user->name)->toBe('Jane Doe') ->and($verification->fresh()->consumed_at)->not->toBeNull(); $accessToken = $user->tokens()->sole(); expect($accessToken->abilities)->toEqualCanonicalizing(TokenAbility::customerAbilities()); }); test('registering fails when the verification token was already consumed', function () { $verification = RegistrationVerification::factory()->verified()->create([ 'identifier' => 'new@example.com', 'consumed_at' => now(), ]); $this->postJson('/api/v1/auth/register', [ 'verification_token' => $verification->verification_token, 'name' => 'Jane Doe', 'password' => 'super-secret-password', 'password_confirmation' => 'super-secret-password', 'device_name' => 'iphone', ])->assertUnprocessable(); }); test('registering fails with an unknown verification token', function () { $this->postJson('/api/v1/auth/register', [ 'verification_token' => 'not-a-real-token', 'name' => 'Jane Doe', 'password' => 'super-secret-password', 'password_confirmation' => 'super-secret-password', 'device_name' => 'iphone', ])->assertUnprocessable(); }); test('the full request-code, verify-code, register flow works end to end', function () { Mail::fake(); $this->postJson('/api/v1/auth/registration/request-code', ['identifier' => 'flow@example.com']) ->assertSuccessful(); $verification = RegistrationVerification::where('identifier', 'flow@example.com')->sole(); $verification->forceFill(['code' => Hash::make('111222')])->save(); $verifyResponse = $this->postJson('/api/v1/auth/registration/verify-code', [ 'identifier' => 'flow@example.com', 'code' => '111222', ])->assertSuccessful(); $registerResponse = $this->postJson('/api/v1/auth/register', [ 'verification_token' => $verifyResponse->json('verification_token'), 'name' => 'Flow User', 'password' => 'super-secret-password', 'password_confirmation' => 'super-secret-password', 'device_name' => 'iphone', ])->assertSuccessful(); $token = $registerResponse->json('token'); $this->withHeader('Authorization', "Bearer {$token}") ->getJson('/api/v1/companies') ->assertSuccessful(); });