Some checks failed
Main Confidence / confidence (push) Failing after 48s
## Summary - add derived onboarding readiness to the managed tenant onboarding workflow and multi-draft picker - keep provider-specific permission diagnostics secondary while preserving canonical `Open operation` and existing onboarding action semantics - add spec-kit artifacts for `240-tenant-onboarding-readiness` and align roadmap/spec-candidate planning notes - unify the required-permissions empty state copy to English ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - browser smoke exercised the onboarding picker, route-bound mismatch readiness state, canonical `Open operation` path, and local fixture cleanup ## Notes - branch includes the generated spec artifacts under `specs/240-tenant-onboarding-readiness/` - temporary browser smoke tenants/drafts/runs were cleaned from the local environment after validation Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #277
433 lines
16 KiB
PHP
433 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Verification\VerificationReportWriter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
function seedPickerReadinessPermissions(Tenant $tenant, ?int $staleDays = null): void
|
|
{
|
|
$configured = array_merge(
|
|
config('intune_permissions.permissions', []),
|
|
config('entra_permissions.permissions', []),
|
|
);
|
|
|
|
foreach ($configured as $permission) {
|
|
if (! is_array($permission)) {
|
|
continue;
|
|
}
|
|
|
|
$key = $permission['key'] ?? null;
|
|
|
|
if (! is_string($key) || trim($key) === '') {
|
|
continue;
|
|
}
|
|
|
|
TenantPermission::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'permission_key' => trim($key),
|
|
'status' => 'granted',
|
|
'details' => ['source' => 'picker-readiness-test'],
|
|
'last_checked_at' => $staleDays === null ? now() : now()->subDays($staleDays),
|
|
]);
|
|
}
|
|
}
|
|
|
|
it('shows a draft picker with resumable draft metadata when multiple drafts exist', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
$startedBy = User::factory()->create(['name' => 'Primary Owner']);
|
|
$updatedBy = User::factory()->create(['name' => 'Second Operator']);
|
|
|
|
foreach ([$user, $startedBy, $updatedBy] as $member) {
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $member->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
}
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $startedBy,
|
|
'updated_by' => $updatedBy,
|
|
'state' => [
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'tenant_name' => 'Contoso',
|
|
'environment' => 'prod',
|
|
'primary_domain' => 'contoso.example',
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $updatedBy,
|
|
'updated_by' => $startedBy,
|
|
'current_step' => 'connection',
|
|
'state' => [
|
|
'entra_tenant_id' => '22222222-2222-2222-2222-222222222222',
|
|
'tenant_name' => 'Fabrikam',
|
|
'environment' => 'staging',
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.onboarding'))
|
|
->assertSuccessful()
|
|
->assertSee('Multiple onboarding drafts are available.')
|
|
->assertSee('Contoso')
|
|
->assertSee('Fabrikam')
|
|
->assertSee('Current stage')
|
|
->assertSee('Started by')
|
|
->assertSee('Last updated by')
|
|
->assertSee('Primary Owner')
|
|
->assertSee('Second Operator')
|
|
->assertSee('Resume onboarding')
|
|
->assertSee('View summary');
|
|
});
|
|
|
|
it('excludes completed and cancelled drafts from the landing picker', 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());
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'state' => [
|
|
'entra_tenant_id' => '33333333-3333-3333-3333-333333333333',
|
|
'tenant_name' => 'Visible Draft A',
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'state' => [
|
|
'entra_tenant_id' => '44444444-4444-4444-4444-444444444444',
|
|
'tenant_name' => 'Visible Draft B',
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'status' => 'completed',
|
|
'state' => [
|
|
'entra_tenant_id' => '55555555-5555-5555-5555-555555555555',
|
|
'tenant_name' => 'Completed Draft',
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'status' => 'cancelled',
|
|
'state' => [
|
|
'entra_tenant_id' => '66666666-6666-6666-6666-666666666666',
|
|
'tenant_name' => 'Cancelled Draft',
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.onboarding'))
|
|
->assertSuccessful()
|
|
->assertSee('Visible Draft A')
|
|
->assertSee('Visible Draft B')
|
|
->assertDontSee('Completed Draft')
|
|
->assertDontSee('Cancelled Draft');
|
|
});
|
|
|
|
it('shows compact readiness snippets for multiple resumable drafts while keeping picker actions', 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',
|
|
]);
|
|
|
|
$blockedTenant = Tenant::factory()->onboarding()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '41414141-4141-4141-4141-414141414141',
|
|
'name' => 'Needs Connection Tenant',
|
|
]);
|
|
$readyTenant = Tenant::factory()->onboarding()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '42424242-4242-4242-4242-424242424242',
|
|
'name' => 'Ready Picker Tenant',
|
|
]);
|
|
|
|
$user->tenants()->syncWithoutDetaching([
|
|
$blockedTenant->getKey() => ['role' => 'owner'],
|
|
$readyTenant->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
seedPickerReadinessPermissions($readyTenant);
|
|
|
|
$connection = ProviderConnection::factory()->platform()->verifiedHealthy()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $readyTenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $readyTenant->tenant_id,
|
|
'display_name' => 'Ready picker connection',
|
|
'is_default' => true,
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $readyTenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'provider.connection.check',
|
|
'title' => 'Provider connection check',
|
|
'status' => 'pass',
|
|
'severity' => 'info',
|
|
'blocking' => false,
|
|
'reason_code' => 'ok',
|
|
'message' => 'Connection is healthy.',
|
|
'evidence' => [],
|
|
'next_steps' => [],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $blockedTenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'connection',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $blockedTenant->tenant_id,
|
|
'tenant_name' => (string) $blockedTenant->name,
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $readyTenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'complete',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $readyTenant->tenant_id,
|
|
'tenant_name' => (string) $readyTenant->name,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'verification_operation_run_id' => (int) $run->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.onboarding'))
|
|
->assertSuccessful()
|
|
->assertSee('Needs Connection Tenant')
|
|
->assertSee('Ready Picker Tenant')
|
|
->assertSee('Compact readiness')
|
|
->assertSee('Provider connection required')
|
|
->assertSee('Connect provider')
|
|
->assertSee('Ready for activation')
|
|
->assertSee('Verification and permission evidence are current.')
|
|
->assertSee('Resume onboarding')
|
|
->assertSee('View summary');
|
|
});
|
|
|
|
it('shows stale and mismatched readiness cues across multiple drafts in the picker', 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',
|
|
]);
|
|
|
|
$staleTenant = Tenant::factory()->onboarding()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '44444444-4444-4444-4444-444444444444',
|
|
'name' => 'Picker Stale Tenant',
|
|
]);
|
|
$mismatchTenant = Tenant::factory()->onboarding()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '45454545-4545-4545-4545-454545454545',
|
|
'name' => 'Picker Mismatch Tenant',
|
|
]);
|
|
|
|
$user->tenants()->syncWithoutDetaching([
|
|
$staleTenant->getKey() => ['role' => 'owner'],
|
|
$mismatchTenant->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
seedPickerReadinessPermissions($staleTenant, staleDays: 45);
|
|
seedPickerReadinessPermissions($mismatchTenant);
|
|
|
|
$staleConnection = ProviderConnection::factory()->platform()->verifiedHealthy()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $staleTenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $staleTenant->tenant_id,
|
|
'display_name' => 'Stale picker connection',
|
|
'is_default' => true,
|
|
]);
|
|
$oldMismatchConnection = ProviderConnection::factory()->platform()->verifiedHealthy()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $mismatchTenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => '46464646-4646-4646-4646-464646464646',
|
|
'display_name' => 'Old mismatch picker connection',
|
|
]);
|
|
$selectedMismatchConnection = ProviderConnection::factory()->platform()->verifiedHealthy()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $mismatchTenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $mismatchTenant->tenant_id,
|
|
'display_name' => 'Selected mismatch picker connection',
|
|
'is_default' => true,
|
|
]);
|
|
|
|
$staleRun = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $staleTenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $staleConnection->getKey(),
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'provider.connection.check',
|
|
'title' => 'Provider connection check',
|
|
'status' => 'pass',
|
|
'severity' => 'info',
|
|
'blocking' => false,
|
|
'reason_code' => 'ok',
|
|
'message' => 'Connection is healthy.',
|
|
'evidence' => [],
|
|
'next_steps' => [],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
$mismatchRun = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $mismatchTenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $oldMismatchConnection->getKey(),
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'provider.connection.check',
|
|
'title' => 'Provider connection check',
|
|
'status' => 'pass',
|
|
'severity' => 'info',
|
|
'blocking' => false,
|
|
'reason_code' => 'ok',
|
|
'message' => 'Connection is healthy.',
|
|
'evidence' => [],
|
|
'next_steps' => [],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $staleTenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'complete',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $staleTenant->tenant_id,
|
|
'tenant_name' => (string) $staleTenant->name,
|
|
'provider_connection_id' => (int) $staleConnection->getKey(),
|
|
'verification_operation_run_id' => (int) $staleRun->getKey(),
|
|
],
|
|
]);
|
|
|
|
createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $mismatchTenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'complete',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $mismatchTenant->tenant_id,
|
|
'tenant_name' => (string) $mismatchTenant->name,
|
|
'provider_connection_id' => (int) $selectedMismatchConnection->getKey(),
|
|
'verification_operation_run_id' => (int) $mismatchRun->getKey(),
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.onboarding'))
|
|
->assertSuccessful()
|
|
->assertSee('Picker Stale Tenant')
|
|
->assertSee('Picker Mismatch Tenant')
|
|
->assertSee('Permission data is older than the 30-day freshness window.')
|
|
->assertSee('Verification evidence belongs to a different provider connection.')
|
|
->assertSee('Rerun verification');
|
|
});
|
|
|
|
it('preserves the single-draft landing redirect instead of rendering compact readiness', 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',
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'state' => [
|
|
'entra_tenant_id' => '43434343-4343-4343-4343-434343434343',
|
|
'tenant_name' => 'Single Redirect Tenant',
|
|
],
|
|
]);
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.onboarding'))
|
|
->assertRedirect(route('admin.onboarding.draft', ['onboardingDraft' => $draft->getKey()]));
|
|
});
|