postJson('/api/auth/register', [ 'name' => 'Leon', 'email' => 'leon@example.com', 'password' => 'Motsdfdepasse123*', 'locale' => 'fr-FR', 'physicalActivityLevel' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value, 'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value, 'pacePreference' => PacePreference::NORMAL->value, 'sex' => UserSex::WOMAN->value, 'dateOfBirth' => '2003-04-24', ]) ->assertCreated() ->assertJsonPath('user.email', 'leon@example.com') ->assertJsonPath('user.locale', 'fr') ->assertJsonPath('user.emailVerified', false) ->assertJsonPath('user.physicalActivityLevel', PhysicalActivityLevel::LIGHTLY_ACTIVE->value) ->assertJsonPath('user.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value) ->assertJsonPath('user.pacePreference', PacePreference::NORMAL->value) ->assertJsonPath('user.sex', UserSex::WOMAN->value) ->assertJsonPath('user.dateOfBirth', '2003-04-24'); $user = User::where('email', 'leon@example.com')->firstOrFail(); expect($user->hasVerifiedEmail())->toBeFalse(); $this->assertDatabaseHas('users', [ 'id' => $user->id, 'locale' => 'fr', 'physical_activity_level' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value, 'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value, 'pace_preference' => PacePreference::NORMAL->value, 'sex' => UserSex::WOMAN->value, ]); expect($user->date_of_birth?->toDateString())->toBe('2003-04-24'); Notification::assertSentTo($user, VerifyEmail::class); }); it('uses the custom account verification mailable', function () { $user = User::factory()->unverified()->create([ 'locale' => 'fr', ]); $mailable = (new VerifyEmail)->toMail($user); expect($mailable)->toBeInstanceOf(VerifyAccount::class); $mailable ->assertSeeInHtml('Vérifier mon compte') ->assertSeeInHtml($user->name); }); it('uses the user locale for account verification emails', function () { $user = User::factory()->unverified()->create([ 'locale' => 'en', ]); $mailable = (new VerifyEmail)->toMail($user); expect($mailable)->toBeInstanceOf(VerifyAccount::class); $mailable ->assertSeeInHtml('Verify my account') ->assertSeeInHtml('Confirm your email address') ->assertDontSeeInHtml('Vérifier mon compte'); }); it('generates verification links with a relative signature', function () { $user = User::factory()->unverified()->create(); $verificationUrl = (new VerifyEmail)->toMail($user)->verificationUrl; expect($verificationUrl)->toContain('/api/auth/email/verify/') ->and($verificationUrl)->toContain('signature='); $urlParts = parse_url($verificationUrl); $requestUrl = ($urlParts['path'] ?? '').'?'.($urlParts['query'] ?? ''); $this ->withServerVariables(['HTTP_HOST' => 'different-host.test']) ->getJson($requestUrl) ->assertOk() ->assertJsonPath('message', __('api.auth.verified')); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); }); it('verifies a user from a signed email link', function () { $user = User::factory()->unverified()->create(); $url = URL::temporarySignedRoute('verification.verify', now()->addMinutes(60), [ 'id' => $user->getKey(), 'hash' => sha1($user->email), ], absolute: false); $this->getJson($url) ->assertOk() ->assertJsonPath('message', __('api.auth.verified')) ->assertJsonPath('user.emailVerified', true); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); }); it('rejects signed verification links with an invalid hash', function () { $user = User::factory()->unverified()->create(); $url = URL::temporarySignedRoute('verification.verify', now()->addMinutes(60), [ 'id' => $user->getKey(), 'hash' => sha1('wrong-address@example.com'), ], absolute: false); $this->getJson($url)->assertForbidden(); expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); }); it('blocks login for unverified users', function () { $user = User::factory()->unverified()->create([ 'password' => 'password', ]); $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => 'password', ]) ->assertForbidden() ->assertJsonPath('message', __('api.auth.email_not_verified')) ->assertCookieMissing('token'); $this->assertDatabaseMissing('personal_access_tokens', [ 'tokenable_id' => $user->getKey(), ]); }); it('allows login for verified users', function () { $user = User::factory()->create([ 'locale' => 'en', 'password' => 'password', ]); $this->postJson('/api/auth/login', [ 'email' => $user->email, 'locale' => 'fr-FR', 'password' => 'password', ]) ->assertOk() ->assertJsonPath('user.email', $user->email) ->assertJsonPath('user.locale', 'fr') ->assertJsonPath('user.emailVerified', true) ->assertCookie('token'); expect($user->fresh()->locale)->toBe('fr'); }); it('resends a verification email for an unverified user without authentication', function () { Notification::fake(); $user = User::factory()->unverified()->create(); $this->postJson('/api/auth/email/verification-notification', [ 'email' => $user->email, ]) ->assertOk() ->assertJsonPath('message', __('api.auth.verification_sent_if_unverified')); Notification::assertSentTo($user, VerifyEmail::class); }); it('does not reveal whether an email can receive a verification email', function () { Notification::fake(); $verifiedUser = User::factory()->create(); $this->postJson('/api/auth/email/verification-notification', [ 'email' => $verifiedUser->email, ]) ->assertOk() ->assertJsonPath('message', __('api.auth.verification_sent_if_unverified')); $this->postJson('/api/auth/email/verification-notification', [ 'email' => 'missing@example.com', ]) ->assertOk() ->assertJsonPath('message', __('api.auth.verification_sent_if_unverified')); Notification::assertNothingSent(); }); it('blocks verified routes for unverified users', function () { $user = User::factory()->unverified()->create(); Sanctum::actingAs($user); $this->getJson('/api/meal-posts')->assertForbidden(); });