Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 2m1s
Implemented the first version of the PDF and HTML renderer for review packs. Added ReviewPackRenderedReportController and related blade views to render reports. Updated EnvironmentReviewResource, ReviewPackResource, ReviewPackService, and routing. Added new tests for the renderer and download actions, and updated UI documentation.
441 lines
20 KiB
PHP
441 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ReviewPack;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
pest()->browser()->timeout(60_000);
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
it('smokes the customer review workspace handoff from environment review detail', function (): void {
|
|
$tenantPublished = ManagedEnvironment::factory()->create(['name' => 'Published ManagedEnvironment']);
|
|
[$user, $tenantPublished] = createUserWithTenant(
|
|
tenant: $tenantPublished,
|
|
role: 'owner',
|
|
workspaceRole: 'manager',
|
|
);
|
|
$user->forceFill(['preferred_locale' => 'de'])->save();
|
|
|
|
$tenantWithoutPublished = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $tenantPublished->workspace_id,
|
|
'name' => 'No Published ManagedEnvironment',
|
|
]);
|
|
|
|
createUserWithTenant(
|
|
tenant: $tenantWithoutPublished,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'manager',
|
|
);
|
|
|
|
$publishedSnapshot = seedEnvironmentReviewEvidence($tenantPublished, findingCount: 0, driftCount: 0);
|
|
$noPublishedSnapshot = seedEnvironmentReviewEvidence($tenantWithoutPublished, findingCount: 0, driftCount: 0);
|
|
|
|
$publishedReview = composeEnvironmentReviewForTest($tenantPublished, $user, $publishedSnapshot);
|
|
$publishedSummary = array_replace_recursive(is_array($publishedReview->summary) ? $publishedReview->summary : [], [
|
|
'control_interpretation' => [
|
|
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
|
'controls' => [
|
|
[
|
|
'control_key' => 'customer-handoff-readiness',
|
|
'title' => 'Customer handoff readiness',
|
|
'readiness_bucket' => 'evidence_on_record',
|
|
'readiness_label' => 'Evidence on record',
|
|
'primary_reason' => 'Evidence path is complete.',
|
|
'recommended_next_action' => 'Share the current review pack.',
|
|
],
|
|
],
|
|
],
|
|
'governance_package' => [
|
|
'decision_summary' => [
|
|
'status' => 'none',
|
|
'evidence_state' => EnvironmentReviewCompletenessState::Complete->value,
|
|
'decision_data_state' => 'complete',
|
|
'total_count' => 0,
|
|
'summary' => '',
|
|
'next_action' => '',
|
|
'entries' => [],
|
|
],
|
|
],
|
|
]);
|
|
$publishedReview->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'summary' => $publishedSummary,
|
|
'published_at' => now(),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
$publishedReview = markEnvironmentReviewCustomerSafeReady($publishedReview);
|
|
|
|
$internalOnlyReview = composeEnvironmentReviewForTest($tenantWithoutPublished, $user, $noPublishedSnapshot);
|
|
$internalOnlyReview->forceFill([
|
|
'status' => EnvironmentReviewStatus::Ready->value,
|
|
'published_at' => null,
|
|
'published_by_user_id' => null,
|
|
])->save();
|
|
|
|
Storage::disk('exports')->put('review-packs/customer-review-workspace-smoke.zip', 'PK-test');
|
|
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $tenantPublished->getKey(),
|
|
'workspace_id' => (int) $tenantPublished->workspace_id,
|
|
'environment_review_id' => (int) $publishedReview->getKey(),
|
|
'evidence_snapshot_id' => (int) $publishedSnapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'options' => [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
],
|
|
'summary' => [
|
|
'governance_package' => [
|
|
'executive_summary' => 'Customer steering summary for the rendered browser smoke report.',
|
|
'evidence_basis_summary' => 'Evidence basis is complete for the published smoke review.',
|
|
'top_findings' => [
|
|
[
|
|
'title' => 'Conditional access drift requires review',
|
|
'summary' => 'An inherited drift signal should stay visible in the rendered report.',
|
|
],
|
|
],
|
|
'accepted_risks' => [
|
|
[
|
|
'title' => 'Temporary exception remains accepted',
|
|
'customer_safe_summary' => 'The accepted risk stays visible as stakeholder context.',
|
|
],
|
|
],
|
|
'decision_summary' => [
|
|
'status' => 'attention_required',
|
|
'summary' => 'A governance decision still requires stakeholder awareness.',
|
|
'next_action' => 'Review the accepted exception before external sharing.',
|
|
'entries' => [
|
|
[
|
|
'title' => 'Conditional access exception',
|
|
'summary' => 'Temporary risk is accepted until remediation finishes.',
|
|
'next_action' => 'Confirm the remediation timeline before customer handoff.',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'control_interpretation' => [
|
|
'non_certification_disclosure' => 'Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.',
|
|
],
|
|
'recommended_next_actions' => [
|
|
'Share the rendered report with the customer steering group.',
|
|
],
|
|
'delivery_bundle' => [
|
|
'executive_entrypoint_file' => 'executive-summary.md',
|
|
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
|
|
],
|
|
],
|
|
'file_path' => 'review-packs/customer-review-workspace-smoke.zip',
|
|
'file_disk' => 'exports',
|
|
]);
|
|
|
|
$publishedReview->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
|
|
|
$limitedEnvironment = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $tenantPublished->workspace_id,
|
|
'name' => 'Limited Report ManagedEnvironment',
|
|
]);
|
|
createUserWithTenant(tenant: $limitedEnvironment, user: $user, role: 'owner', workspaceRole: 'manager');
|
|
[$limitedReview, $limitedPack] = spec356BrowserCreateRenderedReportPack(
|
|
environment: $limitedEnvironment,
|
|
user: $user,
|
|
snapshot: seedEnvironmentReviewEvidence($limitedEnvironment, findingCount: 0, driftCount: 0),
|
|
filePath: 'review-packs/spec356-browser-limited.zip',
|
|
evidenceOverride: EvidenceCompletenessState::Partial,
|
|
);
|
|
|
|
$internalEnvironment = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $tenantPublished->workspace_id,
|
|
'name' => 'Internal PII Report ManagedEnvironment',
|
|
]);
|
|
createUserWithTenant(tenant: $internalEnvironment, user: $user, role: 'owner', workspaceRole: 'manager');
|
|
[$internalReview, $internalPack] = spec356BrowserCreateRenderedReportPack(
|
|
environment: $internalEnvironment,
|
|
user: $user,
|
|
snapshot: seedEnvironmentReviewEvidence($internalEnvironment, findingCount: 0, driftCount: 0),
|
|
packOptions: [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
],
|
|
filePath: 'review-packs/spec356-browser-internal-pii.zip',
|
|
);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $tenantPublished->workspace_id,
|
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
|
(string) $tenantPublished->workspace_id => (int) $tenantPublished->getKey(),
|
|
],
|
|
]);
|
|
|
|
$environmentReviewPage = visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview], $tenantPublished))
|
|
->waitForText('Verwandter Kontext')
|
|
->assertSee('Kunden-Workspace öffnen')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$environmentReviewPage
|
|
->assertScript('document.querySelector(\'a.fi-link[href*="/admin/reviews/workspace?environment_id="]\') instanceof HTMLAnchorElement', true);
|
|
|
|
$workspacePage = visit(CustomerReviewWorkspace::environmentFilterUrl($tenantPublished))
|
|
->waitForText('Kundensichere Review-Pakete')
|
|
->assertSee('Filter löschen')
|
|
->assertSee('Review öffnen')
|
|
->assertSee('Governance-Paket')
|
|
->assertSee('Status')
|
|
->assertSee('Nachweise')
|
|
->assertSee('Prüfen Sie veröffentlichte Governance-Pakete, Evidence-Bereitschaft, akzeptierte Risiken und Übergabestatus über berechtigte Umgebungen hinweg.')
|
|
->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.')
|
|
->assertSee('Letztes veröffentlichtes Review')
|
|
->assertSee('Review-Paket-Index')
|
|
->assertSee('Offenlegungsregel')
|
|
->assertSee('Eingeklappt')
|
|
->assertSee('Kundensicheres Review-Paket herunterladen')
|
|
->assertSee('Das aktuelle Review-Paket ist verfügbar, erfüllt den kundensicheren Output-Vertrag und kann aus dem Review-Detail als gerenderter Bericht geöffnet werden.')
|
|
->assertSee('In diesem veröffentlichten Review benötigen keine Governance-Entscheidungen Kundenaufmerksamkeit.')
|
|
->assertSee('Kundensicheres Review-Paket bereit')
|
|
->assertSee('Verfügbar')
|
|
->assertDontSee('Customer-safe governance package index')
|
|
->assertDontSee('localization.review.customer_safe_review_workspace')
|
|
->assertDontSee('Publishable')
|
|
->assertDontSee('No mapped controls')
|
|
->assertDontSee('Compliance evidence mapping v1')
|
|
->assertDontSee('Publish review')
|
|
->assertDontSee('Refresh review');
|
|
|
|
$workspacePage
|
|
->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
|
|
|
|
visit(CustomerReviewWorkspace::getUrl(panel: 'admin'))
|
|
->waitForText('Published ManagedEnvironment')
|
|
->assertDontSee('No Published ManagedEnvironment')
|
|
->assertDontSee('No published review available yet')
|
|
->assertSeeIn('tbody tr.fi-ta-row:first-of-type td:last-child', 'Review öffnen');
|
|
|
|
$customerContextReviewUrl = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview], $tenantPublished)
|
|
.'?'.http_build_query([
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => (int) $tenantPublished->getKey(),
|
|
]);
|
|
|
|
$customerReviewDetailPage = visit($customerContextReviewUrl)
|
|
->waitForText('Ergebniszusammenfassung')
|
|
->assertSee('Kundensicheren Bericht anzeigen')
|
|
->assertSee('Governance-Paket')
|
|
->assertSee('Veröffentlichter Governance-Nachweis')
|
|
->assertSee('Review-Status')
|
|
->assertSee('Primäre Aktion')
|
|
->assertSee('Executive-Einstieg')
|
|
->assertSee('Strukturierter Auditor-Anhang')
|
|
->assertSee('Prüfgrundlage')
|
|
->assertDontSee('Released governance record')
|
|
->assertDontSee('Control readiness interpretation')
|
|
->assertDontSee('Compliance evidence mapping v1')
|
|
->assertDontSee('Publish review')
|
|
->assertDontSee('Refresh review')
|
|
->assertDontSee('Create next review')
|
|
->assertDontSee('Export executive pack')
|
|
->assertDontSee('Archive review')
|
|
->assertScript('document.querySelector(\'a[href*="/admin/review-packs/"][href*="/report"]\') instanceof HTMLAnchorElement', true)
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$renderedReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => (int) $tenantPublished->getKey(),
|
|
'review_id' => (int) $publishedReview->getKey(),
|
|
'interpretation_version' => $publishedReview->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$renderedReportPage = visit($renderedReportUrl)
|
|
->resize(1280, 1440)
|
|
->waitForText('Kundensicherer Bericht bereit')
|
|
->assertPathContains('/admin/review-packs/')
|
|
->assertPathEndsWith('/report')
|
|
->assertSee('Evidence-Basis')
|
|
->assertSee('Bericht drucken')
|
|
->assertSee('Review-Detail öffnen')
|
|
->assertSee('Review-Pack-Detail öffnen')
|
|
->assertSee('Conditional access drift requires review')
|
|
->assertSee('A governance decision still requires stakeholder awareness.')
|
|
->assertSee('Unterstützender Anhang')
|
|
->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.')
|
|
->assertDontSee('localization.')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
|
|
$renderedReportPage->screenshot(true, spec356BrowserScreenshotName('report-customer-safe-ready'));
|
|
spec356CopyBrowserScreenshot('report-customer-safe-ready');
|
|
|
|
$renderedReportPage->script('document.body.classList.add("print-preview-smoke"); window.scrollTo(0, 0);');
|
|
$renderedReportPage
|
|
->assertScript('window.getComputedStyle(document.querySelector("[data-testid=\"rendered-report-toolbar\"]")).display === "none"', true);
|
|
$renderedReportPage->screenshot(true, spec356BrowserScreenshotName('report-print-view'));
|
|
spec356CopyBrowserScreenshot('report-print-view');
|
|
|
|
$renderedReportPage->script('document.body.classList.remove("print-preview-smoke"); document.querySelector("[data-testid=\"rendered-report-supporting-appendix\"]")?.scrollIntoView({ block: "start" });');
|
|
$renderedReportPage->screenshot(false, spec356BrowserScreenshotName('report-appendix'));
|
|
spec356CopyBrowserScreenshot('report-appendix');
|
|
|
|
$limitedReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($limitedPack, [
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => (int) $limitedEnvironment->getKey(),
|
|
'review_id' => (int) $limitedReview->getKey(),
|
|
'interpretation_version' => $limitedReview->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$limitedReportPage = visit($limitedReportUrl)
|
|
->resize(1280, 1440)
|
|
->waitForText('Bericht mit Einschränkungen')
|
|
->assertSee('Nicht extern weitergeben, bevor der Bericht geprüft wurde.')
|
|
->assertSee('Output-Einschränkungen')
|
|
->assertSee('Die Evidence-Basis ist unvollständig')
|
|
->assertDontSee('Kundensicherer Bericht bereit')
|
|
->assertDontSee('localization.')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$limitedReportPage->screenshot(true, spec356BrowserScreenshotName('report-with-limitations'));
|
|
spec356CopyBrowserScreenshot('report-with-limitations');
|
|
|
|
$internalReportUrl = app(ReviewPackService::class)->generateRenderedReportUrl($internalPack, [
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => (int) $internalEnvironment->getKey(),
|
|
'review_id' => (int) $internalReview->getKey(),
|
|
'interpretation_version' => $internalReview->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$internalReportPage = visit($internalReportUrl)
|
|
->resize(1280, 1440)
|
|
->waitForText('Interner Bericht mit Einschränkungen')
|
|
->assertSee('Nicht extern weitergeben, bevor der Bericht geprüft wurde.')
|
|
->assertSee('interne oder PII-tragende Details')
|
|
->assertSee('Internes Review-Paket herunterladen')
|
|
->assertDontSee('Kundensicherer Bericht bereit')
|
|
->assertDontSee('localization.')
|
|
->assertNoJavaScriptErrors()
|
|
->assertNoConsoleLogs();
|
|
$internalReportPage->screenshot(true, spec356BrowserScreenshotName('report-internal-pii'));
|
|
spec356CopyBrowserScreenshot('report-internal-pii');
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $packOptions
|
|
* @return array{0:\App\Models\EnvironmentReview,1:ReviewPack}
|
|
*/
|
|
function spec356BrowserCreateRenderedReportPack(
|
|
ManagedEnvironment $environment,
|
|
\App\Models\User $user,
|
|
EvidenceSnapshot $snapshot,
|
|
array $packOptions = [],
|
|
string $filePath = 'review-packs/spec356-browser-report.zip',
|
|
?EvidenceCompletenessState $evidenceOverride = null,
|
|
): array {
|
|
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'published_at' => now()->subMinutes(5),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
])->save();
|
|
|
|
$review = markEnvironmentReviewCustomerSafeReady($review);
|
|
|
|
if ($evidenceOverride instanceof EvidenceCompletenessState) {
|
|
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, $evidenceOverride);
|
|
$review = $review->fresh(['sections', 'evidenceSnapshot']);
|
|
}
|
|
|
|
Storage::disk('exports')->put($filePath, 'PK-spec356-browser-test');
|
|
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'options' => array_replace([
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
], $packOptions),
|
|
'summary' => [
|
|
'governance_package' => [
|
|
'executive_summary' => 'Management summary for the Spec 356 rendered browser report.',
|
|
'evidence_basis_summary' => 'The report is anchored to the stored review evidence basis.',
|
|
'top_findings' => [],
|
|
'accepted_risks' => [],
|
|
'decision_summary' => [
|
|
'status' => 'none',
|
|
'summary' => '',
|
|
'next_action' => '',
|
|
'entries' => [],
|
|
],
|
|
],
|
|
'control_interpretation' => [
|
|
'non_certification_disclosure' => 'Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.',
|
|
],
|
|
'recommended_next_actions' => [],
|
|
'delivery_bundle' => [
|
|
'executive_entrypoint_file' => 'executive-summary.md',
|
|
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
|
|
],
|
|
],
|
|
'file_path' => $filePath,
|
|
'file_disk' => 'exports',
|
|
'generated_at' => now()->subMinutes(4),
|
|
]);
|
|
|
|
$review->forceFill([
|
|
'current_export_review_pack_id' => (int) $pack->getKey(),
|
|
])->save();
|
|
|
|
return [$review->refresh(), $pack->refresh()];
|
|
}
|
|
|
|
function spec356BrowserScreenshotName(string $name): string
|
|
{
|
|
return 'spec356-review-pack-rendered-report-'.$name;
|
|
}
|
|
|
|
function spec356CopyBrowserScreenshot(string $name): void
|
|
{
|
|
$filename = spec356BrowserScreenshotName($name).'.png';
|
|
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
|
$targetDirectory = repo_path('specs/356-review-pack-pdf-html-renderer-v1/artifacts/screenshots');
|
|
|
|
if (! is_dir($targetDirectory)) {
|
|
@mkdir($targetDirectory, 0755, true);
|
|
}
|
|
|
|
if (! is_file($source)) {
|
|
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
|
}
|
|
|
|
for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) {
|
|
usleep(100_000);
|
|
clearstatcache(true, $source);
|
|
}
|
|
|
|
if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) {
|
|
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png');
|
|
}
|
|
}
|