Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #459
1327 lines
70 KiB
PHP
1327 lines
70 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Filament\Resources\EnvironmentReviewResource\Pages\ResolveReviewPublication;
|
|
use App\Jobs\ProviderConnectionHealthCheckJob;
|
|
use App\Models\AuditLog;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\StoredReport;
|
|
use App\Models\User;
|
|
use App\Services\OperationRunService;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofCurrentness;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofUsability;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofVisibility;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionService;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
it('uses newer current stored report proof to supersede an old failed report operation', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
$newReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and($step?->proof_type)->toBe('stored_report')
|
|
->and((int) $step?->proof_id)->toBe((int) $newReport->getKey())
|
|
->and(data_get($step?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Current->value)
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::Usable->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.required_reports_supersede_operation');
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::ReviewPublicationResolutionStepCompleted->value)
|
|
->where('resource_id', (string) $case->getKey())
|
|
->latest('id')
|
|
->firstOrFail();
|
|
$auditPayload = json_encode($audit->metadata, JSON_THROW_ON_ERROR);
|
|
|
|
expect($auditPayload)->toContain('proof_currentness', 'proof_usability', 'proof_visibility')
|
|
->and($auditPayload)->toContain(ResolutionProofCurrentness::Current->value, ResolutionProofUsability::Usable->value)
|
|
->and($auditPayload)->not->toContain('rawGraphPayload', 'access_token', 'secret-token');
|
|
});
|
|
|
|
it('keeps required reports complete when a latest ready report predates a failed direct report operation', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$failedStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$failedRun = $failedStep?->operationRun;
|
|
|
|
expect($failedRun)->toBeInstanceOf(OperationRun::class);
|
|
|
|
$readyReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$olderThanRun = ($failedRun->created_at ?? now())->copy()->subMinutes(10);
|
|
|
|
StoredReport::query()
|
|
->whereKey($readyReport->getKey())
|
|
->update([
|
|
'generated_at' => $olderThanRun,
|
|
'created_at' => $olderThanRun,
|
|
'updated_at' => $olderThanRun,
|
|
]);
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items', 'environmentReview.currentExportReviewPack']), $owner);
|
|
|
|
$reportStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$evidenceStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
|
|
->and($reportStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and($reportStep?->proof_type)->toBe('stored_report')
|
|
->and((int) $reportStep?->proof_id)->toBe((int) $readyReport->getKey())
|
|
->and(data_get($reportStep?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Current->value)
|
|
->and(data_get($reportStep?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::Usable->value)
|
|
->and(data_get($reportStep?->metadata, 'proof_reason_code'))->toBe('proof.required_reports_current')
|
|
->and($evidenceStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($evidenceStep?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale')
|
|
->and(data_get($evidenceStep?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('does not complete required reports or evidence with a failed latest stored report', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 failed report must not prove currentness.'];
|
|
$section->forceFill(['summary_payload' => $summaryPayload])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
$failedReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$failedReport->forceFill([
|
|
'status' => StoredReport::STATUS_FAILED,
|
|
'generated_at' => now()->addMinutes(10),
|
|
'created_at' => now()->addMinutes(10),
|
|
'updated_at' => now()->addMinutes(10),
|
|
])->save();
|
|
$review->evidenceSnapshot?->items()->where('dimension_key', 'entra_admin_roles')->update([
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'source_record_id' => (int) $failedReport->getKey(),
|
|
'source_fingerprint' => (string) $failedReport->fingerprint,
|
|
]);
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items', 'environmentReview.currentExportReviewPack']), $owner);
|
|
$reportStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$evidenceStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
|
|
->and($reportStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($reportStep?->metadata, 'proof_reason_code'))->toBe('proof.required_report_missing')
|
|
->and(data_get($reportStep?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value)
|
|
->and($evidenceStep?->status)->not->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and(data_get($evidenceStep?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale')
|
|
->and(data_get($evidenceStep?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('clears terminal timestamps when completed proof is reopened as stale', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and($step?->completed_at)->not->toBeNull();
|
|
|
|
$failedReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$failedReport->forceFill([
|
|
'status' => StoredReport::STATUS_FAILED,
|
|
'generated_at' => now()->addMinutes(10),
|
|
'created_at' => now()->addMinutes(10),
|
|
'updated_at' => now()->addMinutes(10),
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($step?->completed_at)->toBeNull()
|
|
->and($step?->failed_at)->toBeNull()
|
|
->and($step?->started_at)->toBeNull();
|
|
});
|
|
|
|
it('keeps successful operation proof inspection-only when the expected artifact is missing', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now(),
|
|
'context' => [
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Succeeded->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->not->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::InspectionOnly->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_succeeded_without_artifact');
|
|
});
|
|
|
|
it('keeps running operation proof inspection-only for the matching current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Running->value)
|
|
->and(data_get($step?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Current->value)
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::InspectionOnly->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_running');
|
|
});
|
|
|
|
it('does not keep a same-type operation without review context running for the current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($step?->operation_run_id)->toBeNull()
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_context_missing')
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('rejects malformed operation context ids for the current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'environment_review_id' => 'not-a-review-id',
|
|
],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($step?->operation_run_id)->toBeNull()
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_context_mismatch')
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('rejects operation context with conflicting review id aliases for the current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_id' => (int) $case->environment_review_id + 999,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($step?->operation_run_id)->toBeNull()
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_context_mismatch')
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('does not keep an unrelated same-scope operation running for the current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::ReviewPackGenerate->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($step?->operation_run_id)->toBeNull()
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_type_mismatch')
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::NotUsable->value);
|
|
});
|
|
|
|
it('fails closed when any active provider connection check belongs to another resolution context', function (): void {
|
|
Queue::fake();
|
|
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
$connection = ProviderConnection::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('provider', 'microsoft')
|
|
->where('is_default', true)
|
|
->firstOrFail();
|
|
$matchingRun = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now()->subMinute(),
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
$foreignRun = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'trigger' => 'manual_check',
|
|
],
|
|
]);
|
|
|
|
expect(fn () => app(\App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService::class)
|
|
->executeCurrentStep($case->fresh(['steps.operationRun', 'environmentReview.tenant']), $owner))
|
|
->toThrow(InvalidArgumentException::class, 'Provider connection check is already running');
|
|
|
|
$step = $case->fresh(['steps.operationRun'])->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Failed->value)
|
|
->and($step?->operation_run_id)->toBeNull()
|
|
->and(OperationRun::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('type', 'provider.connection.check')
|
|
->where('context->provider_connection_id', (int) $connection->getKey())
|
|
->count())->toBe(2)
|
|
->and(AuditLog::query()
|
|
->where('action', AuditActionId::ReviewPublicationResolutionOperationLinked->value)
|
|
->where('resource_id', (string) $case->getKey())
|
|
->exists())->toBeFalse()
|
|
->and($matchingRun->fresh()?->status)->toBe(OperationRunStatus::Running->value)
|
|
->and($foreignRun->fresh()?->status)->toBe(OperationRunStatus::Running->value);
|
|
|
|
Queue::assertNotPushed(ProviderConnectionHealthCheckJob::class);
|
|
});
|
|
|
|
it('dedupes an active provider connection check when it matches the current resolution context', function (): void {
|
|
Queue::fake();
|
|
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
$connection = ProviderConnection::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('provider', 'microsoft')
|
|
->where('is_default', true)
|
|
->firstOrFail();
|
|
$matchingRun = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
$result = app(\App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService::class)
|
|
->executeCurrentStep($case->fresh(['steps.operationRun', 'environmentReview.tenant']), $owner);
|
|
$step = $result['step']->fresh('operationRun');
|
|
|
|
expect($result['operation_run'])->toBeInstanceOf(OperationRun::class)
|
|
->and((int) $result['operation_run']?->getKey())->toBe((int) $matchingRun->getKey())
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Running->value)
|
|
->and((int) $step?->operation_run_id)->toBe((int) $matchingRun->getKey());
|
|
|
|
Queue::assertNotPushed(ProviderConnectionHealthCheckJob::class);
|
|
});
|
|
|
|
it('keeps failed operation proof inspection-only when no current artifact exists', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Failed->value)
|
|
->and(data_get($step?->metadata, 'proof_usability'))->toBe(ResolutionProofUsability::InspectionOnly->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_terminal_without_current_artifact')
|
|
->and($step?->proof_type)->toBe('operation_run');
|
|
});
|
|
|
|
it('does not link a reused active Entra scan from another resolution context', function (): void {
|
|
Queue::fake();
|
|
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$oldRun = app(OperationRunService::class)->ensureRunWithIdentity(
|
|
tenant: $tenant,
|
|
type: OperationRunType::EntraAdminRolesScan->value,
|
|
identityInputs: [
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'trigger' => 'scan',
|
|
],
|
|
context: [
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'trigger' => 'manual_scan',
|
|
],
|
|
initiator: $owner,
|
|
);
|
|
|
|
$result = app(\App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService::class)
|
|
->executeCurrentStep($case->fresh(['steps.operationRun', 'environmentReview.tenant']), $owner);
|
|
$run = $result['operation_run'];
|
|
|
|
expect($run)->toBeInstanceOf(OperationRun::class)
|
|
->and((int) $run?->getKey())->not->toBe((int) $oldRun->getKey())
|
|
->and(data_get($run?->context, 'environment_review_id'))->toBe((int) $case->environment_review_id)
|
|
->and(data_get($run?->context, 'review_publication_resolution_case_id'))->toBe((int) $case->getKey());
|
|
});
|
|
|
|
it('hides cross-scope operation proof instead of using it for the current step', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$otherTenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Other Scope']);
|
|
$run = OperationRun::factory()->forTenant($otherTenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Failed->value,
|
|
])->save();
|
|
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
|
|
expect(data_get($step?->metadata, 'proof_visibility'))->toBe(ResolutionProofVisibility::Hidden->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_scope_mismatch')
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and($step?->proof_id)->toBeNull();
|
|
});
|
|
|
|
it('does not leak a cross-scope operation id when current reports exist', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$otherTenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Other Superseded Scope']);
|
|
$run = OperationRun::factory()->forTenant($otherTenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Failed->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Failed->value,
|
|
])->save();
|
|
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
$metadataPayload = json_encode($step?->metadata ?? [], JSON_THROW_ON_ERROR);
|
|
|
|
expect(data_get($step?->metadata, 'proof_visibility'))->toBe(ResolutionProofVisibility::Hidden->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_scope_mismatch')
|
|
->and($step?->proof_type)->toBeNull()
|
|
->and($step?->proof_id)->toBeNull()
|
|
->and($metadataPayload)->not->toContain((string) $run->getKey(), 'superseded_operation_run_id');
|
|
});
|
|
|
|
it('does not reuse previously superseded operation metadata from another resolution context', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$otherRun = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subMinute(),
|
|
'context' => [
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey() + 1000,
|
|
],
|
|
]);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Completed->value,
|
|
'operation_run_id' => null,
|
|
'proof_type' => 'stored_report',
|
|
'proof_id' => null,
|
|
'proof_status' => StoredReport::STATUS_READY,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'proof_reason_code' => 'proof.required_reports_supersede_operation',
|
|
'proof_summary' => [
|
|
'superseded_operation_run_id' => (int) $otherRun->getKey(),
|
|
],
|
|
]),
|
|
])->save();
|
|
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$step = $step?->fresh();
|
|
$metadataPayload = json_encode($step?->metadata ?? [], JSON_THROW_ON_ERROR);
|
|
|
|
expect($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.required_reports_current')
|
|
->and($metadataPayload)->not->toContain((string) $otherRun->getKey(), 'superseded_operation_run_id');
|
|
});
|
|
|
|
it('marks evidence proof stale when required reports changed after collection', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$snapshot = seedEnvironmentReviewEvidence($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,
|
|
]);
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['evidenceSnapshot.items']), $owner);
|
|
$step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($case)->toBeInstanceOf(ReviewPublicationResolutionCase::class)
|
|
->and($step)->not->toBeNull()
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($step?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale');
|
|
});
|
|
|
|
it('routes a missing snapshot report reference to evidence collection when current reports already exist', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Required Reports Current Snapshot Missing']);
|
|
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$permissionReport = StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
|
|
->latest('id')
|
|
->firstOrFail();
|
|
$adminRolesReport = StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
|
|
->latest('id')
|
|
->firstOrFail();
|
|
|
|
$snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete);
|
|
$snapshot->items()->where('dimension_key', 'permission_posture')->update([
|
|
'state' => EvidenceCompletenessState::Missing->value,
|
|
'source_record_id' => null,
|
|
'source_fingerprint' => null,
|
|
]);
|
|
$snapshot->items()->where('dimension_key', 'entra_admin_roles')->update([
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'source_record_id' => (int) $adminRolesReport->getKey(),
|
|
'source_fingerprint' => (string) $adminRolesReport->fingerprint,
|
|
]);
|
|
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
|
|
$stepKeys = $case?->steps->pluck('step_key')->all();
|
|
$evidenceStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($permissionReport->status)->toBe(StoredReport::STATUS_READY)
|
|
->and($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
|
|
->and($stepKeys)->not->toContain(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
|
|
->and($evidenceStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($evidenceStep?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($evidenceStep?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale');
|
|
});
|
|
|
|
it('keeps the required report action first when a required report is genuinely missing', function (): void {
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
$stepKeys = $case->steps->pluck('step_key')->all();
|
|
$reportStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
expect(StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
|
|
->where('status', StoredReport::STATUS_READY)
|
|
->exists())->toBeFalse()
|
|
->and($owner)->toBeInstanceOf(User::class)
|
|
->and($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
|
|
->and($stepKeys)->toContain(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value)
|
|
->and($reportStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($reportStep?->metadata, 'proof_reason_code'))->toBe('proof.required_report_missing');
|
|
});
|
|
|
|
it('does not reopen required reports when newer ready reports make existing evidence stale', function (): void {
|
|
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Report Step Ordering']);
|
|
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$permissionReport = StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
|
|
->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::Missing->value,
|
|
'source_record_id' => null,
|
|
'source_fingerprint' => null,
|
|
]);
|
|
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
|
|
$adminRolesReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot->fresh('items'), EvidenceCompletenessState::Complete);
|
|
$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,
|
|
]);
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
|
|
$initialReportStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
expect($initialReportStep === null || $initialReportStep->status === ReviewPublicationResolutionStepStatus::Completed->value)
|
|
->toBeTrue();
|
|
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
$reportStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$evidenceStep = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
|
|
->and($reportStep === null || $reportStep->status === ReviewPublicationResolutionStepStatus::Completed->value)->toBeTrue()
|
|
->and($reportStep === null || data_get($reportStep?->metadata, 'proof_reason_code') === 'proof.required_reports_current')->toBeTrue()
|
|
->and($evidenceStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($evidenceStep?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale');
|
|
});
|
|
|
|
it('marks evidence proof stale when a newer report ties the snapshot timestamp', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 temporary case-opening blocker.'];
|
|
$section->forceFill(['summary_payload' => $summaryPayload])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
|
|
spec388ClearReviewPublicationBlockers($review->fresh('sections'));
|
|
|
|
$snapshot = $review->evidenceSnapshot?->fresh('items');
|
|
$timestamp = now()->addMinutes(5);
|
|
$snapshot?->forceFill([
|
|
'generated_at' => $timestamp,
|
|
'created_at' => $timestamp,
|
|
'updated_at' => $timestamp,
|
|
])->save();
|
|
$newReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$newReport->forceFill([
|
|
'generated_at' => $timestamp,
|
|
'created_at' => $timestamp,
|
|
'updated_at' => $timestamp,
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items', 'environmentReview.currentExportReviewPack']), $owner);
|
|
$step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($step)->not->toBeNull()
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($step?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.evidence_stale');
|
|
});
|
|
|
|
it('requires review recomposition when current evidence becomes stale after a report update', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 temporary case-opening blocker.'];
|
|
$section->forceFill(['summary_payload' => $summaryPayload])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
|
|
spec388ClearReviewPublicationBlockers($review->fresh('sections'));
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items', 'environmentReview.currentExportReviewPack']), $owner);
|
|
$collectStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
$refreshStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::RefreshReviewComposition->value);
|
|
|
|
expect($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
|
|
->and($collectStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($refreshStep)->not->toBeNull()
|
|
->and($refreshStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Pending->value)
|
|
->and(data_get($refreshStep?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($refreshStep?->metadata, 'proof_reason_code'))->toBe('proof.review_output_stale');
|
|
});
|
|
|
|
it('keeps executed evidence collection running after case refresh', function (): void {
|
|
Queue::fake();
|
|
|
|
[$owner, $tenant] = createUserWithTenant(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(\App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService::class)
|
|
->executeCurrentStep($case, $owner);
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($result['case']->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value);
|
|
|
|
expect($case->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value)
|
|
->and($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value)
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Running->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_running');
|
|
});
|
|
|
|
it('keeps source-owned review refresh operation running without matching readiness fingerprint', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 review changed before refresh execution.'];
|
|
$section->forceFill([
|
|
'completeness_state' => \App\Support\EnvironmentReviewCompletenessState::Partial->value,
|
|
'summary_payload' => $summaryPayload,
|
|
])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
$step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::RefreshReviewComposition->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EnvironmentReviewCompose->value,
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $review->evidence_snapshot_id,
|
|
],
|
|
]);
|
|
|
|
$review->forceFill(['operation_run_id' => (int) $run->getKey()])->save();
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'environment_review',
|
|
'proof_id' => (int) $review->getKey(),
|
|
'proof_status' => OperationRunStatus::Running->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => 'spec388-stale-fingerprint',
|
|
]),
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)
|
|
->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::RefreshReviewComposition->value);
|
|
|
|
expect($case->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value)
|
|
->and($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::RefreshReviewComposition->value)
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Running->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.operation_running');
|
|
});
|
|
|
|
it('keeps review pack proof stale when the current export is no longer ready', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 temporary case-opening blocker.'];
|
|
$section->forceFill(['summary_payload' => $summaryPayload])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
|
|
spec388ClearReviewPublicationBlockers($review->fresh('sections'));
|
|
|
|
$review->currentExportReviewPack?->forceFill([
|
|
'status' => ReviewPackStatus::Expired->value,
|
|
'expires_at' => now()->subMinute(),
|
|
'file_path' => null,
|
|
'file_disk' => null,
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.currentExportReviewPack']), $owner);
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::GenerateReviewPack->value);
|
|
|
|
expect($step)->not->toBeNull()
|
|
->and($step?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($step?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($step?->metadata, 'proof_reason_code'))->toBe('proof.review_pack_stale');
|
|
});
|
|
|
|
it('marks review pack proof stale when review output changes after export', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 review changed after export.'];
|
|
$section->forceFill([
|
|
'completeness_state' => \App\Support\EnvironmentReviewCompletenessState::Partial->value,
|
|
'summary_payload' => $summaryPayload,
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
$refreshStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::RefreshReviewComposition->value);
|
|
$packStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::GenerateReviewPack->value);
|
|
|
|
expect($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::RefreshReviewComposition->value)
|
|
->and($refreshStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and($packStep)->not->toBeNull()
|
|
->and($packStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Pending->value)
|
|
->and(data_get($packStep?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($packStep?->metadata, 'proof_reason_code'))->toBe('proof.review_pack_stale');
|
|
});
|
|
|
|
it('requires a new review pack when the review fingerprint changes without publication blockers', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
$section = $review->sections()->firstOrFail();
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = ['Spec388 temporary case-opening blocker.'];
|
|
$section->forceFill(['summary_payload' => $summaryPayload])->save();
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']), $owner);
|
|
|
|
spec388ClearReviewPublicationBlockers($review->fresh('sections'));
|
|
$review->forceFill([
|
|
'fingerprint' => hash('sha256', 'spec388-review-output-changed-without-blockers'),
|
|
])->save();
|
|
|
|
$case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.sections', 'environmentReview.evidenceSnapshot.items', 'environmentReview.currentExportReviewPack']), $owner);
|
|
$packStep = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::GenerateReviewPack->value);
|
|
|
|
expect($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::GenerateReviewPack->value)
|
|
->and($packStep)->not->toBeNull()
|
|
->and($packStep?->status)->toBe(ReviewPublicationResolutionStepStatus::Actionable->value)
|
|
->and(data_get($packStep?->metadata, 'proof_currentness'))->toBe(ResolutionProofCurrentness::Stale->value)
|
|
->and(data_get($packStep?->metadata, 'proof_reason_code'))->toBe('proof.review_pack_stale');
|
|
});
|
|
|
|
it('shows normalized proof states in the secondary technical disclosure', function (): void {
|
|
[$owner, $tenant, $case] = spec388RequiredReportCaseWithFailedOperation();
|
|
|
|
spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner);
|
|
setAdminEnvironmentContext($tenant);
|
|
|
|
Livewire::actingAs($owner)
|
|
->test(ResolveReviewPublication::class, ['record' => $case->environment_review_id])
|
|
->assertSee('Technical proof and operation history')
|
|
->assertSee('Current proof')
|
|
->assertSee('Superseded by newer result');
|
|
});
|
|
|
|
it('keeps customer-facing review workspace free of internal proof mechanics', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$review = spec388ReadyReviewWithPack($tenant, $owner);
|
|
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'published_at' => now(),
|
|
'published_by_user_id' => (int) $owner->getKey(),
|
|
])->save();
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
setAdminPanelContext();
|
|
|
|
Livewire::actingAs($owner)
|
|
->test(CustomerReviewWorkspace::class)
|
|
->assertSee('Customer Review Workspace')
|
|
->assertDontSee('proof.required_reports_current')
|
|
->assertDontSee('proof.required_reports_supersede_operation')
|
|
->assertDontSee('complete_required_reports')
|
|
->assertDontSee('generate_review_pack')
|
|
->assertDontSee('OperationRun')
|
|
->assertDontSee('proof_currentness');
|
|
});
|
|
|
|
it('shows technical operation disclosure only for a current same-scope safe-summary proof', function (): void {
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
[$operator] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'operator', workspaceRole: 'operator');
|
|
$connection = ProviderConnection::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('provider', 'microsoft')
|
|
->where('is_default', true)
|
|
->firstOrFail();
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
spec388ForceRunningOperationDisclosureStep($case, $run);
|
|
|
|
$stepState = spec388OperationDisclosureStepState($case, $tenant, $operator);
|
|
|
|
expect($owner)->toBeInstanceOf(User::class)
|
|
->and($operator->can('view', $run))->toBeTrue()
|
|
->and($stepState['operation_run_id'])->toBe((int) $run->getKey())
|
|
->and($stepState['operation_url'])->not->toBeNull();
|
|
});
|
|
|
|
it('hides technical operation disclosure for a cross-scope operation even when metadata claims currentness', function (): void {
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
[$operator] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'operator', workspaceRole: 'operator');
|
|
$otherTenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Disclosure Other Scope']);
|
|
$run = OperationRun::factory()->forTenant($otherTenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'trigger' => 'review_publication_resolution',
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
spec388ForceRunningOperationDisclosureStep($case, $run);
|
|
|
|
$stepState = spec388OperationDisclosureStepState($case, $tenant, $operator);
|
|
|
|
expect($owner)->toBeInstanceOf(User::class)
|
|
->and($stepState['operation_run_id'])->toBe((int) $run->getKey())
|
|
->and($stepState['operation_url'])->toBeNull();
|
|
});
|
|
|
|
it('hides technical operation disclosure when persisted proof summary is not already safe', function (): void {
|
|
[$owner, $tenant, $case] = spec388PermissionPostureRequiredReportCase();
|
|
[$operator] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'operator', workspaceRole: 'operator');
|
|
$connection = ProviderConnection::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('provider', 'microsoft')
|
|
->where('is_default', true)
|
|
->firstOrFail();
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Running->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'started_at' => now(),
|
|
'context' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'trigger' => 'review_publication_resolution',
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
],
|
|
]);
|
|
|
|
spec388ForceRunningOperationDisclosureStep($case, $run, [
|
|
'proof_summary' => [
|
|
'label' => 'Operation running',
|
|
'operation_type' => 'provider.connection.check',
|
|
'raw_graph_response' => [
|
|
'access_token' => 'secret-token',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$stepState = spec388OperationDisclosureStepState($case, $tenant, $operator);
|
|
|
|
expect($owner)->toBeInstanceOf(User::class)
|
|
->and($operator->can('view', $run))->toBeTrue()
|
|
->and($stepState['operation_run_id'])->toBe((int) $run->getKey())
|
|
->and($stepState['operation_url'])->toBeNull();
|
|
});
|
|
|
|
/**
|
|
* @return array{0: User, 1: ManagedEnvironment, 2: ReviewPublicationResolutionCase}
|
|
*/
|
|
function spec388RequiredReportCaseWithFailedOperation(): array
|
|
{
|
|
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Required Report']);
|
|
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$permissionReport = StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
|
|
->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::Missing->value,
|
|
'source_record_id' => null,
|
|
'source_fingerprint' => null,
|
|
]);
|
|
StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
|
|
->delete();
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
|
|
$step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => OperationRunType::EntraAdminRolesScan->value,
|
|
'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) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => OperationRunOutcome::Failed->value,
|
|
'metadata' => array_replace(is_array($step?->metadata) ? $step->metadata : [], [
|
|
'readiness_fingerprint' => (string) $case?->readiness_fingerprint,
|
|
]),
|
|
])->save();
|
|
|
|
$case?->forceFill([
|
|
'status' => ReviewPublicationResolutionCaseStatus::Blocked->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
])->save();
|
|
|
|
return [$owner, $tenant, $case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items'])];
|
|
}
|
|
|
|
/**
|
|
* @return array{0: User, 1: ManagedEnvironment, 2: ReviewPublicationResolutionCase}
|
|
*/
|
|
function spec388PermissionPostureRequiredReportCase(): array
|
|
{
|
|
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec388 Permission Posture Report']);
|
|
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', fixtureProfile: 'credential-enabled');
|
|
$snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$adminRolesReport = StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
|
|
->latest('id')
|
|
->firstOrFail();
|
|
|
|
$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::Complete->value,
|
|
'source_record_id' => (int) $adminRolesReport->getKey(),
|
|
'source_fingerprint' => (string) $adminRolesReport->fingerprint,
|
|
]);
|
|
$snapshot->items()->where('dimension_key', 'permission_posture')->update([
|
|
'state' => EvidenceCompletenessState::Missing->value,
|
|
'source_record_id' => null,
|
|
'source_fingerprint' => null,
|
|
]);
|
|
StoredReport::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
|
|
->delete();
|
|
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
$case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner);
|
|
|
|
return [$owner, $tenant, $case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items'])];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $metadata
|
|
*/
|
|
function spec388ForceRunningOperationDisclosureStep(
|
|
ReviewPublicationResolutionCase $case,
|
|
OperationRun $run,
|
|
array $metadata = [],
|
|
): void {
|
|
$step = $case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value);
|
|
|
|
$step?->forceFill([
|
|
'status' => ReviewPublicationResolutionStepStatus::Running->value,
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'proof_type' => 'operation_run',
|
|
'proof_id' => (int) $run->getKey(),
|
|
'proof_status' => (string) $run->status,
|
|
'metadata' => array_replace([
|
|
'proof_currentness' => ResolutionProofCurrentness::Current->value,
|
|
'proof_usability' => ResolutionProofUsability::InspectionOnly->value,
|
|
'proof_visibility' => ResolutionProofVisibility::OperatorVisible->value,
|
|
'proof_reason_code' => 'proof.operation_running',
|
|
'proof_summary' => [
|
|
'label' => 'Operation running',
|
|
'operation_type' => (string) $run->type,
|
|
],
|
|
], $metadata),
|
|
])->save();
|
|
|
|
$case->forceFill([
|
|
'status' => ReviewPublicationResolutionCaseStatus::WaitingForRun->value,
|
|
'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value,
|
|
])->save();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
function spec388OperationDisclosureStepState(
|
|
ReviewPublicationResolutionCase $case,
|
|
ManagedEnvironment $tenant,
|
|
User $viewer,
|
|
): array {
|
|
setAdminEnvironmentContext($tenant);
|
|
|
|
$component = Livewire::actingAs($viewer)
|
|
->test(ResolveReviewPublication::class, ['record' => (int) $case->environment_review_id]);
|
|
|
|
return collect($component->instance()->caseState()['steps'] ?? [])
|
|
->firstWhere('key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value) ?? [];
|
|
}
|
|
|
|
function spec388StoredReport(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(),
|
|
]);
|
|
}
|
|
|
|
function spec388ReadyReviewWithPack(ManagedEnvironment $tenant, User $owner): EnvironmentReview
|
|
{
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$permissionReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
|
|
$adminRolesReport = spec388StoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
|
|
$snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete);
|
|
$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,
|
|
]);
|
|
$review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items'));
|
|
$review = markEnvironmentReviewCustomerSafeReady($review);
|
|
spec388ClearReviewPublicationBlockers($review);
|
|
|
|
Storage::disk('exports')->put('review-packs/spec388-ready.zip', 'PK-test');
|
|
|
|
$packOptions = [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
];
|
|
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $review->evidence_snapshot_id,
|
|
'initiated_by_user_id' => (int) $owner->getKey(),
|
|
'status' => ReviewPackStatus::Ready->value,
|
|
'fingerprint' => app(ReviewPackService::class)->computeFingerprintForReview($review, $packOptions),
|
|
'options' => $packOptions,
|
|
'file_path' => 'review-packs/spec388-ready.zip',
|
|
'file_disk' => 'exports',
|
|
]);
|
|
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Ready->value,
|
|
'current_export_review_pack_id' => (int) $pack->getKey(),
|
|
])->save();
|
|
|
|
return $review->fresh(['sections', 'evidenceSnapshot.items', 'currentExportReviewPack']);
|
|
}
|
|
|
|
function spec388ClearReviewPublicationBlockers(EnvironmentReview $review): void
|
|
{
|
|
$baselineReadiness = [
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'readiness_state' => 'customer_ready',
|
|
'publication_blockers' => [],
|
|
'limitations' => [],
|
|
'limitation_codes' => [],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => 'customer_ready',
|
|
'verified_subject_count' => 1,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 0,
|
|
'limitation_count' => 0,
|
|
],
|
|
];
|
|
|
|
$review->sections()->get()->each(function ($section) use ($baselineReadiness): void {
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$summaryPayload['publication_blockers'] = [];
|
|
$summaryPayload['baseline_readiness'] = $baselineReadiness;
|
|
|
|
$section->forceFill([
|
|
'summary_payload' => $summaryPayload,
|
|
])->save();
|
|
});
|
|
}
|