[ 'label' => 'GitHub', 'icon' => 'i-simple-icons-github', 'enabled' => true, ]]); config(['auth-ui.providers.google' => [ 'label' => 'Google', 'icon' => 'i-simple-icons-google', 'enabled' => true, ]]); config(['auth-ui.features.registration' => true]); }); it('creates social users without a password', function () { $user = User::factory()->social()->create(); expect($user->password)->toBeNull(); expect($user->hasPassword())->toBeFalse(); }); it('creates regular users with a password', function () { $user = User::factory()->create(); expect($user->password)->not->toBeNull(); expect($user->hasPassword())->toBeTrue(); }); it('prevents brute force on social-only accounts', function () { $user = User::factory()->social()->create([ 'email' => 'social@example.com', ]); $attempts = ['password', '123456', 'admin', $user->email, $user->username, '']; foreach ($attempts as $password) { $this->post('/login', [ 'login' => 'social@example.com', 'password' => $password, ]); $this->assertGuest(); } }); it('does not expose whether a user is social-only via login error', function () { User::factory()->social()->create(['email' => 'social@example.com']); User::factory()->create(['email' => 'regular@example.com', 'password' => 'password123']); $socialResponse = $this->post('/login', [ 'login' => 'social@example.com', 'password' => 'wrongpassword', ]); $socialErrors = $socialResponse->assertSessionHasErrors('login') ->getSession()->get('errors')->get('login'); $this->refreshApplication(); $regularErrors = $this->post('/login', [ 'login' => 'regular@example.com', 'password' => 'wrongpassword', ])->assertSessionHasErrors('login') ->getSession()->get('errors')->get('login'); expect($socialErrors)->toEqual($regularErrors); }); it('allows social user to set a password later and login with it', function () { $user = User::factory()->social()->create([ 'email' => 'social@example.com', ]); expect($user->hasPassword())->toBeFalse(); $user->update(['password' => 'newpassword123']); expect($user->fresh()->hasPassword())->toBeTrue(); $this->post('/login', [ 'login' => 'social@example.com', 'password' => 'newpassword123', ])->assertRedirect('/dashboard'); $this->assertAuthenticatedAs($user); }); it('creates a social account record on first social login', function () { Socialite::fake('github', (new SocialiteUser)->map([ 'id' => 'github-123', 'name' => 'John Doe', 'email' => 'john@example.com', 'nickname' => 'johndoe', ])); $this->get('/auth/github/callback'); $this->assertDatabaseHas('social_accounts', [ 'provider' => 'github', 'provider_id' => 'github-123', ]); $user = User::where('email', 'john@example.com')->first(); expect($user->socialAccounts)->toHaveCount(1); expect($user->hasPassword())->toBeFalse(); expect($user->hasVerifiedEmail())->toBeTrue(); }); it('links provider to existing verified user on social login', function () { $user = User::factory()->create([ 'email' => 'john@example.com', 'password' => 'password123', 'email_verified_at' => now(), ]); Socialite::fake('github', (new SocialiteUser)->map([ 'id' => 'github-456', 'name' => 'John Doe', 'email' => 'john@example.com', 'nickname' => 'johndoe', ])); $this->get('/auth/github/callback'); $this->assertAuthenticatedAs($user); expect($user->fresh()->socialAccounts)->toHaveCount(1); expect($user->fresh()->hasPassword())->toBeTrue(); }); it('rejects social login linking to unverified existing user', function () { User::factory()->unverified()->create([ 'email' => 'unverified@example.com', 'password' => 'password123', ]); Socialite::fake('github', (new SocialiteUser)->map([ 'id' => 'github-hijack', 'name' => 'Attacker', 'email' => 'unverified@example.com', 'nickname' => 'attacker', ])); $this->get('/auth/github/callback')->assertRedirect('/login'); $this->assertGuest(); $this->assertDatabaseMissing('social_accounts', [ 'provider_id' => 'github-hijack', ]); }); it('recognizes returning social user by provider id', function () { $user = User::factory()->social()->create([ 'email' => 'john@example.com', ]); $user->socialAccounts()->create([ 'provider' => 'github', 'provider_id' => 'github-789', ]); Socialite::fake('github', (new SocialiteUser)->map([ 'id' => 'github-789', 'name' => 'John Doe', 'email' => 'john@example.com', 'nickname' => 'johndoe', ])); $this->get('/auth/github/callback'); $this->assertAuthenticatedAs($user); expect(SocialAccount::where('provider', 'github')->where('provider_id', 'github-789')->count())->toBe(1); }); it('prevents different provider with same email from creating duplicate user', function () { $user = User::factory()->social()->create([ 'email' => 'john@example.com', 'email_verified_at' => now(), ]); $user->socialAccounts()->create([ 'provider' => 'github', 'provider_id' => 'github-100', ]); Socialite::fake('google', (new SocialiteUser)->map([ 'id' => 'google-200', 'name' => 'John Doe', 'email' => 'john@example.com', 'nickname' => 'johndoe', ])); $this->get('/auth/google/callback'); $this->assertAuthenticatedAs($user); expect(User::where('email', 'john@example.com')->count())->toBe(1); expect($user->fresh()->socialAccounts)->toHaveCount(2); }); it('creates social account when completing profile after social login', function () { session()->put('socialite_user', [ 'email' => 'social@example.com', 'first_name' => 'Social', 'last_name' => 'User', 'suggested_username' => 'socialuser', 'provider' => 'github', 'provider_id' => 'github-complete-123', ]); $this->post('/complete-profile', [ 'username' => 'newsocialuser', 'first_name' => 'Social', 'last_name' => 'User', ])->assertRedirect('/dashboard'); $user = User::where('email', 'social@example.com')->first(); expect($user)->not->toBeNull(); expect($user->socialAccounts)->toHaveCount(1); expect($user->socialAccounts->first()->provider)->toBe('github'); expect($user->socialAccounts->first()->provider_id)->toBe('github-complete-123'); expect($user->hasVerifiedEmail())->toBeTrue(); expect($user->hasPassword())->toBeFalse(); }); it('rejects social login for disabled provider', function () { Socialite::fake('github'); config(['auth-ui.providers.github.enabled' => false]); $this->get('/auth/github/callback')->assertNotFound(); }); it('rejects social registration when registration is disabled', function () { config(['auth-ui.features.registration' => false]); Socialite::fake('github', (new SocialiteUser)->map([ 'id' => 'github-new', 'name' => 'New User', 'email' => 'new@example.com', 'nickname' => 'newuser', ])); $this->get('/auth/github/callback')->assertRedirect('/login'); $this->assertDatabaseMissing('users', [ 'email' => 'new@example.com', ]); });