test: add auth, social login, email verification, and validation tests
This commit is contained in:
254
tests/Feature/Auth/SocialLoginSecurityTest.php
Normal file
254
tests/Feature/Auth/SocialLoginSecurityTest.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Laravel\Socialite\Two\User as SocialiteUser;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['auth-ui.providers.github' => [
|
||||
'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',
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user