create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'operator', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '11111111-1111-1111-1111-111111111111', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'operator'], ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Acme connection', 'is_default' => true, ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => 'top-secret-client-secret', ], ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->assertStatus(403); }); it('returns 404 when a non-member attempts inline connection update', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '22222222-2222-2222-2222-222222222222', 'status' => Tenant::STATUS_ONBOARDING, ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Acme connection', 'is_default' => true, ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => 'top-secret-client-secret', ], ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->assertStatus(404); }); it('updates connection inline, invalidates verification state, and writes audit metadata without secrets', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '33333333-3333-3333-3333-333333333333', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Acme connection', 'is_default' => true, ]); $secret = 'top-secret-client-secret'; ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => $secret, ], ]); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $workspace->getKey(), 'type' => 'provider.connection.check', ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'verify', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), 'verification_operation_run_id' => (int) $run->getKey(), 'bootstrap_operation_runs' => [123, 456], 'bootstrap_operation_types' => ['inventory_sync'], ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Updated name', 'client_id' => 'old-client-id', ]) ->assertSuccessful(); $connection->refresh(); expect($connection->display_name)->toBe('Updated name'); $credential = $connection->credential; expect($credential)->not->toBeNull(); expect($credential?->payload['client_id'] ?? null)->toBe('old-client-id'); expect($credential?->payload['client_secret'] ?? null)->toBe($secret); $session->refresh(); expect($session->state['verification_operation_run_id'] ?? null)->toBeNull(); expect($session->state['bootstrap_operation_runs'] ?? null)->toBeNull(); expect($session->state['bootstrap_operation_types'] ?? null)->toBeNull(); expect($session->state['connection_recently_updated'] ?? null)->toBeTrue(); $audit = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('action', 'provider_connection.updated') ->latest('id') ->first(); expect($audit)->not->toBeNull(); $encodedMetadata = json_encode($audit?->metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); expect($encodedMetadata)->not->toContain($secret); }); it('requires a new secret when changing the client id inline during onboarding', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '66666666-6666-6666-6666-666666666666', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Acme connection', 'is_default' => true, ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => 'top-secret-client-secret', ], ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Updated name', 'client_id' => 'new-client-id', 'client_secret' => '', ]) ->assertHasErrors([ 'client_secret' => 'Enter a dedicated client secret when enabling dedicated mode or changing the App (client) ID.', ]); $connection->refresh(); expect($connection->display_name)->toBe('Acme connection'); expect($connection->credential?->payload['client_id'] ?? null)->toBe('old-client-id'); }); it('rotates the secret inline when a new client secret is provided during onboarding edit', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '77777777-7777-7777-7777-777777777777', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Acme connection', 'is_default' => true, ]); $oldSecret = 'top-secret-client-secret'; $newSecret = 'brand-new-client-secret'; ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => $oldSecret, ], ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Updated name', 'client_id' => 'new-client-id', 'client_secret' => $newSecret, ]) ->assertSuccessful(); $connection->refresh(); expect($connection->display_name)->toBe('Updated name'); expect($connection->credential?->payload['client_id'] ?? null)->toBe('new-client-id'); expect($connection->credential?->payload['client_secret'] ?? null)->toBe($newSecret); $audit = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('action', 'provider_connection.updated') ->latest('id') ->first(); expect($audit)->not->toBeNull(); $encodedMetadata = json_encode($audit?->metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); expect($encodedMetadata)->not->toContain($oldSecret); expect($encodedMetadata)->not->toContain($newSecret); }); it('allows workspace owners to enable a dedicated override inline for the selected connection', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '88888888-1111-1111-1111-111111111111', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $connection = ProviderConnection::factory()->platform()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Platform connection', 'is_default' => true, ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Dedicated connection', 'connection_type' => ProviderConnectionType::Dedicated->value, 'client_id' => 'inline-dedicated-client', 'client_secret' => 'inline-dedicated-secret', ]) ->assertSuccessful(); $connection->refresh(); expect($connection->connection_type)->toBe(ProviderConnectionType::Dedicated) ->and($connection->credential)->not->toBeNull() ->and($connection->credential?->payload['client_id'] ?? null)->toBe('inline-dedicated-client'); }); it('returns 403 when workspace managers try to enable a dedicated override inline', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'manager', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '99999999-1111-1111-1111-111111111111', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'manager'], ]); $connection = ProviderConnection::factory()->platform()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Platform connection', 'is_default' => true, ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Dedicated connection', 'connection_type' => ProviderConnectionType::Dedicated->value, 'client_id' => 'forbidden-inline-client', 'client_secret' => 'forbidden-inline-secret', ]) ->assertForbidden(); }); it('returns 404 when attempting to inline-edit a connection belonging to a different tenant', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '44444444-4444-4444-4444-444444444444', 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $otherTenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '55555555-5555-5555-5555-555555555555', 'status' => Tenant::STATUS_ONBOARDING, ]); $connection = ProviderConnection::factory()->dedicated()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $otherTenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $otherTenant->tenant_id, 'display_name' => 'Other tenant connection', 'is_default' => true, ]); ProviderCredential::factory()->create([ 'provider_connection_id' => (int) $connection->getKey(), 'payload' => [ 'client_id' => 'old-client-id', 'client_secret' => 'top-secret-client-secret', ], ]); $session = TenantOnboardingSession::create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]) ->call('updateSelectedProviderConnectionInline', (int) $connection->getKey(), [ 'display_name' => 'Updated name', 'client_id' => 'new-client-id', ]) ->assertStatus(404); });