From e651c626e1d9c111c01bcb7c5afa782ed2032a03 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Tue, 31 Dec 2024 12:23:08 +0000 Subject: [PATCH] Add tests for email changing --- .../Fixtures/Providers/AdminPanelProvider.php | 2 + .../Fixtures/Providers/SlugsPanelProvider.php | 4 + tests/src/Panels/Auth/EditProfileTest.php | 197 ++++++++++++++++++ .../BlockEmailChangeVerificationTest.php | 102 +++++++++ .../VerifyEmailChangeTest.php | 115 ++++++++++ .../EmailCodeAuthenticationChallengeTest.php | 0 ...emoveEmailCodeAuthenticationActionTest.php | 0 ...SetUpEmailCodeAuthenticationActionTest.php | 0 ...rAuthenticationRecoveryCodesActionTest.php | 0 ...oogleTwoFactorAuthenticationActionTest.php | 0 ...oogleTwoFactorAuthenticationActionTest.php | 0 ...leTwoFactorAuthenticationChallengeTest.php | 0 12 files changed, 420 insertions(+) create mode 100644 tests/src/Panels/Auth/EditProfileTest.php create mode 100644 tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/BlockEmailChangeVerificationTest.php create mode 100644 tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/VerifyEmailChangeTest.php rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/EmailCode/EmailCodeAuthenticationChallengeTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/EmailCode/RemoveEmailCodeAuthenticationActionTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/EmailCode/SetUpEmailCodeAuthenticationActionTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/GoogleTwoFactor/Actions/RegenerateGoogleTwoFactorAuthenticationRecoveryCodesActionTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/GoogleTwoFactor/Actions/RemoveGoogleTwoFactorAuthenticationActionTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/GoogleTwoFactor/Actions/SetUpGoogleTwoFactorAuthenticationActionTest.php (100%) rename tests/src/Panels/Auth/{MultiFactorAuthentication => MultiFactor}/GoogleTwoFactor/GoogleTwoFactorAuthenticationChallengeTest.php (100%) diff --git a/tests/src/Fixtures/Providers/AdminPanelProvider.php b/tests/src/Fixtures/Providers/AdminPanelProvider.php index 7bdb7b535a7..be5c942489b 100644 --- a/tests/src/Fixtures/Providers/AdminPanelProvider.php +++ b/tests/src/Fixtures/Providers/AdminPanelProvider.php @@ -35,7 +35,9 @@ public function panel(Panel $panel): Panel ->login() ->registration() ->passwordReset() + ->emailChangeVerification() ->emailVerification() + ->profile() ->resources([ DepartmentResource::class, PostResource::class, diff --git a/tests/src/Fixtures/Providers/SlugsPanelProvider.php b/tests/src/Fixtures/Providers/SlugsPanelProvider.php index aa7c59e37cf..409bbb518c7 100644 --- a/tests/src/Fixtures/Providers/SlugsPanelProvider.php +++ b/tests/src/Fixtures/Providers/SlugsPanelProvider.php @@ -30,10 +30,14 @@ public function panel(Panel $panel): Panel ->passwordResetRoutePrefix('password-reset-test') ->registration() ->registrationRouteSlug('register-test') + ->emailChangeVerification() + ->emailChangeVerificationRouteSlug('verify-change-test') + ->emailChangeVerificationRoutePrefix('email-change-verification-test') ->emailVerification() ->emailVerificationPromptRouteSlug('prompt-test') ->emailVerificationRouteSlug('verify-test') ->emailVerificationRoutePrefix('email-verification-test') + ->profile() ->resources([]) ->pages([]) ->middleware([ diff --git a/tests/src/Panels/Auth/EditProfileTest.php b/tests/src/Panels/Auth/EditProfileTest.php new file mode 100644 index 00000000000..1d887a9e788 --- /dev/null +++ b/tests/src/Panels/Auth/EditProfileTest.php @@ -0,0 +1,197 @@ +user = User::factory()->create(); + + $this->actingAs($this->user); +}); + +it('can render page', function () { + $this->get(Filament::getProfileUrl()) + ->assertSuccessful(); +}); + +it('can retrieve data', function () { + livewire(EditProfile::class) + ->assertFormSet([ + 'name' => $this->user->name, + 'email' => $this->user->email, + ]); +}); + +it('can save name', function () { + $newUserData = User::factory()->make(); + + livewire(EditProfile::class) + ->fillForm([ + 'name' => $newUserData->name, + ]) + ->call('save') + ->assertHasNoFormErrors() + ->assertNotified('Saved'); + + expect($this->user->refresh()) + ->name->toBe($newUserData->name); +}); + +it('can save email', function () { + Filament::getCurrentOrDefaultPanel()->emailChangeVerification(false); + + $newUserData = User::factory()->make(); + + livewire(EditProfile::class) + ->fillForm([ + 'email' => $newUserData->email, + 'currentPassword' => 'password', + ]) + ->call('save') + ->assertHasNoFormErrors() + ->assertNotified('Saved'); + + expect($this->user->refresh()) + ->email->toBe($newUserData->email); +}); + +it('can send email change verification', function () { + Notification::fake(); + + Filament::getCurrentOrDefaultPanel()->emailChangeVerification(); + + $oldEmail = $this->user->email; + + $newUserData = User::factory()->make(); + + livewire(EditProfile::class) + ->fillForm([ + 'email' => $newUserData->email, + 'currentPassword' => 'password', + ]) + ->call('save') + ->assertHasNoFormErrors() + ->assertNotified('Email address change request sent') + ->assertFormSet([ + 'email' => $oldEmail, + ]); + + expect($this->user->refresh()) + ->email->toBe($oldEmail); + + Notification::assertSentTo($this->user, NoticeOfEmailChangeRequest::class, function (NoticeOfEmailChangeRequest $notification) use ($newUserData): bool { + if (blank($notification->blockVerificationUrl)) { + return false; + } + + return $notification->newEmail === $newUserData->email; + }); + + Notification::assertSentOnDemand(VerifyEmailChange::class, function (VerifyEmailChange $notification): bool { + $verificationSignature = Query::new($notification->url)->get('signature'); + + return cache()->has($verificationSignature); + }); +}); + +it('can save password', function () { + expect(Filament::auth()->attempt([ + 'email' => $this->user->email, + 'password' => 'password', + ]))->toBeTrue(); + + $newPassword = Str::random(); + + livewire(EditProfile::class) + ->fillForm([ + 'password' => $newPassword, + 'passwordConfirmation' => $newPassword, + 'currentPassword' => 'password', + ]) + ->call('save') + ->assertHasNoFormErrors() + ->assertNotified('Saved') + ->assertFormSet([ + 'password' => '', + 'passwordConfirmation' => '', + ]); + + expect(Filament::auth()->attempt([ + 'email' => $this->user->email, + 'password' => 'password', + ]))->toBeFalse(); + + expect(Filament::auth()->attempt([ + 'email' => $this->user->email, + 'password' => $newPassword, + ]))->toBeTrue(); +}); + +it('can validate', function (array $formData, array $errors) { + Filament::getCurrentOrDefaultPanel()->emailChangeVerification(false); + + livewire(EditProfile::class) + ->fillForm($formData) + ->call('save') + ->assertHasFormErrors($errors) + ->assertNotNotified('Saved'); + + $this->user->refresh(); + + foreach ($formData as $key => $value) { + expect($this->user->getAttributeValue($key)) + ->not->toBe($value); + } +})->with([ + '`name` is required' => [ + ['name' => '', 'email' => fake()->email], + ['name' => ['required']], + ], + '`name` is max 255 characters' => [ + ['name' => Str::random(256), 'email' => fake()->email], + ['name' => ['max']], + ], + '`email` is required' => [ + ['name' => fake()->name, 'email' => ''], + ['email' => ['required']], + ], + '`email` is valid email address' => [ + ['name' => fake()->name, 'email' => 'not-an-email'], + ['email' => ['email']], + ], + '`email` is unique' => fn (): array => [ + ['name' => fake()->name, 'email' => User::factory()->create()->email], + ['email' => ['unique']], + ], + '`password` is confirmed' => fn (): array => [ + ['name' => fake()->name, 'email' => fake()->email, 'password' => Str::random(), 'passwordConfirmation' => Str::random()], + ['password' => ['same']], + ], + '`passwordConfirmation` is required when `password` is filled' => fn (): array => [ + ['name' => fake()->name, 'email' => fake()->email, 'password' => Str::random(), 'passwordConfirmation' => ''], + ['passwordConfirmation' => ['required']], + ], + '`currentPassword` is required when `password` is filled' => fn (): array => [ + ['name' => fake()->name, 'email' => fake()->email, 'password' => Str::random(), 'currentPassword' => ''], + ['currentPassword' => ['required']], + ], + '`currentPassword` is required when `email` is changed' => fn (): array => [ + ['name' => fake()->name, 'email' => fake()->email, 'currentPassword' => ''], + ['currentPassword' => ['required']], + ], + '`currentPassword` is valid password' => fn (): array => [ + ['name' => fake()->name, 'email' => fake()->email, 'currentPassword' => 'invalid-password'], + ['currentPassword' => ['current_password']], + ], +]); diff --git a/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/BlockEmailChangeVerificationTest.php b/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/BlockEmailChangeVerificationTest.php new file mode 100644 index 00000000000..58fe7166cab --- /dev/null +++ b/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/BlockEmailChangeVerificationTest.php @@ -0,0 +1,102 @@ +create(); + $newEmail = fake()->email; + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $blockVerificationUrl = Filament::getBlockEmailChangeVerificationUrl($userToVerify, $newEmail, $verificationSignature); + + $this + ->actingAs($userToVerify) + ->get($blockVerificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + Notification::assertNotified('Email address change blocked'); + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); + + expect(cache()->has($verificationSignature)) + ->toBeFalse(); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertForbidden(); + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); +}); + +it('cannot block an email change when signed in as another user', function () { + $userToVerify = User::factory()->create(); + $newEmail = fake()->email; + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $blockVerificationUrl = Filament::getBlockEmailChangeVerificationUrl($userToVerify, $newEmail, $verificationSignature); + + $anotherUser = User::factory()->create(); + + $this + ->actingAs($anotherUser) + ->get($blockVerificationUrl) + ->assertForbidden(); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); +}); + +it('cannot block an email change once it has been verified', function () { + $userToVerify = User::factory()->create(); + $newEmail = fake()->email; + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); + + $blockVerificationUrl = Filament::getBlockEmailChangeVerificationUrl($userToVerify, $newEmail, $verificationSignature); + + $this + ->actingAs($userToVerify) + ->get($blockVerificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + Notification::assertNotified('Failed to block email address change'); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); +}); diff --git a/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/VerifyEmailChangeTest.php b/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/VerifyEmailChangeTest.php new file mode 100644 index 00000000000..4fd17a8db8b --- /dev/null +++ b/tests/src/Panels/Auth/EmailVerification/EmailChangeVerification/VerifyEmailChangeTest.php @@ -0,0 +1,115 @@ +create(); + $newEmail = fake()->email; + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + Notification::assertNotified('Email address changed'); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); +}); + +it('can verify an email with a custom slug', function () { + Filament::setCurrentPanel('slugs'); + + $userToVerify = User::factory()->create(); + $newEmail = fake()->email; + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + expect($userToVerify) + ->email->not->toBe($newEmail) + ->and($verificationUrl)->toContain('/email-change-verification-test/verify-change-test/'); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); +}); + +it('cannot verify an email when signed in as another user', function () { + $userToVerify = User::factory()->create(); + $newEmail = fake()->email; + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $anotherUser = User::factory()->create(); + + $this + ->actingAs($anotherUser) + ->get($verificationUrl) + ->assertForbidden(); + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); +}); + +it('cannot verify an email change with the same URL twice', function () { + $userToVerify = User::factory()->create(); + $newEmail = fake()->email; + + expect($userToVerify->refresh()) + ->email->not->toBe($newEmail); + + $verificationUrl = Filament::getVerifyEmailChangeUrl($userToVerify, $newEmail); + + $verificationSignature = Query::new($verificationUrl)->get('signature'); + cache()->put($verificationSignature, true, ttl: now()->addHour()); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertRedirect(Filament::getProfileUrl()); + + expect($userToVerify->refresh()) + ->email->toBe($newEmail); + + expect(cache()->has($verificationSignature)) + ->toBeFalse(); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertForbidden(); + + $userToVerify->update(['email' => fake()->email]); + + $this + ->actingAs($userToVerify) + ->get($verificationUrl) + ->assertForbidden(); +}); diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/EmailCodeAuthenticationChallengeTest.php b/tests/src/Panels/Auth/MultiFactor/EmailCode/EmailCodeAuthenticationChallengeTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/EmailCodeAuthenticationChallengeTest.php rename to tests/src/Panels/Auth/MultiFactor/EmailCode/EmailCodeAuthenticationChallengeTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/RemoveEmailCodeAuthenticationActionTest.php b/tests/src/Panels/Auth/MultiFactor/EmailCode/RemoveEmailCodeAuthenticationActionTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/RemoveEmailCodeAuthenticationActionTest.php rename to tests/src/Panels/Auth/MultiFactor/EmailCode/RemoveEmailCodeAuthenticationActionTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/SetUpEmailCodeAuthenticationActionTest.php b/tests/src/Panels/Auth/MultiFactor/EmailCode/SetUpEmailCodeAuthenticationActionTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/EmailCode/SetUpEmailCodeAuthenticationActionTest.php rename to tests/src/Panels/Auth/MultiFactor/EmailCode/SetUpEmailCodeAuthenticationActionTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/RegenerateGoogleTwoFactorAuthenticationRecoveryCodesActionTest.php b/tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/RegenerateGoogleTwoFactorAuthenticationRecoveryCodesActionTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/RegenerateGoogleTwoFactorAuthenticationRecoveryCodesActionTest.php rename to tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/RegenerateGoogleTwoFactorAuthenticationRecoveryCodesActionTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/RemoveGoogleTwoFactorAuthenticationActionTest.php b/tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/RemoveGoogleTwoFactorAuthenticationActionTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/RemoveGoogleTwoFactorAuthenticationActionTest.php rename to tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/RemoveGoogleTwoFactorAuthenticationActionTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/SetUpGoogleTwoFactorAuthenticationActionTest.php b/tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/SetUpGoogleTwoFactorAuthenticationActionTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/Actions/SetUpGoogleTwoFactorAuthenticationActionTest.php rename to tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/Actions/SetUpGoogleTwoFactorAuthenticationActionTest.php diff --git a/tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/GoogleTwoFactorAuthenticationChallengeTest.php b/tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/GoogleTwoFactorAuthenticationChallengeTest.php similarity index 100% rename from tests/src/Panels/Auth/MultiFactorAuthentication/GoogleTwoFactor/GoogleTwoFactorAuthenticationChallengeTest.php rename to tests/src/Panels/Auth/MultiFactor/GoogleTwoFactor/GoogleTwoFactorAuthenticationChallengeTest.php