182 lines
7.7 KiB
PHP
182 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ReviewPack;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\ReviewPacks\CustomerOutputGate;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function spec392GateCurrentReviewPack(bool $customerSafeReady = true): array
|
|
{
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true);
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$review->forceFill([
|
|
'status' => 'published',
|
|
'published_at' => now(),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
$review = markEnvironmentReviewCustomerSafeReady($review);
|
|
|
|
if (! $customerSafeReady) {
|
|
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
|
$review = $review->fresh(['sections', 'evidenceSnapshot']);
|
|
}
|
|
|
|
$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) $user->getKey(),
|
|
'options' => [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
],
|
|
'file_disk' => 'exports',
|
|
'file_path' => 'review-packs/spec392-gate.zip',
|
|
'expires_at' => now()->addDay(),
|
|
]);
|
|
|
|
$review->forceFill([
|
|
'current_export_review_pack_id' => (int) $pack->getKey(),
|
|
])->save();
|
|
|
|
return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh(['tenant', 'environmentReview'])];
|
|
}
|
|
|
|
it('allows customer output only when the current review pack is customer safe and the actor can view review packs', function (): void {
|
|
[, $tenant, , $pack] = spec392GateCurrentReviewPack(customerSafeReady: true);
|
|
[$viewer] = createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: \App\Models\User::factory()->create(),
|
|
role: 'readonly',
|
|
clearCapabilityCaches: true,
|
|
);
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $viewer);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeTrue()
|
|
->and($decision->canStreamInternalPreview)->toBeFalse()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_READY);
|
|
});
|
|
|
|
it('denies customer output when no actor is provided even if the output is ready', function (): void {
|
|
[, , , $pack] = spec392GateCurrentReviewPack(customerSafeReady: true);
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, null);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($decision->canStreamInternalPreview)->toBeFalse()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_READY);
|
|
});
|
|
|
|
it('denies customer output when the actor is missing review pack view capability even if the output is ready', function (): void {
|
|
[, , , $pack] = spec392GateCurrentReviewPack(customerSafeReady: true);
|
|
$unauthorizedActor = \App\Models\User::factory()->create();
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $unauthorizedActor);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($decision->canStreamInternalPreview)->toBeFalse()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_READY);
|
|
});
|
|
|
|
it('denies customer output when the actor can view review packs but the output is not ready', function (): void {
|
|
[, $tenant, , $pack] = spec392GateCurrentReviewPack(customerSafeReady: false);
|
|
[$viewer] = createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: \App\Models\User::factory()->create(),
|
|
role: 'readonly',
|
|
clearCapabilityCaches: true,
|
|
);
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $viewer);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($decision->canStreamInternalPreview)->toBeFalse()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_NEEDS_ATTENTION);
|
|
});
|
|
|
|
it('keeps unsafe review packs behind internal preview authorization', function (): void {
|
|
[$owner, $tenant, , $pack] = spec392GateCurrentReviewPack(customerSafeReady: false);
|
|
[$readonly] = createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: \App\Models\User::factory()->create(),
|
|
role: 'readonly',
|
|
clearCapabilityCaches: true,
|
|
);
|
|
|
|
$ownerDecision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $owner);
|
|
$readonlyDecision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $readonly);
|
|
|
|
expect($ownerDecision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($ownerDecision->canStreamInternalPreview)->toBeTrue()
|
|
->and($readonlyDecision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($readonlyDecision->canStreamInternalPreview)->toBeFalse();
|
|
});
|
|
|
|
it('classifies pii-bearing output as internal preview only', function (): void {
|
|
[$owner, , , $pack] = spec392GateCurrentReviewPack(customerSafeReady: true);
|
|
$pack->forceFill([
|
|
'options' => [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
],
|
|
])->save();
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack->fresh(['tenant', 'environmentReview']), $owner);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($decision->canStreamInternalPreview)->toBeTrue()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_INTERNAL_ONLY);
|
|
});
|
|
|
|
it('blocks missing and expired artifacts before customer output streaming', function (): void {
|
|
[$owner, , , $pack] = spec392GateCurrentReviewPack(customerSafeReady: true);
|
|
|
|
$missingArtifact = $pack->replicate()->forceFill([
|
|
'fingerprint' => fake()->sha256(),
|
|
'file_path' => null,
|
|
'file_disk' => null,
|
|
]);
|
|
$missingArtifact->save();
|
|
|
|
$expiredArtifact = $pack->replicate()->forceFill([
|
|
'fingerprint' => fake()->sha256(),
|
|
'expires_at' => now()->subMinute(),
|
|
]);
|
|
$expiredArtifact->save();
|
|
|
|
$missingDecision = app(CustomerOutputGate::class)->decisionForReviewPack($missingArtifact->fresh(['tenant', 'environmentReview']), $owner);
|
|
$expiredDecision = app(CustomerOutputGate::class)->decisionForReviewPack($expiredArtifact->fresh(['tenant', 'environmentReview']), $owner);
|
|
|
|
expect($missingDecision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($missingDecision->canStreamInternalPreview)->toBeFalse()
|
|
->and($missingDecision->state)->toBe(CustomerOutputGate::STATE_NOT_AVAILABLE)
|
|
->and($expiredDecision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($expiredDecision->canStreamInternalPreview)->toBeFalse()
|
|
->and($expiredDecision->state)->toBe(CustomerOutputGate::STATE_EXPIRED);
|
|
});
|
|
|
|
it('does not treat unanchored review pack artifacts as customer output', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true);
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'file_disk' => 'exports',
|
|
'file_path' => 'review-packs/spec392-unanchored.zip',
|
|
'expires_at' => now()->addDay(),
|
|
]);
|
|
|
|
$decision = app(CustomerOutputGate::class)->decisionForReviewPack($pack, $user);
|
|
|
|
expect($decision->canStreamCustomerOutput)->toBeFalse()
|
|
->and($decision->canStreamInternalPreview)->toBeTrue()
|
|
->and($decision->state)->toBe(CustomerOutputGate::STATE_UNKNOWN);
|
|
});
|