## Summary - add the Spec 194 governance action catalog, friction classes, reason policies, and regression guards - align exception, review, evidence, finding, tenant, provider connection, and system run actions to the shared semantics model - add focused feature, RBAC, audit, unit, and browser coverage, including the tenant detail triage header consistency update ## Verification - ran the focused Spec 194 verification pack from the quickstart and task plan - ran targeted tenant triage coverage after the detail-header update - ran `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Filament Notes - Filament v5 / Livewire v4 compliance preserved - provider registration remains in `apps/platform/bootstrap/providers.php` - globally searchable resources were not changed - destructive actions remain confirmation-gated and server-authorized - no new Filament assets were introduced; the existing `cd apps/platform && php artisan filament:assets` deploy step stays unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #229
275 lines
9.8 KiB
PHP
275 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
use App\Filament\Resources\TenantResource;
|
|
use App\Filament\Resources\TenantReviewResource;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Findings\FindingExceptionService;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\TenantReviewCompletenessState;
|
|
use App\Support\TenantReviewStatus;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\System\SystemOperationRunLinks;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
pest()->browser()->timeout(20_000);
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function spec194ApprovedFindingException(Tenant $tenant, User $requester): FindingException
|
|
{
|
|
$approver = User::factory()->create();
|
|
createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: $approver,
|
|
role: 'owner',
|
|
workspaceRole: 'manager',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$finding = Finding::factory()->for($tenant)->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
|
]);
|
|
|
|
/** @var FindingExceptionService $service */
|
|
$service = app(FindingExceptionService::class);
|
|
|
|
$requested = $service->request($finding, $tenant, $requester, [
|
|
'owner_user_id' => (int) $requester->getKey(),
|
|
'request_reason' => 'Spec194 browser smoke request.',
|
|
'review_due_at' => now()->addDays(7)->toDateTimeString(),
|
|
'expires_at' => now()->addDays(14)->toDateTimeString(),
|
|
]);
|
|
|
|
return $service->approve($requested, $approver, [
|
|
'effective_from' => now()->subDay()->toDateTimeString(),
|
|
'expires_at' => now()->addDays(14)->toDateTimeString(),
|
|
'approval_reason' => 'Spec194 browser smoke approval.',
|
|
]);
|
|
}
|
|
|
|
function spec194SmokeLoginUrl(User $user, Tenant $tenant, string $redirect = ''): string
|
|
{
|
|
return route('admin.local.smoke-login', array_filter([
|
|
'email' => $user->email,
|
|
'tenant' => $tenant->external_id,
|
|
'workspace' => $tenant->workspace->slug,
|
|
'redirect' => $redirect,
|
|
], static fn (?string $value): bool => filled($value)));
|
|
}
|
|
|
|
it('smokes tenant and admin governance semantics through modal entry points', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(
|
|
role: 'owner',
|
|
workspaceRole: 'manager',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$finding = Finding::factory()->for($tenant)->create();
|
|
|
|
$pendingException = FindingException::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_PENDING,
|
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
'request_reason' => 'Spec194 focused review queue smoke.',
|
|
'requested_at' => now()->subDay(),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
$approvedException = spec194ApprovedFindingException($tenant, $user);
|
|
|
|
$snapshotRun = OperationRun::factory()->forTenant($tenant)->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'operation_run_id' => (int) $snapshotRun->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => ['finding_count' => 2],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
ReviewPack::factory()->ready()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
]);
|
|
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
|
|
$review->forceFill([
|
|
'status' => TenantReviewStatus::Ready->value,
|
|
'completeness_state' => TenantReviewCompletenessState::Complete->value,
|
|
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
|
|
'publish_blockers' => [],
|
|
'section_state_counts' => [
|
|
'complete' => 6,
|
|
'partial' => 0,
|
|
'missing' => 0,
|
|
'stale' => 0,
|
|
],
|
|
]),
|
|
])->save();
|
|
|
|
$review = $review->refresh();
|
|
|
|
$archivedTenant = Tenant::factory()->archived()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Spec194 Archived Tenant',
|
|
]);
|
|
|
|
createUserWithTenant(
|
|
tenant: $archivedTenant,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'manager',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
visit(spec194SmokeLoginUrl($user, $tenant))
|
|
->waitForText('Dashboard')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin').'?exception='.(int) $pendingException->getKey())
|
|
->waitForText('Focused review lane')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Approve exception')
|
|
->assertSee('Reject exception');
|
|
|
|
visit(FindingExceptionResource::getUrl('view', ['record' => $approvedException], tenant: $tenant))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Renew exception')
|
|
->assertSee('Revoke exception');
|
|
|
|
visit(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->click('Publish review')
|
|
->waitForText('Publication reason')
|
|
->click('Cancel')
|
|
->click('[aria-label="More"]')
|
|
->assertSee('Refresh review')
|
|
->assertSee('Export executive pack')
|
|
->click('[aria-label="Danger"]')
|
|
->click('Archive review')
|
|
->waitForText('Archive reason')
|
|
->click('Cancel')
|
|
->assertSee('Publish review')
|
|
->assertSee('Evidence snapshot');
|
|
|
|
visit(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->click('Refresh evidence')
|
|
->waitForText('Confirm')
|
|
->click('Cancel')
|
|
->click('Expire snapshot')
|
|
->waitForText('Expiry reason')
|
|
->click('Cancel')
|
|
->assertSee('Refresh evidence')
|
|
->assertSee('Expire snapshot');
|
|
|
|
visit(TenantResource::getUrl('view', ['record' => $tenant], panel: 'admin'))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->click('[aria-label="Lifecycle"]')
|
|
->click('Archive')
|
|
->waitForText('Archive reason')
|
|
->click('Cancel')
|
|
->assertSee('Lifecycle');
|
|
|
|
visit(TenantResource::getUrl('edit', ['record' => $tenant], panel: 'admin'))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Lifecycle');
|
|
|
|
visit(TenantResource::getUrl('view', ['record' => $archivedTenant], panel: 'admin'))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Lifecycle');
|
|
|
|
visit(TenantResource::getUrl('edit', ['record' => $archivedTenant], panel: 'admin'))
|
|
->waitForText('Related context')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Lifecycle');
|
|
});
|
|
|
|
it('smokes system run triage semantics without javascript errors', function (): void {
|
|
$failedRun = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'type' => 'inventory_sync',
|
|
]);
|
|
|
|
$runningRun = OperationRun::factory()->create([
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'type' => 'inventory_sync',
|
|
'created_at' => now()->subMinutes(15),
|
|
'started_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$platformUser = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::OPERATIONS_VIEW,
|
|
PlatformCapabilities::OPERATIONS_MANAGE,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
auth('web')->logout();
|
|
$this->flushSession();
|
|
$this->actingAs($platformUser, 'platform');
|
|
|
|
visit(SystemOperationRunLinks::view($failedRun))
|
|
->waitForText('Operation #'.(int) $failedRun->getKey())
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Retry')
|
|
->assertSee('Mark investigated')
|
|
->assertDontSee('Cancel');
|
|
|
|
visit(SystemOperationRunLinks::view($runningRun))
|
|
->waitForText('Operation #'.(int) $runningRun->getKey())
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs()
|
|
->assertSee('Mark investigated')
|
|
->assertSee('Cancel');
|
|
});
|