TenantAtlas/apps/platform/tests/Feature/EnvironmentReview/Spec386ReviewPublicationResolutionWorkflowTest.php
Ahmed Darrazi 314a157233
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m18s
feat: add review publication proof currentness contract
2026-06-19 20:59:05 +02:00

408 lines
22 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Resources\EnvironmentReviewResource\Pages\ResolveReviewPublication;
use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview;
use App\Jobs\ScanEntraAdminRolesJob;
use App\Models\AuditLog;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPublicationResolutionCase;
use App\Models\StoredReport;
use App\Models\User;
use App\Services\ReviewPackService;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\EnvironmentReviewStatus;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionService;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
use Filament\Actions\Action;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('creates a review publication resolution case from current readiness truth', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Case']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
expect($case)->toBeInstanceOf(ReviewPublicationResolutionCase::class)
->and($case->status)->toBe(ReviewPublicationResolutionCaseStatus::InProgress->value)
->and($case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::ValidateReviewReadiness->value)?->status)
->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
->and($case->current_step_key)->not->toBeNull();
expect(AuditLog::query()
->where('action', AuditActionId::ReviewPublicationResolutionCreated->value)
->where('resource_id', (string) $case->getKey())
->exists())->toBeTrue();
});
it('does not create a resolution case when a mutable review is already publishable', function (): void {
[$owner, $tenant] = createUserWithTenant(role: 'owner');
$review = composeEnvironmentReviewForTest($tenant, $owner);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
expect($case)->toBeNull()
->and(ReviewPublicationResolutionCase::query()->count())->toBe(0);
});
it('does not let readonly actors create resolution cases', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Readonly No Create']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
[$readonly] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
expect(fn () => app(ReviewPublicationResolutionService::class)->openOrResume($review, $readonly))
->toThrow(AuthorizationException::class)
->and(ReviewPublicationResolutionCase::query()->forReview($review)->count())->toBe(0);
setAdminEnvironmentContext($tenant);
Livewire::actingAs($readonly)
->test(ResolveReviewPublication::class, ['record' => $review->getKey()])
->assertForbidden();
expect(ReviewPublicationResolutionCase::query()->forReview($review)->count())->toBe(0);
});
it('supersedes an active resolution case when the readiness fingerprint changes', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Supersede']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$firstCase = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$section = $review->sections()->where('required', true)->firstOrFail();
$section->forceFill([
'summary_payload' => array_replace_recursive(is_array($section->summary_payload) ? $section->summary_payload : [], [
'publication_blockers' => ['Spec386 changed blocker.'],
]),
])->save();
$secondCase = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items']), $owner);
expect($secondCase)->not->toBeNull()
->and($secondCase->getKey())->not->toBe($firstCase?->getKey())
->and($firstCase?->fresh()->status)->toBe(ReviewPublicationResolutionCaseStatus::Superseded->value)
->and(AuditLog::query()
->where('action', AuditActionId::ReviewPublicationResolutionSuperseded->value)
->where('resource_id', (string) $firstCase?->getKey())
->exists())->toBeTrue();
});
it('keeps readonly actors able to inspect but unable to execute resolution steps', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Readonly']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
[$readonly] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
expect($readonly->can('view', $case))->toBeTrue()
->and($readonly->can('executeStep', $case))->toBeFalse();
setAdminEnvironmentContext($tenant);
Livewire::actingAs($readonly)
->test(ResolveReviewPublication::class, ['record' => $review->getKey()])
->assertSee('Review can\'t be published yet')
->assertSee('Publication preparation')
->assertDontSee('Report-backed evidence')
->assertActionVisible('execute_current_step')
->assertActionDisabled('execute_current_step');
});
it('requires confirmation before executing the current resolution step', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Confirm Execute']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
setAdminEnvironmentContext($tenant);
Livewire::actingAs($owner)
->test(ResolveReviewPublication::class, ['record' => $review->getKey()])
->assertActionExists('execute_current_step', fn (Action $action): bool => $action->isConfirmationRequired());
});
it('authorizes provider report resolution from the provider capability instead of review manage', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Provider Capability']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
[$operator] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'operator', workspaceRole: 'operator');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
expect($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
->and($operator->can(Capabilities::PROVIDER_RUN, $tenant))->toBeTrue()
->and($operator->can(Capabilities::ENVIRONMENT_REVIEW_MANAGE, $tenant))->toBeFalse()
->and($operator->can('executeStep', $case))->toBeTrue();
setAdminEnvironmentContext($tenant);
Livewire::actingAs($operator)
->test(ResolveReviewPublication::class, ['record' => $review->getKey()])
->assertActionVisible('execute_current_step')
->assertActionEnabled('execute_current_step');
});
it('plans only relevant required steps for the current blocker set', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Relevant Steps']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete);
$snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([
'state' => EvidenceCompletenessState::Complete->value,
]);
$snapshot->items()->where('dimension_key', 'permission_posture')->update([
'state' => EvidenceCompletenessState::Missing->value,
'source_record_id' => null,
'source_fingerprint' => null,
]);
spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$stepKeys = $case?->steps->pluck('step_key')->all();
expect($stepKeys)->toContain(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
->and($stepKeys)->not->toContain(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
});
it('lets current readiness truth supersede stale failed operation proof on an existing step', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Stale Proof']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
$failedRun = OperationRun::factory()->forTenant($tenant)->create([
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subMinute(),
'context' => [
'environment_review_id' => (int) $review->getKey(),
'review_publication_resolution_case_id' => (int) $case?->getKey(),
],
]);
$step?->forceFill([
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
'operation_run_id' => (int) $failedRun->getKey(),
'proof_type' => 'operation_run',
'proof_id' => (int) $failedRun->getKey(),
'proof_status' => OperationRunOutcome::Failed->value,
])->save();
$snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete);
$permissionReport = spec386CreateReadyStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
$adminRolesReport = StoredReport::query()
->where('managed_environment_id', (int) $tenant->getKey())
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
->latest('id')
->firstOrFail();
$snapshot->items()->where('dimension_key', 'permission_posture')->update([
'state' => EvidenceCompletenessState::Complete->value,
'source_record_id' => (int) $permissionReport->getKey(),
'source_fingerprint' => (string) $permissionReport->fingerprint,
]);
$snapshot->items()->where('dimension_key', 'entra_admin_roles')->update([
'state' => EvidenceCompletenessState::Complete->value,
'source_record_id' => (int) $adminRolesReport->getKey(),
'source_fingerprint' => (string) $adminRolesReport->fingerprint,
]);
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
expect($step?->fresh()->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value);
});
it('queues the existing report operation when executing the required reports step', function (): void {
Queue::fake();
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Report Step']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([
'state' => EvidenceCompletenessState::Complete->value,
]);
$snapshot->items()->where('dimension_key', 'entra_admin_roles')->update([
'state' => EvidenceCompletenessState::Missing->value,
'source_record_id' => null,
'source_fingerprint' => null,
]);
spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner);
$updatedCase = $result['case']->fresh('steps.operationRun');
$runningStep = $updatedCase->steps->firstWhere('status', ReviewPublicationResolutionStepStatus::Running->value);
expect($result['operation_type'])->toBe(OperationRunType::EntraAdminRolesScan->value)
->and($updatedCase->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value)
->and($runningStep)->not->toBeNull()
->and($runningStep?->step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
->and($runningStep?->operationRun?->type)->toBe(OperationRunType::EntraAdminRolesScan->value)
->and(AuditLog::query()
->where('action', AuditActionId::ReviewPublicationResolutionOperationLinked->value)
->where('resource_id', (string) $updatedCase->getKey())
->exists())->toBeTrue();
Queue::assertPushed(ScanEntraAdminRolesJob::class);
});
it('does not persist raw source-service exception text in failed step audit payloads', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Redacted Failure']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$review = markEnvironmentReviewCustomerSafeReady($review);
$review->forceFill([
'status' => EnvironmentReviewStatus::Ready->value,
'current_export_review_pack_id' => null,
])->save();
$review->sections()->get()->each(function ($section): void {
$summary = is_array($section->summary_payload) ? $section->summary_payload : [];
$baselineReadiness = is_array($summary['baseline_readiness'] ?? null) ? $summary['baseline_readiness'] : [];
$baselineReadiness['publication_blockers'] = [];
$section->forceFill([
'completeness_state' => \App\Support\EnvironmentReviewCompletenessState::Complete->value,
'summary_payload' => array_replace($summary, [
'publication_blockers' => [],
'baseline_readiness' => $baselineReadiness,
]),
])->save();
});
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview']), $owner);
expect($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::GenerateReviewPack->value);
app()->bind(ReviewPackService::class, fn (): ReviewPackService => new class extends ReviewPackService
{
public function __construct() {}
public function generateFromReview(\App\Models\EnvironmentReview $review, User $user, array $options = []): \App\Models\ReviewPack
{
throw new RuntimeException('secret-token=abc123 rawGraphPayload {"access_token":"xyz"}');
}
});
expect(fn () => app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner))
->toThrow(RuntimeException::class);
$failedStep = $case?->fresh('steps')->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::GenerateReviewPack->value);
$failureAudit = AuditLog::query()
->where('action', AuditActionId::ReviewPublicationResolutionStepFailed->value)
->where('resource_id', (string) $case?->getKey())
->latest('id')
->firstOrFail();
$auditPayload = json_encode($failureAudit->metadata, JSON_THROW_ON_ERROR);
$stepPayload = json_encode($failedStep?->summary ?? [], JSON_THROW_ON_ERROR);
expect($failedStep?->summary)->toHaveKey('failure_code')
->and($failedStep?->summary)->not->toHaveKey('failure')
->and($auditPayload)->toContain('review_publication_resolution.step_failed_before_queue')
->and($auditPayload)->not->toContain('secret-token', 'rawGraphPayload', 'access_token')
->and($stepPayload)->not->toContain('secret-token', 'rawGraphPayload', 'access_token');
});
it('queues the existing evidence operation when executing the evidence collection step', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Evidence Step']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([
'state' => EvidenceCompletenessState::Complete->value,
]);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
$result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner);
$updatedCase = $result['case']->fresh('steps.operationRun');
$runningStep = $updatedCase->steps->firstWhere('status', ReviewPublicationResolutionStepStatus::Running->value);
expect($result['operation_type'])->toBe(OperationRunType::EvidenceSnapshotGenerate->value)
->and($updatedCase->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value)
->and($runningStep)->not->toBeNull()
->and($runningStep?->step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
->and($runningStep?->operationRun?->type)->toBe(OperationRunType::EvidenceSnapshotGenerate->value)
->and(AuditLog::query()
->where('action', AuditActionId::ReviewPublicationResolutionOperationLinked->value)
->where('resource_id', (string) $updatedCase->getKey())
->exists())->toBeTrue();
});
it('promotes blocked mutable reviews to the publication resolution page action', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Header']);
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot);
setAdminEnvironmentContext($tenant);
$expectedUrl = EnvironmentReviewResource::environmentScopedUrl('resolve-publication', ['record' => $review], $tenant);
Livewire::actingAs($owner)
->test(ViewEnvironmentReview::class, ['record' => $review->getKey()])
->assertActionExists('resolve_publication_blockers', fn (Action $action): bool => ! $action->isConfirmationRequired())
->assertActionVisible('resolve_publication_blockers')
->callAction('resolve_publication_blockers')
->assertRedirect($expectedUrl);
expect(ReviewPublicationResolutionCase::query()->forReview($review)->active()->count())->toBe(1);
});
function spec386DeleteStoredReport(ManagedEnvironment $tenant, string $reportType): void
{
StoredReport::query()
->where('workspace_id', (int) $tenant->workspace_id)
->where('managed_environment_id', (int) $tenant->getKey())
->where('report_type', $reportType)
->delete();
}
function spec386CreateReadyStoredReport(ManagedEnvironment $tenant, string $reportType): StoredReport
{
$factory = $reportType === StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES
? StoredReport::factory()->entraAdminRoles(['roles' => []])
: StoredReport::factory()->permissionPosture([
'required_count' => 0,
'granted_count' => 0,
'permissions' => [],
]);
return $factory->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'report_type' => $reportType,
'status' => StoredReport::STATUS_READY,
'generated_at' => now()->addMinute(),
'created_at' => now()->addMinute(),
'updated_at' => now()->addMinute(),
]);
}