seed(RolesAndPermissionsSeeder::class); }); /** * @return array{user: User, secret: string|null, company: Company, pr: PressRelease} */ function loginCompletionCustomer(bool $twoFactor = false): array { $user = User::factory()->create(['is_active' => true]); $user->assignRole('customer'); $secret = null; if ($twoFactor) { $secret = app(Google2FA::class)->generateSecretKey(); $user->forceFill(['two_factor_secret' => encrypt($secret), 'two_factor_confirmed_at' => now()])->save(); } $company = Company::factory()->presseecho()->create(); $user->companies()->attach($company->id, ['role' => 'owner']); $pr = PressRelease::factory()->create([ 'user_id' => $user->id, 'company_id' => $company->id, 'portal' => $company->portal->value, 'status' => 'draft', ]); return compact('user', 'secret', 'company', 'pr'); } function loginCompletionLink(User $user, string $token, array $payload = []): MagicLink { return MagicLink::query()->create([ 'user_id' => $user->id, 'token_hash' => hash('sha256', $token), 'purpose' => 'login', 'payload' => $payload ?: null, 'expires_at' => now()->addMinutes(30), ]); } function loginCompletionMethod(User $user): ?string { return ActivityEvent::query() ->where('type', ActivityType::UserLoggedIn->value) ->where('user_id', $user->id) ->latest('id') ->first()?->meta['method'] ?? null; } test('the password login records the last login also after the two-factor challenge', function () { /** @var TestCase $this */ ['user' => $user, 'secret' => $secret] = loginCompletionCustomer(twoFactor: true); Livewire::test('auth.login') ->set('email', $user->email) ->set('password', 'password') ->call('login') ->assertRedirect(route('two-factor.challenge')); $this->post('/two-factor-challenge', ['code' => app(Google2FA::class)->getCurrentOtp($secret)]) ->assertRedirect(route('me.dashboard', absolute: false)); $this->assertAuthenticatedAs($user); expect($user->fresh()->last_login_at)->not->toBeNull() ->and(loginCompletionMethod($user))->toBe('password'); }); test('a magic link keeps its landing target and its method through the two-factor challenge', function () { /** @var TestCase $this */ ['user' => $user, 'secret' => $secret, 'company' => $company, 'pr' => $pr] = loginCompletionCustomer(twoFactor: true); loginCompletionLink($user, 'landing-2fa', ['company_id' => $company->id, 'press_release_id' => $pr->id]); $this->get(route('magic-links.consume', ['token' => 'landing-2fa'])) ->assertRedirect(route('two-factor.challenge')); $this->post('/two-factor-challenge', ['code' => app(Google2FA::class)->getCurrentOtp($secret)]) ->assertRedirect(route('me.press-releases.show', ['id' => $pr->id], absolute: false)); expect($user->fresh()->last_login_at)->not->toBeNull() ->and(loginCompletionMethod($user))->toBe('magic-link'); }); test('google no longer forces a remember cookie, also not through the two-factor challenge', function () { /** @var TestCase $this */ ['user' => $user, 'secret' => $secret] = loginCompletionCustomer(twoFactor: true); $provider = Mockery::mock(Provider::class); // Bereits verknüpftes Konto; die Erstverknüpfung prüft GoogleLoginTest (S-05). $user->forceFill(['oauth_provider' => 'google', 'oauth_provider_id' => 'g-lc-1'])->save(); $provider->shouldReceive('user')->andReturn((new SocialiteUser)->setRaw(['email_verified' => true])->map(['id' => 'g-lc-1', 'email' => $user->email, 'name' => 'Kunde'])); Socialite::shouldReceive('driver')->with('google')->andReturn($provider); $this->get(route('oauth.google.callback'))->assertRedirect(route('two-factor.challenge')); expect(session('login.remember'))->toBeFalse(); $response = $this->post('/two-factor-challenge', ['code' => app(Google2FA::class)->getCurrentOtp($secret)]); $response->assertRedirect(route('me.dashboard', absolute: false)); expect(collect($response->headers->getCookies())->contains(fn ($cookie) => str_starts_with($cookie->getName(), 'remember_web_')))->toBeFalse() ->and(loginCompletionMethod($user))->toBe('google') ->and($user->fresh()->last_login_at)->not->toBeNull(); }); test('google without two-factor logs in without a remember cookie', function () { /** @var TestCase $this */ ['user' => $user] = loginCompletionCustomer(); $provider = Mockery::mock(Provider::class); // Bereits verknüpftes Konto; die Erstverknüpfung prüft GoogleLoginTest (S-05). $user->forceFill(['oauth_provider' => 'google', 'oauth_provider_id' => 'g-lc-2'])->save(); $provider->shouldReceive('user')->andReturn((new SocialiteUser)->setRaw(['email_verified' => true])->map(['id' => 'g-lc-2', 'email' => $user->email, 'name' => 'Kunde'])); Socialite::shouldReceive('driver')->with('google')->andReturn($provider); $response = $this->get(route('oauth.google.callback')); $response->assertRedirect(route('me.dashboard', absolute: false)); $this->assertAuthenticatedAs($user); expect(collect($response->headers->getCookies())->contains(fn ($cookie) => str_starts_with($cookie->getName(), 'remember_web_')))->toBeFalse(); }); test('a logged-in customer opening his own magic link lands on its target', function () { /** @var TestCase $this */ ['user' => $user, 'company' => $company, 'pr' => $pr] = loginCompletionCustomer(); $link = loginCompletionLink($user, 'own-link', ['company_id' => $company->id, 'press_release_id' => $pr->id]); $this->actingAs($user) ->get(route('magic-links.consume', ['token' => 'own-link'])) ->assertRedirect(route('me.press-releases.show', ['id' => $pr->id], absolute: false)); $this->assertAuthenticatedAs($user); expect($link->fresh()->consumed_at)->not->toBeNull(); }); test('a logged-in user opening a link of another account keeps the previous behaviour', function () { /** @var TestCase $this */ ['user' => $user] = loginCompletionCustomer(); ['user' => $other] = loginCompletionCustomer(); $link = loginCompletionLink($other, 'foreign-link'); $this->actingAs($user) ->get(route('magic-links.consume', ['token' => 'foreign-link'])) ->assertRedirect(route('me.dashboard')); $this->assertAuthenticatedAs($user); expect($link->fresh()->consumed_at)->toBeNull(); });