test: add auth, social login, email verification, and validation tests

This commit is contained in:
2026-03-19 23:17:32 +01:00
parent dd1e3d9053
commit 2351718939
7 changed files with 813 additions and 17 deletions

View File

@@ -0,0 +1,154 @@
<?php
use App\Models\User;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\URL;
uses(RefreshDatabase::class);
it('does not send verification email when feature is disabled', function () {
config(['auth-ui.features.email_verification' => false]);
Notification::fake();
$this->post('/register', [
'username' => 'testuser',
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
Notification::assertNothingSent();
});
it('sends verification email when feature is enabled', function () {
config(['auth-ui.features.email_verification' => true]);
Notification::fake();
$this->post('/register', [
'username' => 'testuser',
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$user = User::where('email', 'test@example.com')->first();
Notification::assertSentTo($user, VerifyEmail::class);
});
it('shows verification notice to unverified users when feature is enabled', function () {
config(['auth-ui.features.email_verification' => true]);
$user = User::factory()->unverified()->create();
$this->actingAs($user)
->get('/email/verify')
->assertSuccessful();
});
it('redirects verified users from verification notice', function () {
config(['auth-ui.features.email_verification' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->get('/email/verify')
->assertRedirect('/dashboard');
});
it('verifies email with valid signed url', function () {
config(['auth-ui.features.email_verification' => true]);
$user = User::factory()->unverified()->create();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1($user->email)]
);
$this->actingAs($user)
->get($verificationUrl)
->assertRedirect('/dashboard');
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
});
it('rejects verification with invalid hash', function () {
config(['auth-ui.features.email_verification' => true]);
$user = User::factory()->unverified()->create();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1('wrong@email.com')]
);
$this->actingAs($user)
->get($verificationUrl)
->assertForbidden();
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});
it('allows resending verification email', function () {
config(['auth-ui.features.email_verification' => true]);
Notification::fake();
$user = User::factory()->unverified()->create();
$this->actingAs($user)
->post('/email/verification-notification')
->assertRedirect()
->assertSessionHas('status', 'verification-link-sent');
Notification::assertSentTo($user, VerifyEmail::class);
});
it('does not register verification routes when feature is disabled', function () {
config(['auth-ui.features.email_verification' => false]);
$user = User::factory()->unverified()->create();
$this->actingAs($user)
->get('/email/verify')
->assertNotFound();
});
it('redirects to verification notice after registration when feature is enabled', function () {
config(['auth-ui.features.email_verification' => true]);
Notification::fake();
$this->post('/register', [
'username' => 'newuser',
'first_name' => 'New',
'last_name' => 'User',
'email' => 'new@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
])->assertRedirect('/email/verify');
});
it('redirects to home after registration when feature is disabled', function () {
config(['auth-ui.features.email_verification' => false]);
$this->post('/register', [
'username' => 'newuser',
'first_name' => 'New',
'last_name' => 'User',
'email' => 'new@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
])->assertRedirect('/dashboard');
});

View File

@@ -0,0 +1,114 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('allows login with email and password', function () {
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => 'password123',
]);
$this->post('/login', [
'login' => 'test@example.com',
'password' => 'password123',
])->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
});
it('allows login with username and password', function () {
$user = User::factory()->create([
'username' => 'testuser',
'password' => 'password123',
]);
$this->post('/login', [
'login' => 'testuser',
'password' => 'password123',
])->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
});
it('allows case-insensitive username login', function () {
$user = User::factory()->create([
'username' => 'TestUser',
'password' => 'password123',
]);
$this->post('/login', [
'login' => 'testuser',
'password' => 'password123',
])->assertRedirect('/dashboard');
$this->assertAuthenticatedAs($user);
});
it('rejects login with wrong password', function () {
User::factory()->create([
'email' => 'test@example.com',
'password' => 'password123',
]);
$this->post('/login', [
'login' => 'test@example.com',
'password' => 'wrongpassword',
])->assertSessionHasErrors('login');
$this->assertGuest();
});
it('rejects login with nonexistent user', function () {
$this->post('/login', [
'login' => 'nobody@example.com',
'password' => 'password123',
])->assertSessionHasErrors('login');
$this->assertGuest();
});
it('blocks password login for social-only users', function () {
User::factory()->social()->create([
'email' => 'social@example.com',
]);
$this->post('/login', [
'login' => 'social@example.com',
'password' => 'anything',
])->assertSessionHasErrors('login');
$this->assertGuest();
});
it('blocks password login for social-only users with any password attempt', function () {
User::factory()->social()->create([
'email' => 'social@example.com',
]);
$passwords = ['', 'password', 'null', '0', 'false'];
foreach ($passwords as $password) {
$this->post('/login', [
'login' => 'social@example.com',
'password' => $password,
]);
$this->assertGuest();
}
});
it('blocks password login for social-only users via username', function () {
User::factory()->social()->create([
'username' => 'socialuser',
]);
$this->post('/login', [
'login' => 'socialuser',
'password' => 'anything',
])->assertSessionHasErrors('login');
$this->assertGuest();
});

View 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',
]);
});

View File

@@ -1,19 +1,5 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
test('the application returns a successful response', function () {
$this->get('/')->assertSuccessful();
});