Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #460
847 lines
38 KiB
PHP
847 lines
38 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Governance\GovernanceInbox;
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Models\AuditLog;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\ReviewPublicationResolutionStep;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\GovernanceInbox\ReviewPublicationResolutionInboxProvider;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofCurrentness;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofUsability;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofVisibility;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
it('Spec389 renders active review publication resolution cases in the governance inbox', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Publishing Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner', ensureDefaultMicrosoftProviderConnection: true);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Completed->value,
|
|
'current_step_key' => null,
|
|
'summary' => ['label' => 'Spec389 completed hidden case'],
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Completed->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Cancelled->value,
|
|
'current_step_key' => null,
|
|
'summary' => ['label' => 'Spec389 cancelled hidden case'],
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Superseded->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Superseded->value,
|
|
'current_step_key' => null,
|
|
'summary' => ['label' => 'Spec389 superseded hidden case'],
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Superseded->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review publication work')
|
|
->assertSee('Review cannot be published yet')
|
|
->assertSee('A current evidence snapshot is required.')
|
|
->assertSee('Continue preparation')
|
|
->assertSee('Review publication status')
|
|
->assertSee('Updated: Any time')
|
|
->assertDontSee('Spec389 completed hidden case')
|
|
->assertDontSee('Spec389 cancelled hidden case')
|
|
->assertDontSee('Spec389 superseded hidden case')
|
|
->assertDontSee('Operation #');
|
|
});
|
|
|
|
it('Spec389 sorts publication work by severity before updated time', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Sort Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
[$failedCase, $failedStep, $failedReview] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
]);
|
|
|
|
$run = spec389ResolutionOperationRun($tenant, $failedCase, $failedReview, [
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
spec389AttachResolutionOperationProof($failedStep, $run, proofStatus: OperationRunOutcome::Failed->value);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'updated_at' => now()->subMinute(),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
'summary' => ['missing_report_dimensions' => ['unsupported_report']],
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'updated_at' => now()->subMinutes(2),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'updated_at' => now()->subMinutes(3),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
]);
|
|
|
|
[, $readyStep, $readyReview] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'updated_at' => now()->subMinutes(4),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
spec389AttachReadyToContinueProof($readyStep, $readyReview);
|
|
|
|
[$waitingCase, $waitingStep, $waitingReview] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'updated_at' => now(),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
]);
|
|
$waitingRun = spec389ResolutionOperationRun($tenant, $waitingCase, $waitingReview, [
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
spec389AttachResolutionOperationProof($waitingStep, $waitingRun, proofStatus: OperationRunStatus::Running->value);
|
|
|
|
$response = $this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]));
|
|
|
|
$response
|
|
->assertOk()
|
|
->assertSee('Review publication work');
|
|
|
|
$section = spec389ReviewPublicationProviderSection($user, $tenant, previewLimit: 10);
|
|
$statuses = collect($section['entries'] ?? [])->pluck('inbox_status')->all();
|
|
|
|
expect($statuses)->toBe([
|
|
'failed',
|
|
'blocked',
|
|
'needs_attention',
|
|
'needs_recheck',
|
|
'ready_to_continue',
|
|
'waiting',
|
|
]);
|
|
|
|
$waitingEntry = collect($section['entries'] ?? [])
|
|
->firstWhere('inbox_status', 'waiting');
|
|
|
|
expect($waitingEntry['primary_action_label'] ?? null)->toBe('Open operation')
|
|
->and(collect($waitingEntry['secondary_actions'] ?? [])->pluck('label')->all())->not->toContain('Open operation');
|
|
});
|
|
|
|
it('Spec389 applies derived status and updated-date filters only to review publication work', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Filter Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner', ensureDefaultMicrosoftProviderConnection: true);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'updated_at' => now()->subHours(2),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
'updated_at' => now()->subHours(2),
|
|
]);
|
|
|
|
[, $readyStep, $readyReview] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'updated_at' => now()->subDays(10),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
'updated_at' => now()->subDays(10),
|
|
]);
|
|
spec389AttachReadyToContinueProof($readyStep, $readyReview);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
'status' => 'needs_attention',
|
|
'updated' => 'last_24_hours',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Status: Needs attention')
|
|
->assertSee('Updated: Last 24 hours')
|
|
->assertSee('Review cannot be published yet')
|
|
->assertDontSee('Review preparation can continue');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
'status' => 'ready_to_continue',
|
|
'updated' => 'last_24_hours',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('These review publication filters are hiding active preparation work')
|
|
->assertSee('Clear review publication filters')
|
|
->assertDontSee('Review preparation can continue');
|
|
});
|
|
|
|
it('Spec389 falls back to needs re-check when ready-to-continue proof is stale', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Stale Ready Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
[, $step, $review] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
spec389AttachReadyToContinueProof($step, $review, [
|
|
'proof_currentness' => ResolutionProofCurrentness::Stale->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review preparation needs re-check')
|
|
->assertSee('Inspect preparation')
|
|
->assertDontSee('Review preparation can continue');
|
|
|
|
$section = spec389ReviewPublicationProviderSection($user, $tenant);
|
|
$entry = collect($section['entries'] ?? [])->first();
|
|
|
|
expect($entry['inbox_status'] ?? null)->toBe('needs_recheck');
|
|
});
|
|
|
|
it('Spec389 discloses operation links only for safe current linked runs', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Operation Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
[$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
]);
|
|
|
|
$run = spec389ResolutionOperationRun($tenant, $case, $review, [
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
|
|
$step->forceFill([
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Failed->value,
|
|
'metadata' => spec389SafeOperationProofMetadata(),
|
|
])->save();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
'status' => 'failed',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review preparation action failed')
|
|
->assertSee('Open operation')
|
|
->assertDontSee('Operation #');
|
|
});
|
|
|
|
it('Spec389 hides operation links when proof currentness cannot be validated', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Stale Proof Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
[$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
]);
|
|
|
|
$run = spec389ResolutionOperationRun($tenant, $case, $review, [
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
|
|
$step->forceFill([
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Failed->value,
|
|
'metadata' => spec389SafeOperationProofMetadata([
|
|
'proof_currentness' => ResolutionProofCurrentness::Stale->value,
|
|
]),
|
|
])->save();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review preparation needs re-check')
|
|
->assertSee('Inspect preparation')
|
|
->assertDontSee('Open operation')
|
|
->assertDontSee('Operation #');
|
|
});
|
|
|
|
it('Spec389 hides operation links when operation context or proof binding is invalid', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Invalid Operation Context Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
|
|
[$otherCase, , $otherReview] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Cancelled->value,
|
|
'current_step_key' => null,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Superseded->value,
|
|
]);
|
|
$otherRun = spec389ResolutionOperationRun($tenant, $otherCase, $otherReview, [
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
|
|
$scenarios = [
|
|
'missing case context' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant): array {
|
|
return [
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
],
|
|
];
|
|
},
|
|
'missing trigger' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant): array {
|
|
return [
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
];
|
|
},
|
|
'cross case' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant, $otherCase): array {
|
|
return [
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'review_publication_resolution_case_id' => (int) $otherCase->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
],
|
|
];
|
|
},
|
|
'cross review' => function (ReviewPublicationResolutionCase $case) use ($tenant, $otherReview): array {
|
|
return [
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $otherReview->getKey(),
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
],
|
|
];
|
|
},
|
|
'wrong type' => fn (): array => [
|
|
'type' => OperationRunType::EvidenceSnapshotGenerate->value,
|
|
],
|
|
'wrong proof id' => fn (): array => [
|
|
'proof_id' => (int) $otherRun->getKey(),
|
|
],
|
|
];
|
|
|
|
foreach ($scenarios as $label => $runOverrides) {
|
|
[$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'updated_at' => now()->subMinutes(count($scenarios)),
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
'summary' => ['scenario' => $label],
|
|
]);
|
|
|
|
$overrides = $runOverrides($case, $review);
|
|
$proofId = is_numeric($overrides['proof_id'] ?? null) ? (int) $overrides['proof_id'] : null;
|
|
unset($overrides['proof_id']);
|
|
|
|
$run = spec389ResolutionOperationRun($tenant, $case, $review, array_replace([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
], $overrides));
|
|
|
|
spec389AttachResolutionOperationProof($step, $run, proofId: $proofId, proofStatus: OperationRunOutcome::Failed->value);
|
|
}
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review preparation needs re-check')
|
|
->assertDontSee('Open operation')
|
|
->assertDontSee('Operation #');
|
|
|
|
$section = spec389ReviewPublicationProviderSection($user, $tenant, previewLimit: 10);
|
|
$entries = collect($section['entries'] ?? []);
|
|
|
|
expect($entries)->toHaveCount(count($scenarios))
|
|
->and($entries->pluck('inbox_status')->unique()->values()->all())->toBe(['needs_recheck'])
|
|
->and(spec389EntryActionLabels($entries->all()))->not->toContain('Open operation');
|
|
});
|
|
|
|
it('Spec389 hides operation links when OperationRunPolicy denies the linked run', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Policy Denied Operation Tenant',
|
|
'slug' => 'spec389-policy-denied-operation-tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
|
|
[$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
]);
|
|
$run = spec389ResolutionOperationRun($tenant, $case, $review, [
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
spec389AttachResolutionOperationProof($step, $run, proofStatus: OperationRunOutcome::Failed->value);
|
|
|
|
$originalFixture = config('tenantpilot.backup_health.browser_smoke_fixture');
|
|
|
|
config([
|
|
'tenantpilot.backup_health.browser_smoke_fixture.user.email' => $user->email,
|
|
'tenantpilot.backup_health.browser_smoke_fixture.blocked_drillthrough.tenant_external_id' => (string) $tenant->external_id,
|
|
'tenantpilot.backup_health.browser_smoke_fixture.blocked_drillthrough.capability_denials' => [
|
|
Capabilities::PROVIDER_VIEW,
|
|
],
|
|
]);
|
|
app(CapabilityResolver::class)->clearCache();
|
|
|
|
try {
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Review preparation needs re-check')
|
|
->assertDontSee('Open operation')
|
|
->assertDontSee('Operation #');
|
|
|
|
$section = spec389ReviewPublicationProviderSection($user, $tenant);
|
|
$entry = collect($section['entries'] ?? [])->first();
|
|
|
|
expect($entry['inbox_status'] ?? null)->toBe('needs_recheck')
|
|
->and(spec389EntryActionLabels([$entry]))->not->toContain('Open operation');
|
|
} finally {
|
|
config(['tenantpilot.backup_health.browser_smoke_fixture' => $originalFixture]);
|
|
app(CapabilityResolver::class)->clearCache();
|
|
}
|
|
});
|
|
|
|
it('Spec389 hides resolution cases outside the viewer environment scope', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Visible Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
$hiddenTenant = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Spec389 Hidden Tenant',
|
|
]);
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($hiddenTenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Spec389 Visible Tenant')
|
|
->assertDontSee('Spec389 Hidden Tenant');
|
|
});
|
|
|
|
it('Spec389 hides resolution cases outside the active workspace', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Workspace Visible Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
|
|
$foreignTenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Foreign Workspace Tenant',
|
|
]);
|
|
[$foreignUser, $foreignTenant] = createUserWithTenant($foreignTenant, role: 'owner', workspaceRole: 'owner');
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
spec389CreateResolutionCase($foreignTenant, $foreignUser, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Spec389 Workspace Visible Tenant')
|
|
->assertDontSee('Spec389 Foreign Workspace Tenant');
|
|
});
|
|
|
|
it('Spec389 does not surface resolution intake work on the customer review workspace', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Customer Surface Tenant',
|
|
]);
|
|
[$owner, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
[$customer] = createUserWithTenant($tenant, User::factory()->create(), role: 'readonly', workspaceRole: 'readonly');
|
|
|
|
spec389CreateResolutionCase($tenant, $owner, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
$this->actingAs($customer)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(CustomerReviewWorkspace::environmentFilterUrl($tenant))
|
|
->assertOk()
|
|
->assertDontSee('Review publication work')
|
|
->assertDontSee('Review cannot be published yet')
|
|
->assertDontSee('Continue preparation')
|
|
->assertDontSee('Open operation');
|
|
});
|
|
|
|
it('Spec389 renders governance inbox publication work without creating audit events', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec389 Audit Neutral Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
|
|
|
spec389CreateResolutionCase($tenant, $user, [
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
], [
|
|
'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
]);
|
|
|
|
$auditCount = AuditLog::query()->count();
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
|
'family' => 'review_publication_resolution',
|
|
]))
|
|
->assertOk()
|
|
->assertSee('Continue preparation')
|
|
->assertDontSee('Publish review')
|
|
->assertDontSee('Cancel resolution')
|
|
->assertDontSee('Prepare export');
|
|
|
|
expect(AuditLog::query()->count())->toBe($auditCount);
|
|
});
|
|
|
|
/**
|
|
* @return array{0: ReviewPublicationResolutionCase, 1: ReviewPublicationResolutionStep, 2: EnvironmentReview}
|
|
*/
|
|
function spec389CreateResolutionCase(
|
|
ManagedEnvironment $tenant,
|
|
User $actor,
|
|
array $caseOverrides = [],
|
|
array $stepOverrides = [],
|
|
): array {
|
|
$now = now();
|
|
$snapshot = EvidenceSnapshot::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->latest('id')
|
|
->first();
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
$snapshot = seedPartialEnvironmentReviewEvidence(
|
|
tenant: $tenant,
|
|
findingCount: 0,
|
|
driftCount: 0,
|
|
operationRunCount: 0,
|
|
);
|
|
}
|
|
$review = EnvironmentReview::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $actor->getKey(),
|
|
'generated_at' => $caseOverrides['review_generated_at'] ?? $now,
|
|
]);
|
|
$stepKey = (string) ($stepOverrides['step_key'] ?? ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$caseUpdatedAt = $caseOverrides['updated_at'] ?? $now;
|
|
$stepUpdatedAt = $stepOverrides['updated_at'] ?? $caseUpdatedAt;
|
|
$defaultStepSummary = $stepKey === ReviewPublicationResolutionStepKey::CompleteRequiredReports->value
|
|
? ['missing_report_dimensions' => ['permission_posture']]
|
|
: [];
|
|
|
|
unset($caseOverrides['review_generated_at']);
|
|
|
|
$case = ReviewPublicationResolutionCase::query()->create(array_replace([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'action_key' => ReviewPublicationResolutionCase::ACTION_KEY,
|
|
'status' => ReviewPublicationResolutionCaseStatus::InProgress->value,
|
|
'current_step_key' => $stepKey,
|
|
'readiness_fingerprint' => hash('sha256', 'spec389-'.$tenant->getKey().'-'.$review->getKey().'-'.str()->uuid()),
|
|
'created_by_user_id' => (int) $actor->getKey(),
|
|
'assigned_to_user_id' => (int) $actor->getKey(),
|
|
'started_at' => $now,
|
|
'last_evaluated_at' => $now,
|
|
'summary' => $defaultStepSummary,
|
|
'metadata' => [],
|
|
'created_at' => $caseUpdatedAt,
|
|
'updated_at' => $caseUpdatedAt,
|
|
], $caseOverrides));
|
|
|
|
$step = ReviewPublicationResolutionStep::query()->create(array_replace([
|
|
'case_id' => (int) $case->getKey(),
|
|
'position' => 1,
|
|
'step_key' => $stepKey,
|
|
'status' => ReviewPublicationResolutionStepStatus::Actionable->value,
|
|
'primary_action_key' => ReviewPublicationResolutionStepKey::tryFrom($stepKey)?->primaryActionKey(),
|
|
'summary' => [],
|
|
'metadata' => [],
|
|
'created_at' => $stepUpdatedAt,
|
|
'updated_at' => $stepUpdatedAt,
|
|
], $stepOverrides));
|
|
|
|
return [$case->fresh(['tenant', 'environmentReview', 'steps.operationRun']), $step->fresh('operationRun'), $review->fresh()];
|
|
}
|
|
|
|
function spec389ResolutionOperationRun(
|
|
ManagedEnvironment $tenant,
|
|
ReviewPublicationResolutionCase $case,
|
|
EnvironmentReview $review,
|
|
array $overrides = [],
|
|
): OperationRun {
|
|
return OperationRun::factory()->forTenant($tenant)->create(array_replace([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
],
|
|
], $overrides));
|
|
}
|
|
|
|
function spec389AttachResolutionOperationProof(
|
|
ReviewPublicationResolutionStep $step,
|
|
OperationRun $run,
|
|
array $metadata = [],
|
|
?int $proofId = null,
|
|
?string $proofStatus = null,
|
|
): ReviewPublicationResolutionStep {
|
|
$step->forceFill([
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => $proofId ?? (int) $run->getKey(),
|
|
'proof_status' => $proofStatus ?? (string) $run->outcome,
|
|
'metadata' => array_replace(spec389SafeOperationProofMetadata(), $metadata),
|
|
])->save();
|
|
|
|
return $step->fresh('operationRun');
|
|
}
|
|
|
|
function spec389AttachReadyToContinueProof(
|
|
ReviewPublicationResolutionStep $step,
|
|
EnvironmentReview $review,
|
|
array $metadata = [],
|
|
): ReviewPublicationResolutionStep {
|
|
$step->forceFill([
|
|
'proof_type' => 'environment_review',
|
|
'proof_id' => (int) $review->getKey(),
|
|
'proof_status' => 'ready',
|
|
'metadata' => array_replace(spec389SafeReadyToContinueProofMetadata(), $metadata),
|
|
])->save();
|
|
|
|
return $step->fresh();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function spec389SafeOperationProofMetadata(array $overrides = []): array
|
|
{
|
|
return array_replace([
|
|
'proof_currentness' => ResolutionProofCurrentness::Current->value,
|
|
'proof_usability' => ResolutionProofUsability::InspectionOnly->value,
|
|
'proof_visibility' => ResolutionProofVisibility::OperatorVisible->value,
|
|
'proof_summary' => [
|
|
'message' => 'Safe current operation proof is available.',
|
|
],
|
|
], $overrides);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function spec389SafeReadyToContinueProofMetadata(array $overrides = []): array
|
|
{
|
|
return array_replace([
|
|
'proof_currentness' => ResolutionProofCurrentness::Current->value,
|
|
'proof_usability' => ResolutionProofUsability::Usable->value,
|
|
'proof_visibility' => ResolutionProofVisibility::OperatorVisible->value,
|
|
'proof_summary' => [
|
|
'message' => 'Current review proof is available.',
|
|
],
|
|
], $overrides);
|
|
}
|
|
|
|
function spec389ReviewPublicationProviderSection(
|
|
User $user,
|
|
ManagedEnvironment $tenant,
|
|
?string $selectedStatus = null,
|
|
?string $selectedUpdated = null,
|
|
int $previewLimit = 10,
|
|
): array {
|
|
$workspace = Workspace::query()->findOrFail((int) $tenant->workspace_id);
|
|
|
|
return app(ReviewPublicationResolutionInboxProvider::class)->section(
|
|
user: $user,
|
|
workspace: $workspace,
|
|
reviewTenants: [(int) $tenant->getKey() => $tenant->fresh()],
|
|
selectedTenant: null,
|
|
selectedStatus: $selectedStatus,
|
|
selectedUpdated: $selectedUpdated,
|
|
navigationContext: null,
|
|
previewLimit: $previewLimit,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>|null> $entries
|
|
* @return list<string>
|
|
*/
|
|
function spec389EntryActionLabels(array $entries): array
|
|
{
|
|
return collect($entries)
|
|
->filter(fn (mixed $entry): bool => is_array($entry))
|
|
->flatMap(function (array $entry): array {
|
|
return array_merge(
|
|
collect($entry['secondary_actions'] ?? [])->pluck('label')->all(),
|
|
collect($entry['linked_records'] ?? [])->pluck('label')->all(),
|
|
);
|
|
})
|
|
->filter(fn (mixed $label): bool => is_string($label))
|
|
->values()
|
|
->all();
|
|
}
|