WS-6: Google-Login via Laravel Socialite
- Socialite installiert; oauth_provider/oauth_provider_id an users (Migration). - GoogleController (redirect/callback) + SocialAuthService: De-Dup über E-Mail, neuer User aktiv + verifiziert + customer (Verifizierung über den Google- Kanal), offener Selbst-Registrierer wird onboardet, deaktivierter Account wird NICHT reaktiviert. Abschluss über die gemeinsame LoginRedirect-Logik (rollengerecht, 403-sicher). - Routen /auth/google/redirect + /auth/google/callback (guest), "Mit Google anmelden/registrieren"-Buttons auf Login und Register. - config/services.php google + .env.example-Keys; Sicherheits-/Deployment-Doku ergänzt (Keys, Redirect-URI, Migration). Tests: neuer User, De-Dup bestehender User, deaktivierter Account blockiert, unverifizierter Registrierer onboardet, fehlgeschlagener Callback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ae79d5bee4
commit
068a5a4b49
13 changed files with 715 additions and 1 deletions
104
tests/Feature/Auth/GoogleLoginTest.php
Normal file
104
tests/Feature/Auth/GoogleLoginTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Laravel\Socialite\Contracts\Provider;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Laravel\Socialite\Two\User as SocialiteUser;
|
||||
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');
|
||||
$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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue