398 lines
16 KiB
PHP
398 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EnvironmentReviewSection;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\ReviewPacks\ReportProfileRegistry;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
it('renders a management-ready first screen with repo-backed identity, KPIs, and safe copy', function (): void {
|
|
[$user, $tenant, $review, $pack] = spec366CreateRenderedReportPack(
|
|
packOverrides: [
|
|
'summary' => [
|
|
'governance_package' => [
|
|
'executive_summary' => 'Spec 366 executive narrative for management readers.',
|
|
'top_findings' => [
|
|
[
|
|
'title' => 'Conditional access drift',
|
|
'summary' => 'One policy drift requires owner awareness.',
|
|
],
|
|
],
|
|
'accepted_risks' => [
|
|
[
|
|
'title' => 'Migration exception',
|
|
'governance_state' => 'valid_exception',
|
|
'customer_safe_summary' => 'The exception is time-bound and governance-tracked.',
|
|
'owner_label' => 'Service Delivery',
|
|
'expires_at' => '2026-07-15',
|
|
],
|
|
],
|
|
'decision_summary' => [
|
|
'status' => 'open',
|
|
'summary' => 'One governance decision requires owner awareness.',
|
|
'next_action' => 'Brief named stakeholders and retain the structured appendix.',
|
|
'entries' => [
|
|
[
|
|
'title' => 'Retain monitoring exception',
|
|
'summary' => 'Owner accepted limited monitoring until the next review.',
|
|
'next_action' => 'Confirm the next review date.',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'recommended_next_actions' => [
|
|
'Brief named stakeholders and retain the ZIP package as the structured appendix.',
|
|
],
|
|
],
|
|
],
|
|
customerSafeReady: true,
|
|
);
|
|
|
|
$signedUrl = spec366RenderedReportUrl($pack, $review, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
|
bindFailHardGraphClient();
|
|
$packCount = ReviewPack::query()->count();
|
|
$operationRunCount = OperationRun::query()->count();
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
$content = (string) $response->getContent();
|
|
$toolbarPosition = strpos($content, 'data-testid="rendered-report-toolbar"');
|
|
$canvasPosition = strpos($content, 'data-testid="rendered-report-canvas"');
|
|
|
|
$response->assertOk()
|
|
->assertSee('Spec 366 executive narrative for management readers.')
|
|
->assertSee('Prepared by Spec366 MSP for Spec366 Production')
|
|
->assertSee('Generated by TenantPilot')
|
|
->assertSee('Customer-safe report ready')
|
|
->assertSee('Report decision strip')
|
|
->assertSee('Governance status')
|
|
->assertSee('Evidence coverage')
|
|
->assertSee('Key risks')
|
|
->assertSee('Open decisions')
|
|
->assertSee('Conditional access drift')
|
|
->assertSee('Retain monitoring exception')
|
|
->assertSee('Migration exception')
|
|
->assertSee('Audience')
|
|
->assertSee('Non-certification disclosure')
|
|
->assertSee('Supporting appendix')
|
|
->assertSee('data-layout-mode="executive"', false)
|
|
->assertSee('data-appendix-prominence="minimal"', false)
|
|
->assertSee('data-testid="rendered-report-kpi-strip"', false)
|
|
->assertSee('body.print-preview-smoke .report-toolbar', false)
|
|
->assertSee('@media print', false)
|
|
->assertDontSee('localization.')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee((string) $review->fingerprint);
|
|
|
|
expect($toolbarPosition)->not->toBeFalse()
|
|
->and($canvasPosition)->not->toBeFalse()
|
|
->and($toolbarPosition)->toBeLessThan($canvasPosition)
|
|
->and(AuditLog::query()->where('action', AuditActionId::ReviewPackDownloaded->value)->count())->toBe(0)
|
|
->and(ReviewPack::query()->count())->toBe($packCount)
|
|
->and(OperationRun::query()->count())->toBe($operationRunCount);
|
|
});
|
|
|
|
it('keeps limited and PII-bearing report states visibly bounded', function (): void {
|
|
[$limitedUser, $limitedTenant, $limitedReview, $limitedPack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
|
restateEnvironmentReviewEvidenceSnapshot($limitedReview->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
|
|
|
$limitedResponse = $this->actingAs($limitedUser)->get(
|
|
spec366RenderedReportUrl(
|
|
$limitedPack->fresh(['environmentReview']),
|
|
$limitedReview->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']),
|
|
ReportProfileRegistry::CUSTOMER_EXECUTIVE,
|
|
),
|
|
);
|
|
|
|
$limitedResponse->assertOk()
|
|
->assertSee('Report with limitations')
|
|
->assertSee('Do not share externally before review.')
|
|
->assertSee('Output limitations')
|
|
->assertSee('Review or refresh the evidence basis before external sharing.')
|
|
->assertSee('data-layout-mode="executive"', false)
|
|
->assertSee('data-section-rank="30"', false)
|
|
->assertDontSee('Customer-ready report')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee('localization.');
|
|
|
|
expect($limitedTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
|
|
|
[$piiUser, $piiTenant, $piiReview, $piiPack] = spec366CreateRenderedReportPack(
|
|
packOverrides: [
|
|
'options' => [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
],
|
|
],
|
|
customerSafeReady: true,
|
|
environmentName: 'Spec366 PII Production',
|
|
);
|
|
|
|
$this->actingAs($piiUser)
|
|
->get(spec366RenderedReportUrl($piiPack, $piiReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE))
|
|
->assertOk()
|
|
->assertSee('Internal report with limitations')
|
|
->assertSee('Do not share externally before review.')
|
|
->assertSee('Customer-facing profile blocked by internal-only detail')
|
|
->assertSee('Protected values boundary')
|
|
->assertSee('data-layout-mode="executive"', false)
|
|
->assertDontSee('Customer-safe report ready')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee('localization.');
|
|
|
|
expect($piiTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
|
});
|
|
|
|
it('renders profile-aware hierarchy and keeps fallback requests visible', function (): void {
|
|
[$user, $tenant, $review, $pack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
|
|
|
$profiles = [
|
|
ReportProfileRegistry::CUSTOMER_EXECUTIVE => [
|
|
'mode' => 'executive',
|
|
'prominence' => 'minimal',
|
|
'label' => 'Customer executive',
|
|
'shows_appendix' => false,
|
|
'fallback' => false,
|
|
],
|
|
ReportProfileRegistry::CUSTOMER_TECHNICAL => [
|
|
'mode' => 'technical',
|
|
'prominence' => 'standard',
|
|
'label' => 'Customer technical',
|
|
'shows_appendix' => true,
|
|
'fallback' => false,
|
|
],
|
|
ReportProfileRegistry::INTERNAL_MSP_REVIEW => [
|
|
'mode' => 'internal',
|
|
'prominence' => 'standard',
|
|
'label' => 'Internal MSP review',
|
|
'shows_appendix' => true,
|
|
'fallback' => false,
|
|
],
|
|
ReportProfileRegistry::AUDITOR_APPENDIX => [
|
|
'mode' => 'auditor_appendix',
|
|
'prominence' => 'high',
|
|
'label' => 'Auditor appendix',
|
|
'shows_appendix' => true,
|
|
'fallback' => false,
|
|
],
|
|
ReportProfileRegistry::FRAMEWORK_READINESS => [
|
|
'mode' => 'internal',
|
|
'prominence' => 'standard',
|
|
'label' => 'Internal MSP review',
|
|
'shows_appendix' => true,
|
|
'fallback' => true,
|
|
],
|
|
];
|
|
|
|
foreach ($profiles as $requestedProfile => $expectation) {
|
|
$response = $this->actingAs($user)->get(spec366RenderedReportUrl($pack, $review, $requestedProfile));
|
|
|
|
$response->assertOk()
|
|
->assertSee($expectation['label'])
|
|
->assertSee('data-layout-mode="'.$expectation['mode'].'"', false)
|
|
->assertSee('data-appendix-prominence="'.$expectation['prominence'].'"', false)
|
|
->assertDontSee('localization.');
|
|
|
|
if ($expectation['shows_appendix']) {
|
|
$response->assertSee('Spec366 Technical Control')
|
|
->assertDontSee(__('localization.review.report_appendix_hidden_for_profile'));
|
|
} else {
|
|
$response->assertSee(__('localization.review.report_appendix_hidden_for_profile'))
|
|
->assertDontSee('Spec366 Technical Control');
|
|
}
|
|
|
|
if ($expectation['fallback']) {
|
|
$response->assertSee(__('localization.review.report_profile_fallback_notice'))
|
|
->assertSee(ReportProfileRegistry::FRAMEWORK_READINESS)
|
|
->assertSee(ReportProfileRegistry::INTERNAL_MSP_REVIEW);
|
|
}
|
|
}
|
|
|
|
expect($tenant)->toBeInstanceOf(ManagedEnvironment::class);
|
|
});
|
|
|
|
it('preserves rendered route guards and the Review Pack ZIP download contract', function (): void {
|
|
[$owner, $tenant, $review, $pack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
|
$renderedUrl = spec366RenderedReportUrl($pack, $review, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
|
$downloadUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$this->actingAs($owner)
|
|
->get($renderedUrl)
|
|
->assertOk();
|
|
|
|
$this->actingAs($owner)
|
|
->get($downloadUrl)
|
|
->assertOk()
|
|
->assertDownload();
|
|
|
|
[$outsider] = createUserWithTenant();
|
|
|
|
$this->actingAs($outsider)
|
|
->get($renderedUrl)
|
|
->assertNotFound();
|
|
|
|
[$guardUser, $guardTenant, $guardReview, $guardPack] = spec366CreateRenderedReportPack(
|
|
customerSafeReady: true,
|
|
environmentName: 'Spec366 Guard Production',
|
|
);
|
|
$guardUrl = spec366RenderedReportUrl($guardPack, $guardReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
|
$guardReview->forceFill(['current_export_review_pack_id' => null])->save();
|
|
|
|
$this->actingAs($guardUser)
|
|
->get($guardUrl)
|
|
->assertNotFound();
|
|
|
|
[$expiredUser, $expiredTenant, $expiredReview, $expiredPack] = spec366CreateRenderedReportPack(
|
|
customerSafeReady: true,
|
|
environmentName: 'Spec366 Expired Production',
|
|
);
|
|
$expiredUrl = spec366RenderedReportUrl($expiredPack, $expiredReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
|
$expiredPack->forceFill(['expires_at' => now()->subMinute()])->save();
|
|
|
|
$this->actingAs($expiredUser)
|
|
->get($expiredUrl)
|
|
->assertNotFound();
|
|
|
|
expect($tenant)->toBeInstanceOf(ManagedEnvironment::class)
|
|
->and($guardTenant)->toBeInstanceOf(ManagedEnvironment::class)
|
|
->and($expiredTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
|
});
|
|
|
|
function spec366RenderedReportUrl(ReviewPack $pack, EnvironmentReview $review, string $profile): string
|
|
{
|
|
return app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
ReportProfileRegistry::QUERY_PARAMETER => $profile,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $packOverrides
|
|
* @return array{0:\App\Models\User,1:ManagedEnvironment,2:EnvironmentReview,3:ReviewPack}
|
|
*/
|
|
function spec366CreateRenderedReportPack(
|
|
?array $packOverrides = [],
|
|
bool $customerSafeReady = false,
|
|
?EvidenceSnapshot $snapshot = null,
|
|
string $environmentName = 'Spec366 Production',
|
|
): array {
|
|
$packOverrides ??= [];
|
|
$tenant = ManagedEnvironment::factory()->active()->create([
|
|
'name' => $environmentName,
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'manager');
|
|
$tenant->workspace?->forceFill(['name' => 'Spec366 MSP'])->save();
|
|
$tenant = $tenant->fresh('workspace');
|
|
$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();
|
|
|
|
if ($customerSafeReady) {
|
|
$review = markEnvironmentReviewCustomerSafeReady($review);
|
|
}
|
|
|
|
$review->loadMissing('sections');
|
|
$appendixSection = $review->sections->first();
|
|
|
|
if ($appendixSection instanceof EnvironmentReviewSection) {
|
|
$appendixSection->forceFill([
|
|
'render_payload' => array_replace_recursive(
|
|
is_array($appendixSection->render_payload) ? $appendixSection->render_payload : [],
|
|
[
|
|
'entries' => [
|
|
[
|
|
'title' => 'Spec366 Technical Control',
|
|
'summary' => 'Visible only when the profile allows detailed appendix content.',
|
|
],
|
|
],
|
|
'highlights' => ['Spec366 appendix highlight.'],
|
|
],
|
|
),
|
|
])->save();
|
|
}
|
|
|
|
$filePath = 'review-packs/'.$tenant->external_id.'/spec366-rendered-report.zip';
|
|
Storage::disk('exports')->put($filePath, 'PK-spec366-rendered-report-content');
|
|
|
|
$summary = array_replace_recursive([
|
|
'governance_package' => [
|
|
'executive_summary' => 'Spec 366 management report summary.',
|
|
'evidence_basis_summary' => 'The report is anchored to the current released evidence snapshot.',
|
|
'top_findings' => [],
|
|
'accepted_risks' => [],
|
|
'decision_summary' => [
|
|
'status' => 'none',
|
|
'summary' => '',
|
|
'next_action' => '',
|
|
'entries' => [],
|
|
],
|
|
],
|
|
'control_interpretation' => [
|
|
'non_certification_disclosure' => 'TenantPilot summarizes available service-delivery evidence for governance review. This report is not a certification, legal attestation, audit opinion, or compliance guarantee.',
|
|
],
|
|
'recommended_next_actions' => [],
|
|
'delivery_bundle' => [
|
|
'executive_entrypoint_file' => 'executive-summary.md',
|
|
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
|
|
],
|
|
], is_array($packOverrides['summary'] ?? null) ? $packOverrides['summary'] : []);
|
|
|
|
$packAttributes = array_merge([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'options' => [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
],
|
|
'summary' => $summary,
|
|
'file_path' => $filePath,
|
|
'file_disk' => 'exports',
|
|
'sha256' => hash('sha256', 'PK-spec366-rendered-report-content'),
|
|
'generated_at' => now()->subMinutes(5),
|
|
'expires_at' => now()->addDay(),
|
|
], $packOverrides);
|
|
$packAttributes['summary'] = $summary;
|
|
|
|
$pack = ReviewPack::factory()->ready()->create($packAttributes);
|
|
|
|
$review->forceFill([
|
|
'current_export_review_pack_id' => (int) $pack->getKey(),
|
|
])->save();
|
|
|
|
return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh()];
|
|
}
|