diff --git a/resources/js/components/common/__tests__/ErrorBoundary.test.ts b/resources/js/components/common/__tests__/ErrorBoundary.test.ts
new file mode 100644
index 0000000..738d7ea
--- /dev/null
+++ b/resources/js/components/common/__tests__/ErrorBoundary.test.ts
@@ -0,0 +1,34 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import ErrorBoundary from '../ErrorBoundary.vue'
+
+// Stub UIcon and UButton since they come from Nuxt UI
+const stubs = {
+ UIcon: { template: '' },
+ UButton: { template: '' },
+}
+
+describe('errorBoundary', () => {
+ it('renders slot content when no error', () => {
+ const wrapper = mount(ErrorBoundary, {
+ slots: {
+ default: '
Hello World
',
+ },
+ global: { stubs },
+ })
+
+ expect(wrapper.text()).toContain('Hello World')
+ })
+
+ it('does not show error UI by default', () => {
+ const wrapper = mount(ErrorBoundary, {
+ slots: {
+ default: 'Content
',
+ },
+ global: { stubs },
+ })
+
+ expect(wrapper.text()).not.toContain('Something went wrong')
+ expect(wrapper.text()).toContain('Content')
+ })
+})
diff --git a/resources/js/composables/__tests__/useAuth.test.ts b/resources/js/composables/__tests__/useAuth.test.ts
new file mode 100644
index 0000000..983767a
--- /dev/null
+++ b/resources/js/composables/__tests__/useAuth.test.ts
@@ -0,0 +1,85 @@
+import { describe, expect, it, vi } from 'vitest'
+
+const mockPageProps = {
+ auth: { user: null as any },
+ flash: { success: null, error: null, message: null, status: null },
+ authConfig: {
+ appName: 'TestApp',
+ features: {
+ registration: true,
+ password_reset: true,
+ remember_me: true,
+ email_verification: false,
+ },
+ login: { title: 'Login', description: '', icon: '', submit_label: 'Sign in' },
+ register: { title: 'Register', description: '', icon: '', submit_label: 'Sign up' },
+ forgotPassword: { title: 'Forgot', description: '', icon: '', submit_label: 'Send' },
+ resetPassword: { title: 'Reset', description: '', icon: '', submit_label: 'Reset' },
+ providers: [],
+ legal: { terms_url: null, privacy_url: null, show_in_register: false },
+ },
+}
+
+vi.mock('@inertiajs/vue3', () => ({
+ usePage: () => ({ props: mockPageProps }),
+}))
+
+const { useAuth } = await import('../useAuth')
+
+describe('useAuth', () => {
+ it('returns null user when not authenticated', () => {
+ mockPageProps.auth.user = null
+
+ const { user, isAuthenticated } = useAuth()
+
+ expect(user.value).toBeNull()
+ expect(isAuthenticated.value).toBe(false)
+ })
+
+ it('returns user when authenticated', () => {
+ mockPageProps.auth.user = {
+ id: 1,
+ username: 'testuser',
+ first_name: 'Test',
+ last_name: 'User',
+ full_name: 'Test User',
+ email: 'test@example.com',
+ email_verified_at: '2026-01-01',
+ created_at: '2026-01-01',
+ updated_at: '2026-01-01',
+ }
+
+ const { user, isAuthenticated } = useAuth()
+
+ expect(user.value).not.toBeNull()
+ expect(user.value!.username).toBe('testuser')
+ expect(isAuthenticated.value).toBe(true)
+ })
+
+ it('returns flash messages', () => {
+ mockPageProps.flash.success = 'It worked!'
+
+ const { flash } = useAuth()
+
+ expect(flash.value.success).toBe('It worked!')
+ })
+
+ it('returns auth config', () => {
+ const { config } = useAuth()
+
+ expect(config.value.appName).toBe('TestApp')
+ expect(config.value.features.registration).toBe(true)
+ expect(config.value.features.email_verification).toBe(false)
+ })
+
+ it('returns providers from config', () => {
+ mockPageProps.authConfig.providers = [
+ { key: 'github', label: 'GitHub', icon: 'i-simple-icons-github' },
+ ]
+
+ const { config } = useAuth()
+
+ expect(config.value.providers).toHaveLength(1)
+ expect(config.value.providers[0].key).toBe('github')
+ })
+})
diff --git a/resources/js/validation/__tests__/auth.test.ts b/resources/js/validation/__tests__/auth.test.ts
new file mode 100644
index 0000000..497ba2f
--- /dev/null
+++ b/resources/js/validation/__tests__/auth.test.ts
@@ -0,0 +1,169 @@
+import * as v from 'valibot'
+import { describe, expect, it } from 'vitest'
+import {
+ completeProfileSchema,
+ forgotPasswordSchema,
+ loginSchema,
+ registerSchema,
+ resetPasswordSchema,
+} from '../auth'
+
+function validate>>(schema: T, data: unknown) {
+ return v.safeParse(schema, data)
+}
+
+describe('loginSchema', () => {
+ it('accepts valid login data', () => {
+ const result = validate(loginSchema, { login: 'user@test.com', password: 'secret' })
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects empty login', () => {
+ const result = validate(loginSchema, { login: '', password: 'secret' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects empty password', () => {
+ const result = validate(loginSchema, { login: 'user', password: '' })
+ expect(result.success).toBe(false)
+ })
+
+ it('accepts optional remember field', () => {
+ const result = validate(loginSchema, { login: 'user', password: 'secret', remember: true })
+ expect(result.success).toBe(true)
+ })
+})
+
+describe('registerSchema', () => {
+ const validData = {
+ username: 'testuser',
+ first_name: 'Test',
+ last_name: 'User',
+ email: 'test@example.com',
+ password: 'password123',
+ password_confirmation: 'password123',
+ }
+
+ it('accepts valid registration data', () => {
+ const result = validate(registerSchema, validData)
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects username shorter than 3 characters', () => {
+ const result = validate(registerSchema, { ...validData, username: 'ab' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects username with spaces', () => {
+ const result = validate(registerSchema, { ...validData, username: 'test user' })
+ expect(result.success).toBe(false)
+ })
+
+ it('accepts username with dashes and underscores', () => {
+ const result = validate(registerSchema, { ...validData, username: 'test-user_123' })
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects invalid email', () => {
+ const result = validate(registerSchema, { ...validData, email: 'not-an-email' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects password shorter than 8 characters', () => {
+ const result = validate(registerSchema, { ...validData, password: 'short', password_confirmation: 'short' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects mismatched passwords', () => {
+ const result = validate(registerSchema, { ...validData, password_confirmation: 'different123' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects empty first name', () => {
+ const result = validate(registerSchema, { ...validData, first_name: '' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects empty last name', () => {
+ const result = validate(registerSchema, { ...validData, last_name: '' })
+ expect(result.success).toBe(false)
+ })
+})
+
+describe('forgotPasswordSchema', () => {
+ it('accepts valid email', () => {
+ const result = validate(forgotPasswordSchema, { email: 'test@example.com' })
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects empty email', () => {
+ const result = validate(forgotPasswordSchema, { email: '' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects invalid email', () => {
+ const result = validate(forgotPasswordSchema, { email: 'not-email' })
+ expect(result.success).toBe(false)
+ })
+})
+
+describe('resetPasswordSchema', () => {
+ const validData = {
+ email: 'test@example.com',
+ password: 'newpassword123',
+ password_confirmation: 'newpassword123',
+ }
+
+ it('accepts valid reset data', () => {
+ const result = validate(resetPasswordSchema, validData)
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects mismatched passwords', () => {
+ const result = validate(resetPasswordSchema, { ...validData, password_confirmation: 'different' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects password shorter than 8 characters', () => {
+ const result = validate(resetPasswordSchema, { ...validData, password: 'short', password_confirmation: 'short' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects invalid email', () => {
+ const result = validate(resetPasswordSchema, { ...validData, email: 'bad' })
+ expect(result.success).toBe(false)
+ })
+})
+
+describe('completeProfileSchema', () => {
+ const validData = {
+ username: 'newuser',
+ first_name: 'New',
+ last_name: 'User',
+ }
+
+ it('accepts valid profile data', () => {
+ const result = validate(completeProfileSchema, validData)
+ expect(result.success).toBe(true)
+ })
+
+ it('rejects username shorter than 3 characters', () => {
+ const result = validate(completeProfileSchema, { ...validData, username: 'ab' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects username with special characters', () => {
+ const result = validate(completeProfileSchema, { ...validData, username: 'user@name' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects empty first name', () => {
+ const result = validate(completeProfileSchema, { ...validData, first_name: '' })
+ expect(result.success).toBe(false)
+ })
+
+ it('rejects empty last name', () => {
+ const result = validate(completeProfileSchema, { ...validData, last_name: '' })
+ expect(result.success).toBe(false)
+ })
+})
diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php
new file mode 100644
index 0000000..7165c4c
--- /dev/null
+++ b/tests/Feature/Auth/EmailVerificationTest.php
@@ -0,0 +1,154 @@
+ 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');
+});
diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php
new file mode 100644
index 0000000..e254968
--- /dev/null
+++ b/tests/Feature/Auth/LoginTest.php
@@ -0,0 +1,114 @@
+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();
+});
diff --git a/tests/Feature/Auth/SocialLoginSecurityTest.php b/tests/Feature/Auth/SocialLoginSecurityTest.php
new file mode 100644
index 0000000..eb14fce
--- /dev/null
+++ b/tests/Feature/Auth/SocialLoginSecurityTest.php
@@ -0,0 +1,254 @@
+ [
+ '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',
+ ]);
+});
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
index 8364a84..d2411e5 100644
--- a/tests/Feature/ExampleTest.php
+++ b/tests/Feature/ExampleTest.php
@@ -1,19 +1,5 @@
get('/');
-
- $response->assertStatus(200);
- }
-}
+test('the application returns a successful response', function () {
+ $this->get('/')->assertSuccessful();
+});