artisan('saml:generate-keys'); } $this->seed(Database\Seeders\DatabaseSeeder::class); // Get seeded protocol, provider, and status $this->protocol = ConnectionProtocol::query()->where('name', 'saml')->firstOrFail(); $this->provider = ConnectionProvider::query()->where('slug', 'azure-fd')->firstOrFail(); $this->status = ConnectionStatus::query()->where('name', 'connected')->firstOrFail(); // Create a mock SAML App $this->samlApp = ConnectedApp::query()->create([ 'name' => 'Microsoft Office 365', 'slug' => 'microsoft-office-365', 'connection_protocol_id' => $this->protocol->id, 'connection_provider_id' => $this->provider->id, 'connection_status_id' => $this->status->id, 'settings' => [ 'saml' => [ 'entity_id' => 'urn:federation:MicrosoftOnline', 'acs_url' => 'https://login.microsoftonline.com/login.srf', ], ], ]); // Create a mock User $this->user = User::factory()->create([ 'email' => 'testuser@company.com', 'immutable_id' => 'user-123-immutable', ]); }); test('saml metadata endpoint returns active public certificate and sso location', function (): void { $response = $this->get(route('saml.metadata')); $response->assertStatus(200); $response->assertHeader('Content-Type', 'application/xml; charset=utf-8'); $content = $response->getContent(); expect($content)->toContain('EntityDescriptor') ->toContain('IDPSSODescriptor') ->toContain('SingleSignOnService') ->toContain(route('saml.sso')) ->toContain('X509Certificate'); }); test('saml sso endpoint aborts if missing SAMLRequest', function (): void { $response = $this->get(route('saml.sso')); $response->assertStatus(400); }); test('saml sso endpoint redirects unauthenticated users to login page and remembers request', function (): void { $xml = 'urn:federation:MicrosoftOnline'; $samlRequest = base64_encode(gzdeflate($xml)); $relayState = 'https://teams.microsoft.com/'; $response = $this->get(route('saml.sso', [ 'SAMLRequest' => $samlRequest, 'RelayState' => $relayState, ])); $response->assertRedirect(route('login')); // Assert session retains pending details expect(session('saml_pending_request'))->toBe($samlRequest); expect(session('saml_pending_relay_state'))->toBe($relayState); }); test('saml sso endpoint signs and redirects authenticated users via POST binding', function (): void { $xml = 'urn:federation:MicrosoftOnline'; $samlRequest = base64_encode(gzdeflate($xml)); $relayState = 'https://teams.microsoft.com/'; // Log in the user $this->actingAs($this->user); $response = $this->get(route('saml.sso', [ 'SAMLRequest' => $samlRequest, 'RelayState' => $relayState, ])); $response->assertStatus(200); $response->assertViewIs('saml.post_response'); $response->assertViewHasAll([ 'appName' => 'Microsoft Office 365', 'acsUrl' => 'https://login.microsoftonline.com/login.srf', 'relayState' => $relayState, ]); $content = $response->getContent(); expect($content)->toContain('name="SAMLResponse"') ->toContain('name="RelayState"') ->toContain('value="'.$relayState.'"'); // Verify session was cleared expect(session('saml_pending_request'))->toBeNull(); }); test('user sso settings and immutable id can be assigned via service and mapped to DTO', function (): void { $userService = app(App\Services\UserService::class); // Assign roles and update immutable ID $userService->assignRoles($this->user->id, [], 'new-custom-sso-immutable-id'); // Retrieve fresh user and assert $freshUser = $this->user->fresh(); expect($freshUser->immutable_id)->toBe('new-custom-sso-immutable-id'); // Assert UserIndexData maps it correctly $dto = App\Data\User\UserIndexData::fromModel($freshUser); expect($dto->immutableId)->toBe('new-custom-sso-immutable-id'); }); test('saml settings field is only visible if selected roles contain a SAML app', function (): void { // Create a role with a SAML connected app $samlRole = App\Models\Role::findOrCreate('SAML User'); $samlRole->update(['priority' => 10]); $samlRole->apps()->attach($this->samlApp->id, ['duration' => 365]); // Create a role with no connected apps $emptyRole = App\Models\Role::findOrCreate('Empty User'); $emptyRole->update(['priority' => 20]); // Log in the administrator so they can view users $admin = User::where('email', 'admin@example.com')->firstOrFail(); $this->actingAs($admin); // 1. Livewire component with no roles selected should return false for showSamlSettings Livewire::test('pages::access-manager.users.index') ->set('form.roleIds', []) ->assertSet('showSamlSettings', false); // 2. Livewire component with emptyRole selected should return false Livewire::test('pages::access-manager.users.index') ->set('form.roleIds', [$emptyRole->id]) ->assertSet('showSamlSettings', false); // 3. Livewire component with samlRole selected should return true Livewire::test('pages::access-manager.users.index') ->set('form.roleIds', [$samlRole->id]) ->assertSet('showSamlSettings', true); });