From 64f6309c7272413039628cfa4809473d8281d141 Mon Sep 17 00:00:00 2001 From: roble Date: Mon, 24 Aug 2026 20:36:45 +0100 Subject: [PATCH] test: add WelcomeNotification translatability tests for email content --- ..._notification_enabled_to_auth_settings.php | 11 ++ lang/en/auth.php | 19 +++ lang/pt_BR/auth.php | 19 +++ src/Events/ReturningUserAuthenticated.php | 20 +++ src/Filament/Pages/AuthenticationSettings.php | 10 +- src/Http/Controllers/LoginController.php | 12 +- src/Http/Controllers/MagicLinkController.php | 8 ++ src/Http/Controllers/SocialiteController.php | 15 ++- src/Listeners/SendLoginNotification.php | 27 ++++ src/Notifications/LoginNotification.php | 81 ++++++++++++ src/Settings/AuthSettings.php | 2 + .../AuthenticationSettingsPageTest.php | 10 +- tests/Feature/LoginNotificationTest.php | 124 ++++++++++++++++++ tests/Feature/LoginTest.php | 64 +++++++++ tests/Feature/MagicLinkTest.php | 31 +++++ tests/Feature/RegisterTest.php | 23 ++++ tests/Feature/SocialiteCallbackTest.php | 47 +++++++ ...WelcomeNotificationTranslatabilityTest.php | 56 ++++++++ tests/e2e/tests/sidebar.spec.ts | 7 +- 19 files changed, 576 insertions(+), 10 deletions(-) create mode 100644 database/settings/2026_08_24_190804_add_login_notification_enabled_to_auth_settings.php create mode 100644 src/Events/ReturningUserAuthenticated.php create mode 100644 src/Listeners/SendLoginNotification.php create mode 100644 src/Notifications/LoginNotification.php create mode 100644 tests/Feature/LoginNotificationTest.php create mode 100644 tests/Feature/WelcomeNotificationTranslatabilityTest.php diff --git a/database/settings/2026_08_24_190804_add_login_notification_enabled_to_auth_settings.php b/database/settings/2026_08_24_190804_add_login_notification_enabled_to_auth_settings.php new file mode 100644 index 0000000..5e05c35 --- /dev/null +++ b/database/settings/2026_08_24_190804_add_login_notification_enabled_to_auth_settings.php @@ -0,0 +1,11 @@ +migrator->add('auth.login_notification_enabled', false); + } +}; diff --git a/lang/en/auth.php b/lang/en/auth.php index d7b586b..96db0d0 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -20,4 +20,23 @@ 'verification-link-sent' => 'A fresh verification link has been sent to your email address.', 'magic-link-sent' => "If an account with that email exists, we've sent a magic login link.", 'magic-link-expired' => 'This magic link has expired or has already been used.', + 'notifications' => [ + 'title' => 'Notifications', + 'description' => 'Configure security notifications sent to users.', + 'login-enabled' => 'Send login notifications', + 'login-help' => 'Email users after a successful sign-in to their account.', + ], + 'login-notification' => [ + 'subject' => 'New sign-in to your :app account', + 'greeting' => 'Hello :name,', + 'notice' => 'We noticed a new sign-in to your :app account.', + 'app' => 'App: :app', + 'time' => 'Time: :time', + 'ip-address' => 'IP address: :ip', + 'device-details' => 'Device details: :device', + 'recognized' => 'If this was you, no action is needed.', + 'action' => 'Reset your password', + 'unrecognized' => "If you don't recognize this activity, reset your password immediately.", + 'unknown' => 'Unknown', + ], ]; diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index e843b9b..70f2231 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -18,4 +18,23 @@ 'password' => 'A senha fornecida está incorreta.', 'throttle' => 'Muitas tentativas de login. Tente novamente em :seconds segundos.', 'verification-link-sent' => 'Um novo link de verificação foi enviado para seu endereço de email.', + 'notifications' => [ + 'title' => 'Notificações', + 'description' => 'Configure as notificações de segurança enviadas aos usuários.', + 'login-enabled' => 'Enviar notificações de acesso', + 'login-help' => 'Envie um email aos usuários após um acesso bem-sucedido à conta.', + ], + 'login-notification' => [ + 'subject' => 'Novo acesso à sua conta :app', + 'greeting' => 'Olá :name,', + 'notice' => 'Notamos um novo acesso à sua conta :app.', + 'app' => 'Aplicativo: :app', + 'time' => 'Horário: :time', + 'ip-address' => 'Endereço IP: :ip', + 'device-details' => 'Detalhes do dispositivo: :device', + 'recognized' => 'Se foi você, nenhuma ação é necessária.', + 'action' => 'Redefinir sua senha', + 'unrecognized' => 'Se você não reconhece esta atividade, redefina sua senha imediatamente.', + 'unknown' => 'Desconhecido', + ], ]; diff --git a/src/Events/ReturningUserAuthenticated.php b/src/Events/ReturningUserAuthenticated.php new file mode 100644 index 0000000..ea11e74 --- /dev/null +++ b/src/Events/ReturningUserAuthenticated.php @@ -0,0 +1,20 @@ +suffix(__('minutes')), ]) ->columns(1), + Section::make(__('auth::auth.notifications.title')) + ->description(__('auth::auth.notifications.description')) + ->icon(Heroicon::OutlinedBellAlert) + ->schema([ + Toggle::make('login_notification_enabled') + ->label(__('auth::auth.notifications.login-enabled')) + ->helperText(__('auth::auth.notifications.login-help')), + ]) + ->columns(1), ]); } } diff --git a/src/Http/Controllers/LoginController.php b/src/Http/Controllers/LoginController.php index c1a0017..c220032 100644 --- a/src/Http/Controllers/LoginController.php +++ b/src/Http/Controllers/LoginController.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Auth; use Inertia\Inertia; use Inertia\Response; +use Modules\Auth\Events\ReturningUserAuthenticated; use Modules\Auth\Exceptions\AuthException; use Modules\Auth\Http\Requests\LoginRequest; @@ -36,9 +37,16 @@ public function store(LoginRequest $request) return back()->with(['error' => $e->getMessage()]); } - Auth::login($user, request()->boolean('remember')); + Auth::login($user, $request->boolean('remember')); - request()->session()->regenerate(); + $request->session()->regenerate(); + + ReturningUserAuthenticated::dispatch( + $user, + now(), + $request->ip(), + $request->userAgent(), + ); Toast::default( __('auth::auth.welcome-back', ['name' => $user->name]), diff --git a/src/Http/Controllers/MagicLinkController.php b/src/Http/Controllers/MagicLinkController.php index 2989b8b..b5f306e 100644 --- a/src/Http/Controllers/MagicLinkController.php +++ b/src/Http/Controllers/MagicLinkController.php @@ -12,6 +12,7 @@ use Illuminate\Support\Str; use Inertia\Inertia; use Inertia\Response; +use Modules\Auth\Events\ReturningUserAuthenticated; use Modules\Auth\Http\Middleware\EnsureMagicLinkEnabled; use Modules\Auth\Models\MagicLinkToken; use Modules\Auth\Notifications\MagicLinkNotification; @@ -105,6 +106,13 @@ public function authenticate(Request $request, string $token): \Symfony\Componen $request->session()->regenerate(); + ReturningUserAuthenticated::dispatch( + $user, + now(), + $request->ip(), + $request->userAgent(), + ); + Toast::default(__('auth::auth.welcome-back', ['name' => $user->name])); $intended = $request->query('intended'); diff --git a/src/Http/Controllers/SocialiteController.php b/src/Http/Controllers/SocialiteController.php index ea1e159..92aff1e 100644 --- a/src/Http/Controllers/SocialiteController.php +++ b/src/Http/Controllers/SocialiteController.php @@ -3,10 +3,12 @@ namespace Modules\Auth\Http\Controllers; use App\Helpers\Toast; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Two\User; +use Modules\Auth\Events\ReturningUserAuthenticated; use Modules\Auth\Exceptions\SocialiteException; use Modules\Auth\Services\SocialiteService; use Symfony\Component\HttpFoundation\Response as RedirectResponse; @@ -25,7 +27,7 @@ public function redirect(string $provider): RedirectResponse return Socialite::driver($provider)->redirect(); } - public function callback(string $provider): RedirectResponse + public function callback(Request $request, string $provider): RedirectResponse { $validator = Validator::make(['provider' => $provider], [ 'provider' => 'required|string', @@ -57,7 +59,16 @@ public function callback(string $provider): RedirectResponse Auth::login($user); - request()->session()->regenerate(); + $request->session()->regenerate(); + + if (! $user->wasRecentlyCreated) { + ReturningUserAuthenticated::dispatch( + $user, + now(), + $request->ip(), + $request->userAgent(), + ); + } Toast::default( __($user->wasRecentlyCreated ? 'auth::auth.welcome' : 'auth::auth.welcome-back', [ diff --git a/src/Listeners/SendLoginNotification.php b/src/Listeners/SendLoginNotification.php new file mode 100644 index 0000000..b4e3e96 --- /dev/null +++ b/src/Listeners/SendLoginNotification.php @@ -0,0 +1,27 @@ +settings->login_notification_enabled) { + return; + } + + $event->user->notify(new LoginNotification( + loggedInAt: $event->loggedInAt, + ipAddress: $event->ipAddress, + userAgent: $event->userAgent, + )); + } +} diff --git a/src/Notifications/LoginNotification.php b/src/Notifications/LoginNotification.php new file mode 100644 index 0000000..0e11fb8 --- /dev/null +++ b/src/Notifications/LoginNotification.php @@ -0,0 +1,81 @@ +userAgent = $userAgent === null + ? null + : Str::limit($userAgent, 500, ''); + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + $appName = (string) config('app.name'); + $unknown = __('auth::auth.login-notification.unknown'); + $loggedInAt = $this->loggedInAt + ->copy() + ->setTimezone((string) config('app.timezone', 'UTC')) + ->locale(app()->getLocale()) + ->isoFormat('LLL Z'); + + return (new MailMessage) + ->subject(__('auth::auth.login-notification.subject', ['app' => $appName])) + ->greeting(__('auth::auth.login-notification.greeting', ['name' => $notifiable->name])) + ->line(__('auth::auth.login-notification.notice', ['app' => $appName])) + ->line(__('auth::auth.login-notification.app', ['app' => $appName])) + ->line(__('auth::auth.login-notification.time', ['time' => $loggedInAt])) + ->line(__('auth::auth.login-notification.ip-address', [ + 'ip' => $this->ipAddress ?? $unknown, + ])) + ->line(__('auth::auth.login-notification.device-details', [ + 'device' => $this->userAgent ?? $unknown, + ])) + ->line(__('auth::auth.login-notification.recognized')) + ->action(__('auth::auth.login-notification.action'), route('password.request')) + ->line(__('auth::auth.login-notification.unrecognized')); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'logged_in_at' => $this->loggedInAt->toIso8601String(), + 'ip_address' => $this->ipAddress, + 'user_agent' => $this->userAgent, + ]; + } +} diff --git a/src/Settings/AuthSettings.php b/src/Settings/AuthSettings.php index 974634c..8ef196a 100644 --- a/src/Settings/AuthSettings.php +++ b/src/Settings/AuthSettings.php @@ -10,6 +10,8 @@ class AuthSettings extends Settings public int $magic_link_expiry; + public bool $login_notification_enabled; + public static function group(): string { return 'auth'; diff --git a/tests/Feature/AuthenticationSettingsPageTest.php b/tests/Feature/AuthenticationSettingsPageTest.php index 490c28e..4087f8d 100644 --- a/tests/Feature/AuthenticationSettingsPageTest.php +++ b/tests/Feature/AuthenticationSettingsPageTest.php @@ -26,9 +26,11 @@ public function test_administrator_can_load_authentication_settings_form(): void ->assertOk(); Livewire::test(AuthenticationSettings::class) - ->assertFormSet([ + ->assertFormFieldExists('login_notification_enabled') + ->assertSchemaStateSet([ 'magic_link_enabled' => true, 'magic_link_expiry' => 15, + 'login_notification_enabled' => false, ]); } @@ -43,15 +45,17 @@ public function test_administrator_can_save_authentication_settings(): void ->fillForm([ 'magic_link_enabled' => false, 'magic_link_expiry' => 30, + 'login_notification_enabled' => true, ]) ->call('save') ->assertHasNoFormErrors() ->assertNotified(); - $settings = new AuthSettings(); + $settings = new AuthSettings; $this->assertFalse($settings->magic_link_enabled); $this->assertSame(30, $settings->magic_link_expiry); + $this->assertTrue($settings->login_notification_enabled); } #[DataProvider('invalidExpiryProvider')] @@ -73,7 +77,7 @@ public function test_invalid_expiry_does_not_change_authentication_settings( ->assertHasFormErrors(['magic_link_expiry' => $rule]) ->assertNotNotified(); - $settings = new AuthSettings(); + $settings = new AuthSettings; $this->assertTrue($settings->magic_link_enabled); $this->assertSame(15, $settings->magic_link_expiry); diff --git a/tests/Feature/LoginNotificationTest.php b/tests/Feature/LoginNotificationTest.php new file mode 100644 index 0000000..6508b6e --- /dev/null +++ b/tests/Feature/LoginNotificationTest.php @@ -0,0 +1,124 @@ + 'Saucebase', + 'app.timezone' => 'UTC', + ]); + + $user = User::factory()->create(['name' => 'Ana']); + $loggedInAt = CarbonImmutable::parse('2026-08-24 15:19:00', 'UTC'); + $notification = new LoginNotification( + loggedInAt: $loggedInAt, + ipAddress: '203.0.113.10', + userAgent: 'Test Browser 1.0', + ); + + $mail = $notification->toMail($user); + + $this->assertSame('New sign-in to your Saucebase account', $mail->subject); + $this->assertSame('Hello Ana,', $mail->greeting); + $this->assertContains('We noticed a new sign-in to your Saucebase account.', $mail->introLines); + $this->assertContains('App: Saucebase', $mail->introLines); + $this->assertContains('Time: August 24, 2026 3:19 PM +00:00', $mail->introLines); + $this->assertContains('IP address: 203.0.113.10', $mail->introLines); + $this->assertContains('Device details: Test Browser 1.0', $mail->introLines); + $this->assertContains('If this was you, no action is needed.', $mail->introLines); + $this->assertSame('Reset your password', $mail->actionText); + $this->assertSame(route('password.request'), $mail->actionUrl); + $this->assertContains( + "If you don't recognize this activity, reset your password immediately.", + $mail->outroLines, + ); + $this->assertSame([ + 'logged_in_at' => '2026-08-24T15:19:00+00:00', + 'ip_address' => '203.0.113.10', + 'user_agent' => 'Test Browser 1.0', + ], $notification->toArray($user)); + } + + public function test_login_notification_bounds_device_details_to_five_hundred_characters(): void + { + $user = User::factory()->create(); + $notification = new LoginNotification( + loggedInAt: CarbonImmutable::parse('2026-08-24 15:19:00', 'UTC'), + ipAddress: '203.0.113.10', + userAgent: Str::repeat('A', 600), + ); + + $mail = $notification->toMail($user); + + $this->assertContains( + 'Device details: '.Str::repeat('A', 500), + $mail->introLines, + ); + } + + public function test_login_notification_uses_fallbacks_for_missing_request_metadata(): void + { + $user = User::factory()->create(); + $notification = new LoginNotification( + loggedInAt: CarbonImmutable::parse('2026-08-24 15:19:00', 'UTC'), + ipAddress: null, + userAgent: null, + ); + + $mail = $notification->toMail($user); + + $this->assertContains('IP address: Unknown', $mail->introLines); + $this->assertContains('Device details: Unknown', $mail->introLines); + } + + public function test_login_notification_is_queued(): void + { + $notification = new LoginNotification( + loggedInAt: CarbonImmutable::parse('2026-08-24 15:19:00', 'UTC'), + ipAddress: null, + userAgent: null, + ); + + $this->assertInstanceOf(ShouldQueue::class, $notification); + } + + public function test_login_notification_uses_the_recipient_language(): void + { + config(['app.name' => 'Saucebase']); + App::setLocale('pt_BR'); + + $user = User::factory()->create(['name' => 'Ana']); + $notification = new LoginNotification( + loggedInAt: CarbonImmutable::parse('2026-08-24 15:19:00', 'UTC'), + ipAddress: '203.0.113.10', + userAgent: 'Navegador de teste', + ); + + $mail = $notification->toMail($user); + + $this->assertSame('Novo acesso à sua conta Saucebase', $mail->subject); + $this->assertSame('Olá Ana,', $mail->greeting); + $this->assertContains('Endereço IP: 203.0.113.10', $mail->introLines); + $this->assertContains('Detalhes do dispositivo: Navegador de teste', $mail->introLines); + $this->assertSame('Redefinir sua senha', $mail->actionText); + $this->assertTrue( + collect($mail->introLines)->contains( + fn (string $line): bool => str_contains($line, 'agosto'), + ), + ); + } +} diff --git a/tests/Feature/LoginTest.php b/tests/Feature/LoginTest.php index da90cbf..c7f0b48 100644 --- a/tests/Feature/LoginTest.php +++ b/tests/Feature/LoginTest.php @@ -5,7 +5,9 @@ use Illuminate\Auth\Events\Lockout; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Notification; use Inertia\Testing\AssertableInertia; +use Modules\Auth\Notifications\LoginNotification; use Modules\Auth\Settings\AuthSettings; use Tests\TestCase; @@ -54,6 +56,68 @@ public function test_user_can_login_with_valid_credentials(): void $response->assertRedirect(route('dashboard')); } + public function test_returning_user_receives_login_notification_when_enabled(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $loggedInAt = now()->startOfSecond(); + $this->travelTo($loggedInAt); + + $user = $this->createUser(); + + $this->withServerVariables([ + 'REMOTE_ADDR' => '203.0.113.10', + 'HTTP_USER_AGENT' => 'Test Browser 1.0', + ])->post(route('login'), [ + 'email' => $user->email, + 'password' => 'password', + ]); + + Notification::assertSentTo( + $user, + LoginNotification::class, + fn (LoginNotification $notification): bool => $notification->loggedInAt->equalTo($loggedInAt) + && $notification->ipAddress === '203.0.113.10' + && $notification->userAgent === 'Test Browser 1.0', + ); + } + + public function test_returning_user_does_not_receive_login_notification_when_disabled(): void + { + Notification::fake(); + + $user = $this->createUser(); + + $this->post(route('login'), [ + 'email' => $user->email, + 'password' => 'password', + ]); + + Notification::assertNotSentTo($user, LoginNotification::class); + } + + public function test_invalid_credentials_do_not_send_login_notification(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $user = $this->createUser(); + + $this->post(route('login'), [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + Notification::assertNotSentTo($user, LoginNotification::class); + } + public function test_login_with_wrong_password_returns_error(): void { $user = $this->createUser(); diff --git a/tests/Feature/MagicLinkTest.php b/tests/Feature/MagicLinkTest.php index f5d3566..5d64825 100644 --- a/tests/Feature/MagicLinkTest.php +++ b/tests/Feature/MagicLinkTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Notification; use Illuminate\Support\Str; use Modules\Auth\Models\MagicLinkToken; +use Modules\Auth\Notifications\LoginNotification; use Modules\Auth\Notifications\MagicLinkNotification; use Modules\Auth\Settings\AuthSettings; use Tests\TestCase; @@ -82,6 +83,36 @@ public function test_user_can_authenticate_with_valid_token(): void $response->assertRedirect(route('dashboard')); } + public function test_returning_user_receives_login_notification_after_magic_link_authentication(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $user = $this->createUser(); + $plainToken = Str::random(64); + + MagicLinkToken::create([ + 'user_id' => $user->id, + 'token' => hash('sha256', $plainToken), + 'expires_at' => now()->addMinutes(15), + ]); + + $this->withServerVariables([ + 'REMOTE_ADDR' => '203.0.113.11', + 'HTTP_USER_AGENT' => 'Magic Browser 1.0', + ])->get(route('magic-link.authenticate', $plainToken)); + + Notification::assertSentTo( + $user, + LoginNotification::class, + fn (LoginNotification $notification): bool => $notification->ipAddress === '203.0.113.11' + && $notification->userAgent === 'Magic Browser 1.0', + ); + } + public function test_authentication_fails_with_expired_token(): void { $user = $this->createUser(); diff --git a/tests/Feature/RegisterTest.php b/tests/Feature/RegisterTest.php index 44c1e81..078c7a0 100644 --- a/tests/Feature/RegisterTest.php +++ b/tests/Feature/RegisterTest.php @@ -6,7 +6,9 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Notification; +use Modules\Auth\Notifications\LoginNotification; use Modules\Auth\Notifications\WelcomeNotification; +use Modules\Auth\Settings\AuthSettings; use Tests\TestCase; class RegisterTest extends TestCase @@ -69,6 +71,27 @@ public function test_welcome_notification_is_sent_on_registration(): void Notification::assertSentTo($user, WelcomeNotification::class); } + public function test_registration_does_not_send_login_notification(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $this->post(route('register'), [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + 'terms' => true, + ]); + + $user = User::where('email', 'test@example.com')->sole(); + + Notification::assertNotSentTo($user, LoginNotification::class); + } + public function test_password_is_hashed_on_registration(): void { Notification::fake(); diff --git a/tests/Feature/SocialiteCallbackTest.php b/tests/Feature/SocialiteCallbackTest.php index 932060b..d7a0efc 100644 --- a/tests/Feature/SocialiteCallbackTest.php +++ b/tests/Feature/SocialiteCallbackTest.php @@ -2,11 +2,15 @@ namespace Modules\Auth\Tests\Feature; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Notification; use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\User as SocialiteUser; use Mockery\MockInterface; +use Modules\Auth\Notifications\LoginNotification; +use Modules\Auth\Settings\AuthSettings; use Tests\TestCase; class SocialiteCallbackTest extends TestCase @@ -53,6 +57,49 @@ public function test_callback_sets_last_social_provider_cookie(): void $response->assertCookie('last_social_provider', 'github'); } + public function test_returning_user_receives_login_notification_after_social_authentication(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $user = $this->createUser(); + $socialiteUser = $this->makeSocialiteUser(email: $user->email); + $this->mockSocialiteDriver($socialiteUser); + + $this->withServerVariables([ + 'REMOTE_ADDR' => '203.0.113.12', + 'HTTP_USER_AGENT' => 'Social Browser 1.0', + ])->get(route('auth.socialite.callback', ['provider' => 'github'])); + + Notification::assertSentTo( + $user, + LoginNotification::class, + fn (LoginNotification $notification): bool => $notification->ipAddress === '203.0.113.12' + && $notification->userAgent === 'Social Browser 1.0', + ); + } + + public function test_first_time_social_signup_does_not_send_login_notification(): void + { + Notification::fake(); + + $settings = app(AuthSettings::class); + $settings->login_notification_enabled = true; + $settings->save(); + + $socialiteUser = $this->makeSocialiteUser(); + $this->mockSocialiteDriver($socialiteUser); + + $this->get(route('auth.socialite.callback', ['provider' => 'github'])); + + $user = User::where('email', $socialiteUser->email)->sole(); + + Notification::assertNotSentTo($user, LoginNotification::class); + } + public function test_callback_does_not_set_cookie_during_account_linking(): void { $user = $this->createUser(); diff --git a/tests/Feature/WelcomeNotificationTranslatabilityTest.php b/tests/Feature/WelcomeNotificationTranslatabilityTest.php new file mode 100644 index 0000000..eb25437 --- /dev/null +++ b/tests/Feature/WelcomeNotificationTranslatabilityTest.php @@ -0,0 +1,56 @@ +create(['name' => 'Ana']); + + App::setLocale('xx'); + + $mail = (new WelcomeNotification)->toMail($user); + + $this->assertStringStartsWith('BEMVINDO', $mail->subject); + $this->assertSame('PAINEL', $mail->actionText); + $this->assertContains('CRIADA', $mail->introLines); + $this->assertContains('EXPLORE', $mail->introLines); + $this->assertContains('GRATO', $mail->outroLines); + } + + public function test_the_recipient_name_is_a_placeholder_not_a_concatenation(): void + { + $user = User::factory()->create(['name' => 'Ana']); + + App::setLocale('xx'); + + // Building the greeting with "." would leave "Hello" permanently English. + $this->assertSame('OLA Ana,', (new WelcomeNotification)->toMail($user)->greeting); + } +} diff --git a/tests/e2e/tests/sidebar.spec.ts b/tests/e2e/tests/sidebar.spec.ts index f126fb8..b806b31 100644 --- a/tests/e2e/tests/sidebar.spec.ts +++ b/tests/e2e/tests/sidebar.spec.ts @@ -14,13 +14,16 @@ async function skipIfTenancyInstalled(laravel: Parameters { - test('renders tenant switcher in sidebar header', async ({ page, laravel, credentials, loginAs }) => { + // The sidebar header is a `sidebar-brand` slot. With no module claiming it, core's own + // AppBrand fills it — which is the only thing this module can assert, since the test + // is skipped in exactly the configuration where tenancy claims the slot instead. + test('renders the application brand in the sidebar header', async ({ page, laravel, credentials, loginAs }) => { await skipIfTenancyInstalled(laravel); await loginAs(credentials.user); await page.goto('/dashboard'); await expectAuthenticated(page); - await expect(page.getByTestId('tenant-switcher')).toBeVisible(); + await expect(page.getByTestId('app-brand')).toBeVisible(); }); test('user dropdown contains language and theme selectors', async ({ page, laravel, credentials, loginAs }) => {