presseportale/tests/Feature/Auth/GoogleLoginTest.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

180 lines
6.5 KiB
PHP

<?php
use App\Models\User;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\User as SocialiteUser;
use Livewire\Livewire;
use PragmaRX\Google2FA\Google2FA;
use Tests\TestCase;
beforeEach(function () {
$this->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.
Livewire::test('auth.login')
->set('email', 'pressestelle@firma.test')
->set('password', 'attacker-secret')
->call('login')
->assertHasErrors(['email']);
$this->assertGuest();
});