How to Test Laravel Socialite
- Introduction
- Socialite Controller: Overview
- Laravel Socialite Authentication Test Suite
- Final Thoughts
- Tips for Testing Socialite:
Introduction
Social login is one of the most convenient ways for users to access your application. Laravel makes this simple with Laravel Socialite, a first-party package that abstracts away OAuth complexities.
In this post, we’ll walk through a real-world implementation of a Socialite controller and, more importantly, demystify the tests behind it. Testing third-party authentication might seem tricky, but with mocking and smart assertions, it’s absolutely doable.
Socialite Controller: Overview
Here is the actual SocialiteController class we are testing:
<?php declare(strict_types=1); namespace App\Http\Controllers\Auth; use App\Models\User;use Illuminate\Auth\Events\Verified;use Illuminate\Database\UniqueConstraintViolationException;use Illuminate\Http\RedirectResponse;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Facades\Validator;use Illuminate\Support\Str;use Illuminate\Validation\ValidationException;use Laravel\Socialite\Facades\Socialite; class SocialiteController{ // route: /auth/{provider}/redirect public function redirect(string $provider): RedirectResponse { $this->validate(compact('provider')); return Socialite::driver($provider)->redirect(); } // route: /auth/{provider}/callback public function handleCallback(string $provider): RedirectResponse { $this->validate(compact('provider')); $user = Socialite::driver($provider)->user(); try { $user = User::updateOrCreate([ 'provider' => $provider, 'provider_id' => $user->id, ], [ 'name' => $user->name ?? $user->nickname ?? 'N/A', 'email' => $user->email, 'password' => Str::random(), 'avatar_url' => $user->avatar, ]); } catch (UniqueConstraintViolationException $exception) { throw ValidationException::withMessages([ 'email' => 'This email is already taken.', ]); } if (! $user->hasVerifiedEmail()) { $user->markEmailAsVerified(); event(new Verified($user)); } Auth::login($user, true); return redirect()->intended(route('dashboard')); } protected function validate(array $data): void { Validator::make($data, [ 'provider' => ['required', 'string', 'in:github,google'], ])->validate(); }}
This controller:
- Redirects the user to the OAuth provider
- Handles the callback, updates or creates the user
- Handles email verification if needed
- Logs the user in
Now let’s look at how we thoroughly test this logic.
Laravel Socialite Authentication Test Suite
Here is the full test file along with detailed explanations for each case:
<?php declare(strict_types=1); use App\Models\User;use Illuminate\Auth\Events\Verified;use Illuminate\Support\Arr;use Illuminate\Support\Facades\Event;use Illuminate\Support\Str;use Laravel\Socialite\Contracts\Provider;use Laravel\Socialite\Contracts\User as SocialiteUser;use Laravel\Socialite\Facades\Socialite;use Mockery\MockInterface; // Test: Redirects to provider test('redirects to auth provider', function () { $this ->get(route('socialite.redirect', 'github')) ->assertValid() ->assertRedirect();}); // Test: Invalid provider name fails validation test('provider is invalid', function () { $this ->get(route('socialite.redirect', Str::random())) ->assertInvalid(['provider']);}); // Test: Handles callback and creates a new user test('handles callback from auth provider and creates a user', function () { $provider = Arr::random(['github', 'google']); $user = User::factory()->unverified()->make(); Event::fake(); $socialiteUser = $this->mock(SocialiteUser::class, function (MockInterface $mock) use ($user) { $mock->id = fake()->randomNumber(3); $mock->name = $user->name; $mock->email = $user->email; $mock->avatar = $user->avatar_url; }); $socialiteProvider = $this->mock(Provider::class, function (MockInterface $mock) use ($socialiteUser) { $mock->shouldReceive('user')->andReturn($socialiteUser); }); Socialite::shouldReceive('driver')->with($provider)->andReturn($socialiteProvider); $this->assertDatabaseCount('users', 0); $this ->get(route('socialite.callback', $provider)) ->assertRedirect(route('dashboard')); Event::assertDispatched(Verified::class); $this ->assertAuthenticated() ->assertDatabaseCount('users', 1) ->assertDatabaseHas('users', [ 'provider' => $provider, 'provider_id' => $socialiteUser->id, 'name' => $socialiteUser->name, 'email' => $socialiteUser->email, 'avatar_url' => $socialiteUser->avatar, ]) ->assertTrue(User::firstWhere('email', $user->email)->hasVerifiedEmail());}); // Test: Email already exists test('user will not be created if already exists', function () { $provider = Arr::random(['github', 'google']); $user = User::factory()->create(); $this->assertDatabaseCount('users', 1); $socialiteUser = $this->mock(SocialiteUser::class, function (MockInterface $mock) use ($user) { $mock->id = fake()->randomNumber(3); $mock->name = $user->name; $mock->email = $user->email; $mock->avatar = $user->avatar_url; }); $socialiteProvider = $this->mock(Provider::class, function (MockInterface $mock) use ($socialiteUser) { $mock->shouldReceive('user')->andReturn($socialiteUser); }); Socialite::shouldReceive('driver')->with($provider)->andReturn($socialiteProvider); $this ->get(route('socialite.callback', $provider)) ->assertInvalid([ 'email' => 'This email is already taken.', ]); $this->assertDatabaseCount('users', 1);}); // Test: Log in user if registered with the same provider test('user will login and will not be created again if already registered with the same provider', function () { $user = User::factory()->viaSocialite()->create(); $newName = fake()->name(); $socialiteUser = $this->mock(SocialiteUser::class, function (MockInterface $mock) use ($user, $newName) { $mock->id = $user->provider_id; $mock->name = $newName; $mock->email = $user->email; $mock->avatar = $user->avatar_url; }); $socialiteProvider = $this->mock(Provider::class, function (MockInterface $mock) use ($socialiteUser) { $mock->shouldReceive('user')->andReturn($socialiteUser); }); Socialite::shouldReceive('driver')->with($user->provider)->andReturn($socialiteProvider); $this->assertDatabaseCount('users', 1); $this ->get(route('socialite.callback', $user->provider)) ->assertRedirect(route('dashboard')); $this ->assertAuthenticated() ->assertDatabaseCount('users', 1) ->assertDatabaseHas('users', [ 'provider' => $user->provider, 'provider_id' => $socialiteUser->id, 'name' => $newName, 'email' => $socialiteUser->email, 'avatar_url' => $socialiteUser->avatar, ]);}); // Test: Same email but different provider test('user will not be created if already registered with another provider', function () { $user = User::factory()->viaSocialite()->create(); $provider = collect(['google', 'github'])->diff([$user->provider])->random(); $this->assertDatabaseCount('users', 1); $socialiteUser = $this->mock(SocialiteUser::class, function (MockInterface $mock) use ($user) { $mock->id = fake()->randomNumber(3); $mock->name = $user->name; $mock->email = $user->email; $mock->avatar = $user->avatar_url; }); $socialiteProvider = $this->mock(Provider::class, function (MockInterface $mock) use ($socialiteUser) { $mock->shouldReceive('user')->andReturn($socialiteUser); }); Socialite::shouldReceive('driver')->with($provider)->andReturn($socialiteProvider); $this ->get(route('socialite.callback', $provider)) ->assertInvalid([ 'email' => 'This email is already taken.', ]); $this->assertDatabaseCount('users', 1);});
Each of these tests covers a real-world scenario:
- ✅ Happy paths (successful creation, successful login)
- ❌ Failure paths (duplicate email from form or mismatched providers)
- ✨ User experience improvements (e.g., updated profile info)
Final Thoughts
Socialite makes social logins easy, but without proper testing, you risk account conflicts, missing verification, and hard-to-debug login issues. With a few well-crafted mocks and assertions, you can simulate the entire flow and cover edge cases confidently.
Tips for Testing Socialite:
- Use Laravel's mocking and facades to simulate third-party services.
- Validate both positive and negative scenarios.
- Don’t forget to test for email duplication and provider mismatches.
Stay Updated.
I'll you email you as soon as new, fresh content is published.