seed(RolesAndPermissionsSeeder::class); }); function fakeGoogleUser(string $id, string $email, string $name): void { $socialiteUser = (new SocialiteUser)->map([ 'id' => $id, 'email' => $email, 'name' => $name, ]); $provider = Mockery::mock(Provider::class); $provider->shouldReceive('user')->andReturn($socialiteUser); Socialite::shouldReceive('driver')->with('google')->andReturn($provider); } test('a new google user is created active, verified and as customer', function () { /** @var TestCase $this */ fakeGoogleUser('g-new-1', 'new-google@example.test', 'Neu Google'); $this->get(route('oauth.google.callback')) ->assertRedirect(route('me.dashboard', absolute: false)); $user = User::query()->where('email', 'new-google@example.test')->firstOrFail(); expect($user->is_active)->toBeTrue(); expect($user->hasVerifiedEmail())->toBeTrue(); expect($user->hasRole('customer'))->toBeTrue(); expect($user->oauth_provider)->toBe('google'); expect($user->oauth_provider_id)->toBe('g-new-1'); expect($user->gdpr_consent_at)->not->toBeNull(); $this->assertAuthenticatedAs($user); }); test('an existing user is linked by email without creating a duplicate', function () { /** @var TestCase $this */ $existing = User::factory()->create(['email' => 'existing-google@example.test', 'is_active' => true]); $existing->assignRole('customer'); fakeGoogleUser('g-existing-1', 'Existing-Google@example.test', 'Existing'); $this->get(route('oauth.google.callback')) ->assertRedirect(route('me.dashboard', absolute: false)); expect(User::query()->where('email', 'existing-google@example.test')->count())->toBe(1); $existing->refresh(); expect($existing->oauth_provider)->toBe('google'); expect($existing->oauth_provider_id)->toBe('g-existing-1'); $this->assertAuthenticatedAs($existing); }); test('a deactivated verified account is not reactivated by google login', function () { /** @var TestCase $this */ $existing = User::factory()->create(['email' => 'deactivated@example.test', 'is_active' => false]); $existing->assignRole('customer'); fakeGoogleUser('g-deact-1', 'deactivated@example.test', 'Deactivated'); $this->get(route('oauth.google.callback')) ->assertRedirect(route('login')); expect($existing->fresh()->is_active)->toBeFalse(); $this->assertGuest(); }); test('a pending unverified registrant is onboarded via google', function () { /** @var TestCase $this */ $pending = User::factory()->unverified()->create([ 'email' => 'pending@example.test', 'is_active' => false, ]); fakeGoogleUser('g-pending-1', 'pending@example.test', 'Pending'); $this->get(route('oauth.google.callback')) ->assertRedirect(route('me.dashboard', absolute: false)); $pending->refresh(); expect($pending->is_active)->toBeTrue(); expect($pending->hasVerifiedEmail())->toBeTrue(); expect($pending->hasRole('customer'))->toBeTrue(); }); test('a failed google callback redirects back to login with an error', function () { /** @var TestCase $this */ $provider = Mockery::mock(Provider::class); $provider->shouldReceive('user')->andThrow(new RuntimeException('invalid state')); Socialite::shouldReceive('driver')->with('google')->andReturn($provider); $this->get(route('oauth.google.callback')) ->assertRedirect(route('login')); $this->assertGuest(); }); test('google login hands an account with confirmed two-factor to the challenge (Audit F02)', function () { /** @var TestCase $this */ $user = User::factory()->create(['email' => 'totp-google@example.test', 'is_active' => true]); $user->assignRole('customer'); $user->forceFill([ 'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()), 'two_factor_confirmed_at' => now(), ])->save(); fakeGoogleUser('g-totp-1', 'totp-google@example.test', 'Totp'); $this->get(route('oauth.google.callback')) ->assertRedirect(route('two-factor.challenge')); $this->assertGuest(); expect(session('login.id'))->toBe($user->id); expect($user->fresh()->last_login_at)->toBeNull(); }); /* * K-04 (Umsetzungsaudit 29.09.2026): Vorab-Übernahme. Ein Angreifer * registriert die Adresse der echten Pressestelle mit eigenem Passwort; die * Pressestelle meldet sich später mit Google an. Danach darf weder das * Passwort noch der Remember-Cookie noch eine offene Session des Angreifers * das Konto erreichen. */ test('a pre-registered account is no longer reachable with the old password, token or session after google verifies it', function () { /** @var TestCase $this */ $pending = User::factory()->unverified()->create([ 'email' => 'pressestelle@firma.test', 'password' => Hash::make('attacker-secret'), 'is_active' => false, 'remember_token' => 'attacker-remember-token', ]); $pending->createToken('attacker', ['press-releases:read']); // Offene Session des Angreifers (vor der Google-Anmeldung entstanden). $this->actingAs($pending); $this->flushSession(); fakeGoogleUser('g-takeover-1', 'pressestelle@firma.test', 'Pressestelle'); Auth::logout(); $this->get(route('oauth.google.callback')) ->assertRedirect(route('me.dashboard', absolute: false)); $pending->refresh(); expect($pending->password)->toBeNull() ->and($pending->remember_token)->not->toBe('attacker-remember-token') ->and($pending->tokens()->count())->toBe(0); // Die neue Google-Session bleibt gültig. $this->get(route('me.dashboard'))->assertOk(); $this->assertAuthenticatedAs($pending); // Die Session des Angreifers (ohne Anmeldezeitpunkt nach der Übernahme) nicht. Auth::logout(); $this->flushSession(); $this->actingAs($pending)->get(route('me.dashboard'))->assertRedirect(route('login')); $this->assertGuest(); // Und das alte Passwort führt nicht mehr hinein. Volt::test('auth.login') ->set('email', 'pressestelle@firma.test') ->set('password', 'attacker-secret') ->call('login') ->assertHasErrors(['email']); $this->assertGuest(); });