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. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #427
555 lines
23 KiB
PHP
555 lines
23 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ReviewPack;
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
|
|
use App\Services\ReviewPackService;
|
|
use App\Services\Settings\SettingsWriter;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\ReviewPackStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Tests\Support\FailHardGraphClient;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
// ─── Helper ──────────────────────────────────────────────────
|
|
|
|
function createReadyPackWithFile(?array $packOverrides = []): array
|
|
{
|
|
[$user, $tenant] = createUserWithTenant();
|
|
|
|
$filePath = 'review-packs/'.$tenant->external_id.'/test.zip';
|
|
Storage::disk('exports')->put($filePath, 'PK-fake-zip-content');
|
|
|
|
$pack = ReviewPack::factory()->ready()->create(array_merge([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'file_path' => $filePath,
|
|
'file_disk' => 'exports',
|
|
'sha256' => hash('sha256', 'PK-fake-zip-content'),
|
|
], $packOverrides));
|
|
|
|
return [$user, $tenant, $pack];
|
|
}
|
|
|
|
function createCurrentReviewPackForRenderedReport(
|
|
?array $packOverrides = [],
|
|
bool $customerSafeReady = false,
|
|
?\App\Models\EvidenceSnapshot $snapshot = null,
|
|
): array
|
|
{
|
|
$packOverrides ??= [];
|
|
$tenant = \App\Models\ManagedEnvironment::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
$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);
|
|
}
|
|
|
|
$filePath = 'review-packs/'.$tenant->external_id.'/rendered-report.zip';
|
|
Storage::disk('exports')->put($filePath, 'PK-rendered-report-content');
|
|
|
|
$summary = array_replace_recursive([
|
|
'governance_package' => [
|
|
'executive_summary' => 'The released review is ready for management handoff.',
|
|
'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-rendered-report-content'),
|
|
'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()];
|
|
}
|
|
|
|
function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
|
{
|
|
app(SettingsWriter::class)->updateWorkspaceCommercialLifecycle(
|
|
actor: PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::DIRECTORY_VIEW,
|
|
PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE,
|
|
],
|
|
'is_active' => true,
|
|
]),
|
|
workspace: $pack->workspace,
|
|
state: WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY,
|
|
reason: 'Download preservation test',
|
|
);
|
|
}
|
|
|
|
// ─── Happy Path: Signed URL → 200 ───────────────────────────
|
|
|
|
it('downloads a ready pack via signed URL with correct headers', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile();
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
|
'source_surface' => 'customer_review_workspace',
|
|
'review_id' => '789',
|
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
|
'interpretation_version' => 'compliance_evidence_mapping.v1',
|
|
]);
|
|
$packCount = ReviewPack::query()->count();
|
|
$operationRunCount = OperationRun::query()->count();
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertOk();
|
|
$response->assertHeader('X-Review-Pack-SHA256', $pack->sha256);
|
|
$response->assertDownload();
|
|
|
|
$audit = AuditLog::query()
|
|
->where('action', AuditActionId::ReviewPackDownloaded->value)
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($audit)->not->toBeNull()
|
|
->and($audit?->resource_type)->toBe('review_pack')
|
|
->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey())
|
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe('customer_review_workspace')
|
|
->and(data_get($audit?->metadata, 'review_id'))->toBe('789')
|
|
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
|
|
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe('compliance_evidence_mapping.v1')
|
|
->and(ReviewPack::query()->count())->toBe($packCount)
|
|
->and(OperationRun::query()->count())->toBe($operationRunCount);
|
|
});
|
|
|
|
it('keeps ready pack downloads available while the workspace is suspended read-only', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile();
|
|
suspendReadyPackWorkspaceForDownloadTest($pack);
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
|
'source_surface' => 'suspended_read_only_check',
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertOk();
|
|
$response->assertHeader('X-Review-Pack-SHA256', $pack->sha256);
|
|
$response->assertDownload();
|
|
});
|
|
|
|
it('renders the current review pack as a customer-safe management report via signed URL without creating a download audit event or provider calls', function (): void {
|
|
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
$this->app->instance(GraphClientInterface::class, new FailHardGraphClient());
|
|
$packCount = ReviewPack::query()->count();
|
|
$operationRunCount = OperationRun::query()->count();
|
|
|
|
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View customer-safe report')
|
|
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download customer-safe review pack');
|
|
|
|
$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('Rendered review report')
|
|
->assertSee('Customer-safe report ready')
|
|
->assertSee('Executive summary')
|
|
->assertSee('Overall state')
|
|
->assertSee('Reason')
|
|
->assertSee('Impact')
|
|
->assertSee('Recommended next action')
|
|
->assertSee('Prepared by '.$tenant->workspace->name.' for '.$tenant->name)
|
|
->assertSee('Generated by TenantPilot')
|
|
->assertSee('Download customer-safe review pack')
|
|
->assertSee('No open risks are listed for this review.')
|
|
->assertSee('No accepted risks are listed for this review.')
|
|
->assertSee('No governance decisions require customer awareness in this released review.')
|
|
->assertSee('Evidence basis')
|
|
->assertSee('Supporting appendix')
|
|
->assertSee('Non-certification disclosure')
|
|
->assertSee('@media print', false)
|
|
->assertSee('.report-toolbar, .screen-only { display: none !important; }', false)
|
|
->assertSee((string) data_get($pack->summary, 'governance_package.executive_summary'))
|
|
->assertDontSee('Platform reason family')
|
|
->assertDontSee('localization.')
|
|
->assertDontSee('Evidence state:')
|
|
->assertDontSee('Section completeness:')
|
|
->assertDontSee('Total findings')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee('Do not share externally before review.')
|
|
->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('renders limitations near the top without claiming customer-safe readiness', function (): void {
|
|
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
|
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
|
$review = $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']);
|
|
$pack = $pack->fresh(['environmentReview']);
|
|
|
|
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View report with limitations')
|
|
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download review pack with limitations');
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
$content = (string) $response->getContent();
|
|
|
|
$response->assertOk()
|
|
->assertSee('Report with limitations')
|
|
->assertSee('Do not share externally before review.')
|
|
->assertSee('Output limitations')
|
|
->assertSee('Download review pack with limitations')
|
|
->assertSee('The evidence basis is incomplete, stale, or missing.')
|
|
->assertSee('Review or refresh the evidence basis before external sharing.')
|
|
->assertSee('This report is anchored to Evidence snapshot #')
|
|
->assertDontSee('Customer-safe report ready')
|
|
->assertDontSee('Customer-ready report')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee('localization.');
|
|
|
|
expect(strpos($content, 'data-testid="rendered-report-output-limitations"'))
|
|
->toBeLessThan(strpos($content, 'data-testid="rendered-report-evidence-basis"'));
|
|
});
|
|
|
|
it('renders internal pii reports with a visible internal warning and qualified labels', function (): void {
|
|
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(
|
|
packOverrides: [
|
|
'options' => [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
],
|
|
],
|
|
customerSafeReady: true,
|
|
);
|
|
|
|
expect(\App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($review))->toBe('View internal report')
|
|
->and(\App\Filament\Resources\ReviewPackResource::downloadActionLabelFor($pack))->toBe('Download internal review pack');
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get($signedUrl)
|
|
->assertOk()
|
|
->assertSee('Internal report with limitations')
|
|
->assertSee('Do not share externally before review.')
|
|
->assertSee('This output includes internal or PII-bearing detail.')
|
|
->assertSee('Download internal review pack')
|
|
->assertDontSee('Customer-safe report ready')
|
|
->assertDontSee('Customer-ready report')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('Share with customer')
|
|
->assertDontSee('localization.');
|
|
});
|
|
|
|
it('renders localized report chrome and copy without exposing localization keys', function (): void {
|
|
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
|
$user->forceFill(['preferred_locale' => 'de'])->save();
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get($signedUrl)
|
|
->assertOk()
|
|
->assertSee('Kundensicherer Bericht bereit')
|
|
->assertSee('Executive-Zusammenfassung')
|
|
->assertSee('Erstellt von '.$tenant->workspace->name.' für '.$tenant->name)
|
|
->assertSee('Unterstützender Anhang')
|
|
->assertSee('Nicht-Zertifizierungs-Offenlegung')
|
|
->assertDontSee('localization.');
|
|
});
|
|
|
|
it('renders accepted risks from customer-safe summaries without leaking internal rationale', function (): void {
|
|
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(
|
|
packOverrides: [
|
|
'summary' => [
|
|
'governance_package' => [
|
|
'accepted_risks' => [
|
|
[
|
|
'title' => 'MFA exception accepted during migration',
|
|
'governance_state' => 'expiring_exception',
|
|
'customer_safe_summary' => 'The exception is time-bound and tracked for customer awareness.',
|
|
'summary' => 'Internal committee rationale must stay internal.',
|
|
'owner_label' => 'Customer Success Lead',
|
|
'expires_at' => '2026-07-15',
|
|
],
|
|
[
|
|
'title' => 'Legacy device exception',
|
|
'governance_state' => 'expired_exception',
|
|
'summary' => 'Internal rationale without customer-safe copy.',
|
|
'owner_label' => 'Security Operations',
|
|
'review_due_at' => '2026-06-01',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
customerSafeReady: true,
|
|
);
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get($signedUrl)
|
|
->assertOk()
|
|
->assertSee('MFA exception accepted during migration')
|
|
->assertSee('Expiring soon')
|
|
->assertSee('Expires on 2026-07-15')
|
|
->assertSee('Owner: Customer Success Lead')
|
|
->assertSee('The exception is time-bound and tracked for customer awareness.')
|
|
->assertSee('Legacy device exception')
|
|
->assertSee('Expired')
|
|
->assertSee('Review due on 2026-06-01')
|
|
->assertSee('A customer-safe accepted-risk summary is not recorded.')
|
|
->assertDontSee('Internal committee rationale must stay internal.')
|
|
->assertDontSee('Internal rationale without customer-safe copy.')
|
|
->assertDontSee('Certified report')
|
|
->assertDontSee('Approved compliance report')
|
|
->assertDontSee('localization.');
|
|
});
|
|
|
|
it('returns 404 for a rendered report when the pack is no longer the current export', function (): void {
|
|
[$user, $tenant, $review] = createCurrentReviewPackForRenderedReport();
|
|
|
|
$oldPack = 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(),
|
|
'expires_at' => now()->addDay(),
|
|
]);
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($oldPack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get($signedUrl)
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('returns 404 for a rendered report when the user is not a tenant member', function (): void {
|
|
[$owner, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport();
|
|
$otherTenant = \App\Models\ManagedEnvironment::factory()->create();
|
|
[$otherUser] = createUserWithTenant(tenant: $otherTenant, role: 'owner');
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
|
'source_surface' => 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($otherUser)
|
|
->get($signedUrl)
|
|
->assertNotFound();
|
|
});
|
|
|
|
// ─── Expired Signature → 403 ────────────────────────────────
|
|
|
|
it('rejects requests with an expired signature', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile();
|
|
|
|
// Generate a signed URL that expires immediately
|
|
$signedUrl = URL::signedRoute(
|
|
'admin.review-packs.download',
|
|
['reviewPack' => $pack->getKey()],
|
|
now()->subMinute(),
|
|
);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertForbidden();
|
|
});
|
|
|
|
// ─── Expired Pack → 404 ─────────────────────────────────────
|
|
|
|
it('returns 404 for an expired pack', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile([
|
|
'status' => ReviewPackStatus::Expired->value,
|
|
]);
|
|
|
|
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
// ─── Non-Ready Pack → 404 ───────────────────────────────────
|
|
|
|
it('returns 404 for a queued pack', function (): void {
|
|
[$user, $tenant] = createUserWithTenant();
|
|
|
|
$pack = ReviewPack::factory()->queued()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
]);
|
|
|
|
$signedUrl = URL::signedRoute(
|
|
'admin.review-packs.download',
|
|
['reviewPack' => $pack->getKey()],
|
|
now()->addHour(),
|
|
);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
// ─── Non-Existent Pack → 404 ────────────────────────────────
|
|
|
|
it('returns 404 for a non-existent pack', function (): void {
|
|
[$user, $tenant] = createUserWithTenant();
|
|
|
|
$signedUrl = URL::signedRoute(
|
|
'admin.review-packs.download',
|
|
['reviewPack' => 99999],
|
|
now()->addHour(),
|
|
);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
// ─── Past Expiry Date → 404 ─────────────────────────────────
|
|
|
|
it('returns 404 when pack status is ready but expires_at is in the past', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile([
|
|
'expires_at' => now()->subDay(),
|
|
]);
|
|
|
|
$signedUrl = URL::signedRoute(
|
|
'admin.review-packs.download',
|
|
['reviewPack' => $pack->getKey()],
|
|
now()->addHour(),
|
|
);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
// ─── Missing File on Disk → 404 ─────────────────────────────
|
|
|
|
it('returns 404 when file does not exist on disk', function (): void {
|
|
[$user, $tenant] = createUserWithTenant();
|
|
|
|
$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_path' => 'review-packs/does-not-exist.zip',
|
|
'file_disk' => 'exports',
|
|
]);
|
|
|
|
$signedUrl = URL::signedRoute(
|
|
'admin.review-packs.download',
|
|
['reviewPack' => $pack->getKey()],
|
|
now()->addHour(),
|
|
);
|
|
|
|
$response = $this->actingAs($user)->get($signedUrl);
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
// ─── Unsigned URL → 403 ─────────────────────────────────────
|
|
|
|
it('returns 403 for an unsigned URL', function (): void {
|
|
[$user, $tenant, $pack] = createReadyPackWithFile();
|
|
|
|
$response = $this->actingAs($user)->get(
|
|
route('admin.review-packs.download', ['reviewPack' => $pack->getKey()]),
|
|
);
|
|
|
|
$response->assertForbidden();
|
|
});
|