286 lines
12 KiB
PHP
286 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\BackupSetResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Filament\Resources\TenantReviewResource;
|
|
use App\Models\AuditLog;
|
|
use App\Models\BackupSet;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\User;
|
|
use App\Support\Navigation\RelatedNavigationResolver;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
function setGovernanceArtifactAdminContext(ManagedEnvironment $tenant): void
|
|
{
|
|
setAdminPanelContext();
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
|
(string) $tenant->workspace_id => (int) $tenant->getKey(),
|
|
]);
|
|
}
|
|
|
|
function governanceArtifactAuditRecord(ManagedEnvironment $tenant, string $resourceType, int|string $resourceId): AuditLog
|
|
{
|
|
return AuditLog::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'actor_email' => 'governance@example.com',
|
|
'actor_name' => 'Governance Operator',
|
|
'action' => 'governance.updated',
|
|
'status' => 'success',
|
|
'resource_type' => $resourceType,
|
|
'resource_id' => (string) $resourceId,
|
|
'summary' => 'Governance resource updated',
|
|
'metadata' => [],
|
|
'recorded_at' => now(),
|
|
]);
|
|
}
|
|
|
|
it('keeps evidence snapshot truth drillthroughs on workspace-first operation routes', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', setUiContext: false);
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant);
|
|
|
|
$run = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::EvidenceSnapshotGenerate->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
|
|
$snapshot->forceFill([
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
])->save();
|
|
|
|
$this->actingAs($user);
|
|
setGovernanceArtifactAdminContext($tenant);
|
|
|
|
$envelope = app(ArtifactTruthPresenter::class)->forEvidenceSnapshotFresh($snapshot->fresh());
|
|
|
|
expect($envelope->nextActionUrl)->toBe(OperationRunLinks::tenantlessView($run))
|
|
->and(parse_url((string) $envelope->nextActionUrl, PHP_URL_PATH))
|
|
->toBe('/admin/workspaces/'.$tenant->workspace->getRouteKey().'/operations/'.$run->getRouteKey())
|
|
->not->toContain('/admin/t/');
|
|
});
|
|
|
|
it('builds workspace-first related record links for governance operations', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', setUiContext: false);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$backupSetRun = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::BackupSetUpdate->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'backup_set_id' => (int) $backupSet->getKey(),
|
|
],
|
|
]);
|
|
|
|
$restoreRun = RestoreRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$restoreOperation = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::RestoreExecute->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'restore_run_id' => (int) $restoreRun->getKey(),
|
|
],
|
|
]);
|
|
|
|
$snapshot = seedTenantReviewEvidence($tenant);
|
|
$snapshotRun = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::EvidenceSnapshotGenerate->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
$snapshot->forceFill([
|
|
'operation_run_id' => (int) $snapshotRun->getKey(),
|
|
])->save();
|
|
|
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
|
$reviewRun = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::TenantReviewCompose->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
$review->forceFill([
|
|
'operation_run_id' => (int) $reviewRun->getKey(),
|
|
])->save();
|
|
|
|
$packRun = OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'type' => OperationRunType::ReviewPackGenerate->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'operation_run_id' => (int) $packRun->getKey(),
|
|
'expires_at' => now()->addDay(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setGovernanceArtifactAdminContext($tenant);
|
|
|
|
$backupLinks = OperationRunLinks::related($backupSetRun, $tenant);
|
|
$restoreLinks = OperationRunLinks::related($restoreOperation, $tenant);
|
|
$snapshotLinks = OperationRunLinks::related($snapshotRun, $tenant);
|
|
$reviewLinks = OperationRunLinks::related($reviewRun, $tenant);
|
|
$packLinks = OperationRunLinks::related($packRun, $tenant);
|
|
|
|
expect($backupLinks)->toMatchArray([
|
|
OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant),
|
|
'Backup Sets' => BackupSetResource::getUrl('index', tenant: $tenant),
|
|
'Backup Set' => BackupSetResource::getUrl('view', ['record' => (int) $backupSet->getKey()], tenant: $tenant),
|
|
]);
|
|
|
|
expect($restoreLinks)->toMatchArray([
|
|
OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant),
|
|
'Restore Runs' => RestoreRunResource::getUrl('index', tenant: $tenant),
|
|
'Restore Run' => RestoreRunResource::getUrl('view', ['record' => (int) $restoreRun->getKey()], tenant: $tenant),
|
|
]);
|
|
|
|
expect($snapshotLinks)->toMatchArray([
|
|
OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant),
|
|
'Evidence Snapshot' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant),
|
|
]);
|
|
|
|
expect($reviewLinks)->toMatchArray([
|
|
OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant),
|
|
'ManagedEnvironment Review' => TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant),
|
|
]);
|
|
|
|
expect($packLinks)->toMatchArray([
|
|
OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant),
|
|
'Review Pack' => ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant),
|
|
]);
|
|
|
|
foreach ([$backupLinks, $restoreLinks, $snapshotLinks, $reviewLinks, $packLinks] as $links) {
|
|
foreach ($links as $url) {
|
|
expect(parse_url($url, PHP_URL_PATH))
|
|
->toBeString()
|
|
->not->toContain('/admin/t/');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('builds workspace-first audit target links for governance artifact records', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', setUiContext: false);
|
|
|
|
$backupSet = BackupSet::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$restoreRun = RestoreRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$finding = Finding::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$findingException = FindingException::query()->create([
|
|
'managed_environment_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' => 'Temporary exception request',
|
|
'requested_at' => now(),
|
|
'review_due_at' => now()->addWeek(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
$decision = $findingException->decisions()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'actor_user_id' => (int) $user->getKey(),
|
|
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
|
'reason' => 'Temporary exception request',
|
|
'metadata' => [],
|
|
'decided_at' => now(),
|
|
]);
|
|
$findingException->forceFill([
|
|
'current_decision_id' => (int) $decision->getKey(),
|
|
])->save();
|
|
|
|
$this->actingAs($user);
|
|
setGovernanceArtifactAdminContext($tenant);
|
|
|
|
$resolver = app(RelatedNavigationResolver::class);
|
|
|
|
$cases = [
|
|
[
|
|
'audit' => governanceArtifactAuditRecord($tenant, 'backup_set', (int) $backupSet->getKey()),
|
|
'label' => 'Open backup set',
|
|
'url' => BackupSetResource::getUrl('view', ['record' => (int) $backupSet->getKey()], tenant: $tenant),
|
|
],
|
|
[
|
|
'audit' => governanceArtifactAuditRecord($tenant, 'restore_run', (int) $restoreRun->getKey()),
|
|
'label' => 'Open restore run',
|
|
'url' => RestoreRunResource::getUrl('view', ['record' => (int) $restoreRun->getKey()], tenant: $tenant),
|
|
],
|
|
[
|
|
'audit' => governanceArtifactAuditRecord($tenant, 'finding', (int) $finding->getKey()),
|
|
'label' => 'Open finding',
|
|
'url' => FindingResource::getUrl('view', ['record' => (int) $finding->getKey()], tenant: $tenant),
|
|
],
|
|
[
|
|
'audit' => governanceArtifactAuditRecord($tenant, 'finding_exception', (int) $findingException->getKey()),
|
|
'label' => 'Open finding exception',
|
|
'url' => FindingExceptionResource::getUrl('view', ['record' => $findingException], tenant: $tenant),
|
|
],
|
|
];
|
|
|
|
foreach ($cases as $case) {
|
|
$link = $resolver->auditTargetLink($case['audit']);
|
|
|
|
expect($link)->toMatchArray([
|
|
'label' => $case['label'],
|
|
'url' => $case['url'],
|
|
]);
|
|
|
|
expect(parse_url($link['url'], PHP_URL_PATH))
|
|
->toBeString()
|
|
->not->toContain('/admin/t/');
|
|
}
|
|
}); |