'mock_client_id', 'services.oidc.client_secret' => 'mock_client_secret', 'services.oidc.redirect' => 'https://mock-app.com/auth/oidc/callback', 'services.oidc.base_url' => 'https://mock-oidc-provider.com', ]); }); test('oidc socialite driver resolves to the correct provider class', function () { $driver = Socialite::driver('oidc'); expect($driver)->toBeInstanceOf(Provider::class); }); test('oidc login button is visible on login page when configured', function () { $response = $this->get(route('login')); $response->assertOk(); $response->assertSee('Log in with Single Sign-On'); }); test('oidc login button is hidden when client_id is not configured', function () { config(['services.oidc.client_id' => null]); $response = $this->get(route('login')); $response->assertOk(); $response->assertDontSee('Log in with Single Sign-On'); }); test('oidc redirect endpoint redirects to identity provider', function () { $mockDriver = Mockery::mock(); $mockDriver->shouldReceive('redirect') ->once() ->andReturn(redirect('https://mock-oidc-provider.com/auth')); Socialite::shouldReceive('driver') ->with('oidc') ->once() ->andReturn($mockDriver); $response = $this->get(route('oidc.redirect')); $response->assertRedirect('https://mock-oidc-provider.com/auth'); }); test('oidc callback links account and logs in existing user', function () { $user = User::factory()->create([ 'email' => 'existing@example.com', 'oidc_sub' => null, ]); $mockSocialiteUser = new SocialiteUser; $mockSocialiteUser->map([ 'id' => 'sub_12345', 'email' => 'existing@example.com', 'name' => 'Existing User', ]); $mockDriver = Mockery::mock(); $mockDriver->shouldReceive('user') ->once() ->andReturn($mockSocialiteUser); Socialite::shouldReceive('driver') ->with('oidc') ->once() ->andReturn($mockDriver); $response = $this->get(route('oidc.callback')); $response->assertSessionHasNoErrors(); $response->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticatedAs($user); expect($user->fresh()->oidc_sub)->toBe('sub_12345'); }); test('oidc callback logs in linked user directly by oidc_sub', function () { $user = User::factory()->create([ 'email' => 'existing@example.com', 'oidc_sub' => 'sub_12345', ]); $mockSocialiteUser = new SocialiteUser; $mockSocialiteUser->map([ 'id' => 'sub_12345', 'email' => 'existing@example.com', 'name' => 'Existing User', ]); $mockDriver = Mockery::mock(); $mockDriver->shouldReceive('user') ->once() ->andReturn($mockSocialiteUser); Socialite::shouldReceive('driver') ->with('oidc') ->once() ->andReturn($mockDriver); $response = $this->get(route('oidc.callback')); $response->assertSessionHasNoErrors(); $response->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticatedAs($user); }); test('oidc callback rejects authentication if user does not exist', function () { $mockSocialiteUser = new SocialiteUser; $mockSocialiteUser->map([ 'id' => 'sub_99999', 'email' => 'nonexistent@example.com', 'name' => 'New User', ]); $mockDriver = Mockery::mock(); $mockDriver->shouldReceive('user') ->once() ->andReturn($mockSocialiteUser); Socialite::shouldReceive('driver') ->with('oidc') ->once() ->andReturn($mockDriver); $response = $this->get(route('oidc.callback')); $response->assertRedirect(route('login')); $response->assertSessionHasErrors(['email' => 'Account not found. Please contact an administrator.']); $this->assertGuest(); }); test('oidc callback handles socialite exceptions gracefully', function () { $mockDriver = Mockery::mock(); $mockDriver->shouldReceive('user') ->once() ->andThrow(new Exception('OIDC server error')); Socialite::shouldReceive('driver') ->with('oidc') ->once() ->andReturn($mockDriver); $response = $this->get(route('oidc.callback')); $response->assertRedirect(route('login')); $response->assertSessionHasErrors(['email' => 'Authentication failed: OIDC server error']); $this->assertGuest(); });