## Summary - add canonical onboarding lifecycle and checkpoint fields plus optimistic locking versioning for managed tenant onboarding drafts - introduce centralized onboarding lifecycle and mutation services and route wizard mutations through version-checked writes - convert Verify Access and Bootstrap into live checkpoint-driven wizard states with conditional polling and updated browser/feature/unit coverage - add Spec Kit artifacts for feature 140, including spec, plan, tasks, research, data model, quickstart, checklist, and logical contract ## Validation - branch was committed and pushed cleanly - focused tests and formatting were updated during implementation work - full validation was not re-run as part of this final git/PR step ## Notes - base branch: `dev` - feature branch: `140-onboarding-lifecycle-operation-checkpoints-concurrency-mvp` - outstanding follow-up items, if any, remain tracked in `specs/140-onboarding-lifecycle-operation-checkpoints-concurrency-mvp/tasks.md` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #169
84 lines
2.9 KiB
PHP
84 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Livewire\Livewire;
|
|
|
|
it('rejects stale same-draft writes when the draft is open in two tabs', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => Tenant::STATUS_ONBOARDING,
|
|
]);
|
|
$user = User::factory()->create();
|
|
|
|
createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$firstConnection = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'First Connection',
|
|
'is_default' => true,
|
|
]);
|
|
|
|
$secondConnection = ProviderConnection::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'dummy',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Second Connection',
|
|
'is_default' => false,
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'connection',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'tenant_name' => (string) $tenant->name,
|
|
'provider_connection_id' => (int) $firstConnection->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$firstTab = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class, [
|
|
'onboardingDraft' => (int) $draft->getKey(),
|
|
]);
|
|
|
|
$secondTab = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class, [
|
|
'onboardingDraft' => (int) $draft->getKey(),
|
|
]);
|
|
|
|
$firstTab->call('selectProviderConnection', (int) $secondConnection->getKey());
|
|
|
|
$secondTab
|
|
->call('selectProviderConnection', (int) $firstConnection->getKey())
|
|
->assertSet('onboardingSession.version', 2)
|
|
->assertSet('onboardingSession.state.provider_connection_id', (int) $secondConnection->getKey())
|
|
->assertSet('selectedProviderConnectionId', (int) $secondConnection->getKey());
|
|
|
|
$draft->refresh();
|
|
|
|
expect($draft->state['provider_connection_id'] ?? null)->toBe((int) $secondConnection->getKey())
|
|
->and($draft->version)->toBe(2)
|
|
->and($draft->updated_by_user_id)->toBe((int) $user->getKey());
|
|
});
|