150 lines
6.5 KiB
PHP
150 lines
6.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\StoredReport;
|
|
use App\Models\Tenant;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('maps stored reports into immutable reference, lifecycle, and retention truth', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$historical = StoredReport::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
|
|
'fingerprint' => 'permission-posture-v1',
|
|
'created_at' => now()->subDay(),
|
|
'updated_at' => now()->subDay(),
|
|
]);
|
|
|
|
$current = StoredReport::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
|
|
'fingerprint' => 'permission-posture-v2',
|
|
'previous_fingerprint' => 'permission-posture-v1',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$historicalTruth = app(ArtifactTruthPresenter::class)->for($historical->fresh());
|
|
$currentTruth = app(ArtifactTruthPresenter::class)->for($current->fresh());
|
|
|
|
expect($historicalTruth)->not->toBeNull()
|
|
->and($currentTruth)->not->toBeNull();
|
|
|
|
$historicalState = $historicalTruth?->toArray();
|
|
$currentState = $currentTruth?->toArray();
|
|
|
|
expect($historicalState['displayReference'] ?? null)
|
|
->toContain('Stored report')
|
|
->toContain('#'.$historical->getKey())
|
|
->and($historicalState['integrityAnchor'] ?? null)->toBe('permission-posture-v1')
|
|
->and($historicalState['lifecycleState'] ?? null)->toBe('historical')
|
|
->and($historicalState['retentionState'] ?? null)->toBe('retained')
|
|
->and($currentState['displayReference'] ?? null)->toContain('#'.$current->getKey())
|
|
->and($currentState['integrityAnchor'] ?? null)->toBe('permission-posture-v2')
|
|
->and($currentState['lifecycleState'] ?? null)->toBe('current')
|
|
->and($currentState['retentionState'] ?? null)->toBe('retained');
|
|
});
|
|
|
|
it('maps accepted-risk decision history into current and superseded artifact truth', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$finding = Finding::factory()->for($tenant)->create();
|
|
|
|
$exception = FindingException::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_ACTIVE,
|
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
|
'request_reason' => 'Approve temporary exception',
|
|
'requested_at' => now()->subDays(10),
|
|
'approved_at' => now()->subDays(9),
|
|
'effective_from' => now()->subDays(9),
|
|
'expires_at' => now()->addDays(14),
|
|
'review_due_at' => now()->addWeek(),
|
|
'evidence_summary' => ['reference_count' => 1],
|
|
]);
|
|
|
|
$requestedDecision = $exception->decisions()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'actor_user_id' => (int) $user->getKey(),
|
|
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
|
'reason' => 'Approve temporary exception',
|
|
'metadata' => [],
|
|
'decided_at' => now()->subDays(10),
|
|
]);
|
|
|
|
$approvedDecision = $exception->decisions()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'actor_user_id' => (int) $user->getKey(),
|
|
'decision_type' => FindingExceptionDecision::TYPE_APPROVED,
|
|
'reason' => 'Approved for a bounded review period',
|
|
'metadata' => [],
|
|
'effective_from' => now()->subDays(9),
|
|
'expires_at' => now()->addDays(14),
|
|
'decided_at' => now()->subDays(9),
|
|
]);
|
|
|
|
$exception->forceFill(['current_decision_id' => (int) $approvedDecision->getKey()])->save();
|
|
|
|
$requestedTruth = app(ArtifactTruthPresenter::class)->for($requestedDecision->fresh('exception.currentDecision'));
|
|
$approvedTruth = app(ArtifactTruthPresenter::class)->for($approvedDecision->fresh('exception.currentDecision'));
|
|
|
|
expect($requestedTruth)->not->toBeNull()
|
|
->and($approvedTruth)->not->toBeNull();
|
|
|
|
$requestedState = $requestedTruth?->toArray();
|
|
$approvedState = $approvedTruth?->toArray();
|
|
|
|
expect($requestedState['displayReference'] ?? null)
|
|
->toContain('Accepted-risk decision')
|
|
->toContain('#'.$requestedDecision->getKey())
|
|
->and($requestedState['lifecycleState'] ?? null)->toBe('superseded')
|
|
->and($requestedState['retentionState'] ?? null)->toBe('retained')
|
|
->and($approvedState['displayReference'] ?? null)->toContain('#'.$approvedDecision->getKey())
|
|
->and($approvedState['lifecycleState'] ?? null)->toBe('current')
|
|
->and($approvedState['retentionState'] ?? null)->toBe('retained');
|
|
});
|
|
|
|
it('keeps expired direct access separate from historical lifecycle on review packs', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$pack = ReviewPack::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'status' => ReviewPackStatus::Expired->value,
|
|
'fingerprint' => 'review-pack-expired',
|
|
'sha256' => 'sha-expired-pack',
|
|
'generated_at' => now()->subDays(3),
|
|
'expires_at' => now()->subDay(),
|
|
]);
|
|
|
|
$truth = app(ArtifactTruthPresenter::class)->forReviewPack($pack->fresh());
|
|
$state = $truth->toArray();
|
|
|
|
expect($state['displayReference'] ?? null)->not->toBeNull()
|
|
->and($state['displayReference'])->toContain('Review pack')
|
|
->and($state['displayReference'])->toContain('#'.$pack->getKey())
|
|
->and($state['integrityAnchor'] ?? null)->toBe('sha-expired-pack')
|
|
->and($state['lifecycleState'] ?? null)->toBe('historical')
|
|
->and($state['retentionState'] ?? null)->toBe('expired_direct_access');
|
|
});
|