- Canonical /admin/onboarding entry point; legacy routes 404\n- Tenantless run viewer at /admin/operations/{run} with membership-based 404\n- RBAC UX (disabled controls + tooltips) and server-side 403\n- DB-only rendering/refresh; contract registry enforced\n- Adds migrations + tests + spec artifacts
102 lines
3.3 KiB
PHP
102 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
it('allows viewing an operation run without a selected workspace when the user is a member of the run workspace', 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()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertSuccessful();
|
|
|
|
expect(session()->get(WorkspaceContext::SESSION_KEY))->toBeNull();
|
|
});
|
|
|
|
it('returns 404 for non-members when viewing an operation run without a selected workspace', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
session()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('renders stored target scope and failure details for a completed run', 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()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$entraTenantId = '11111111-1111-1111-1111-111111111111';
|
|
$failureMessage = 'Missing required Graph permissions.';
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'context' => [
|
|
'target_scope' => [
|
|
'entra_tenant_id' => $entraTenantId,
|
|
'entra_tenant_name' => 'Contoso',
|
|
],
|
|
],
|
|
'failure_summary' => [
|
|
[
|
|
'code' => 'provider.connection.check.failed',
|
|
'reason_code' => 'permission_denied',
|
|
'message' => $failureMessage,
|
|
],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertSuccessful()
|
|
->assertSee($entraTenantId)
|
|
->assertSee('permission_denied')
|
|
->assertSee($failureMessage);
|
|
});
|