TenantAtlas/apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php
ahmido dbff2a0a90 feat(report): implement management report pdf runtime (#450)
Added jobs, controllers, and PDF generation logic for management report runtime as defined in Spec 379. Includes artifact migrations, payload builders, and testing coverage.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #450
2026-06-15 11:36:29 +00:00

696 lines
27 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\ReviewPackResource\Pages\ViewReviewPack;
use App\Jobs\GenerateManagementReportPdfJob;
use App\Jobs\GenerateReviewPackJob;
use App\Models\AuditLog;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Services\ReviewPacks\ManagementReportPdfService;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\ReviewPacks\ManagementReportPdfPayloadBuilder;
use App\Support\ReviewPacks\ReportProfileRegistry;
use Filament\Actions\Action;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Tests\Support\FailHardGraphClient;
beforeEach(function (): void {
Storage::fake('exports');
app()->instance(GraphClientInterface::class, new FailHardGraphClient);
});
function spec379ConfigurePdfRenderer(bool $runtimeValidated = true, array $overrides = []): void
{
config([
'tenantpilot.pdf_renderer' => array_merge([
'enabled' => true,
'runtime_validated' => $runtimeValidated,
'driver' => 'gotenberg',
'base_url' => 'http://gotenberg.test',
'health_path' => '/health',
'html_route' => '/forms/chromium/convert/html',
'timeout_seconds' => 30,
'connect_timeout_seconds' => 5,
'max_html_bytes' => 1024 * 1024,
'max_asset_bytes' => 512 * 1024,
'max_output_bytes' => 2 * 1024 * 1024,
'correlation_header' => 'Gotenberg-Trace',
'output_filename' => 'tenantpilot-management-report',
], $overrides),
]);
}
/**
* @param array<string, string> $files
*/
function spec379ZipContents(array $files = []): string
{
$tempFile = tempnam(sys_get_temp_dir(), 'spec379-review-pack-');
if ($tempFile === false) {
throw new RuntimeException('Failed to allocate Spec379 archive.');
}
try {
$zip = new ZipArchive;
$result = $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($result !== true) {
throw new RuntimeException("Failed to create Spec379 archive: {$result}");
}
foreach (array_replace([
'metadata.json' => json_encode(['fixture' => 'spec379'], JSON_THROW_ON_ERROR),
'executive-summary.md' => 'Spec379 current review pack.',
], $files) as $filename => $contents) {
$zip->addFromString($filename, $contents);
}
$zip->close();
$contents = file_get_contents($tempFile);
if (! is_string($contents) || $contents === '') {
throw new RuntimeException('Spec379 archive is empty.');
}
return $contents;
} finally {
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
}
/**
* @param array<string, mixed> $summaryOverrides
* @return array{0: \App\Models\User, 1: \App\Models\ManagedEnvironment, 2: \App\Models\EnvironmentReview, 3: ReviewPack}
*/
function spec379CurrentReadyPack(array $summaryOverrides = []): 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);
$zipContents = spec379ZipContents();
$filePath = sprintf('review-packs/%s/spec379-current.zip', $tenant->external_id);
Storage::disk('exports')->put($filePath, $zipContents);
$summary = array_replace_recursive([
'governance_package' => [
'executive_summary' => 'The released review is ready for a customer management handoff.',
],
'decision_summary' => [
'entries' => [
[
'title' => 'Proceed with governed handoff',
'summary' => 'Customer-safe evidence is ready.',
'rationale' => 'Current Review Pack is complete.',
],
],
],
'top_findings' => [
[
'title' => 'Privileged access review',
'severity' => 'medium',
'summary' => 'Review privileged role assignment cadence.',
],
],
'risk_acceptance' => [
[
'title' => 'Known exception',
'summary' => 'Accepted until the next operating review.',
],
],
'recommended_next_actions' => [
[
'title' => 'Schedule review',
'summary' => 'Review the management report with customer stakeholders.',
],
],
'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.',
],
], $summaryOverrides);
$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) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
'summary' => $summary,
'file_path' => $filePath,
'file_disk' => 'exports',
'file_size' => strlen($zipContents),
'sha256' => hash('sha256', $zipContents),
'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'])];
}
function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
{
$pdfBytes = '%PDF-1.7 Spec379 ready management report';
$filePath = sprintf('management-reports/%s/ready-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk('exports')->put($filePath, $pdfBytes);
return StoredReport::factory()->managementReportPdf([
'title' => 'TenantPilot Management Report',
])->create([
'workspace_id' => (int) $pack->workspace_id,
'managed_environment_id' => (int) $pack->managed_environment_id,
'source_environment_review_id' => (int) $pack->environment_review_id,
'source_review_pack_id' => (int) $pack->getKey(),
'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE,
'file_disk' => 'exports',
'file_path' => $filePath,
'file_size' => strlen($pdfBytes),
'sha256' => hash('sha256', $pdfBytes),
'generated_at' => now(),
]);
}
it('Spec379 blocks generation until the PDF runtime is validated', function (): void {
spec379ConfigurePdfRenderer(runtimeValidated: false);
[$user, $tenant, , $pack] = spec379CurrentReadyPack();
Queue::fake();
$runCount = OperationRun::query()->count();
$reportCount = StoredReport::query()
->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)
->count();
$service = app(ManagementReportPdfService::class);
$decision = $service->generationDecision($pack);
$result = $service->startGeneration($pack, $user);
expect($decision['is_blocked'])->toBeTrue()
->and($decision['reason_code'])->toBe('runtime_validation_missing')
->and($result['mode'])->toBe('blocked')
->and(OperationRun::query()->count())->toBe($runCount)
->and(StoredReport::query()->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)->count())->toBe($reportCount);
Queue::assertNothingPushed();
$audit = AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfGenerationBlocked->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id)
->and($audit?->resource_type)->toBe('stored_report')
->and(data_get($audit?->metadata, 'decision.reason_code'))->toBe('runtime_validation_missing');
});
it('Spec379 queues generation, renders through Gotenberg, stores a private PDF, and records operation proof', function (): void {
spec379ConfigurePdfRenderer();
[$user, $tenant, $review, $pack] = spec379CurrentReadyPack([
'top_findings' => [
[
'title' => 'Secret-shaped source value',
'summary' => 'SQLSTATE raw error with access_token should never appear.',
],
],
]);
Queue::fake();
$service = app(ManagementReportPdfService::class);
$result = $service->startGeneration($pack, $user);
expect($result['mode'])->toBe('queued')
->and($result['report'])->toBeInstanceOf(StoredReport::class)
->and($result['operation_run'])->toBeInstanceOf(OperationRun::class);
Queue::assertPushed(GenerateManagementReportPdfJob::class);
/** @var StoredReport $report */
$report = $result['report'];
/** @var OperationRun $run */
$run = $result['operation_run'];
expect($run->type)->toBe(OperationRunType::ManagementReportGenerate->value)
->and($report->status)->toBe(StoredReport::STATUS_QUEUED)
->and($report->source_review_pack_id)->toBe((int) $pack->getKey())
->and($report->source_environment_review_id)->toBe((int) $review->getKey());
Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [
'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-render',
]),
]);
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $report->getKey(),
operationRunId: (int) $run->getKey(),
), 'handle']);
$report->refresh();
$run->refresh();
expect($report->status)->toBe(StoredReport::STATUS_READY)
->and($report->file_disk)->toBe('exports')
->and(Storage::disk('exports')->exists((string) $report->file_path))->toBeTrue()
->and($report->sha256)->toBe(hash('sha256', '%PDF-1.7 rendered spec379'))
->and(json_encode($report->payload, JSON_THROW_ON_ERROR))->not->toContain('SQLSTATE', 'access_token')
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
->and($run->summary_counts)->toMatchArray([
'total' => 1,
'processed' => 1,
'succeeded' => 1,
'report_created' => 1,
]);
Http::assertSent(fn (Request $request): bool => $request->url() === 'http://gotenberg.test/forms/chromium/convert/html'
&& $request->method() === 'POST');
$audit = AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfGenerated->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and($audit?->workspace_id)->toBe((int) $tenant->workspace_id)
->and($audit?->resource_type)->toBe('stored_report')
->and(data_get($audit?->metadata, 'stored_report_id'))->toBe((int) $report->getKey());
});
it('Spec379 builds a customer-executive payload without profile fallback or sensitive source strings', function (): void {
[$user, , , $pack] = spec379CurrentReadyPack([
'decision_summary' => [
'entries' => [
[
'title' => 'Token shaped value',
'summary' => 'Bearer abc.def access_token=secret should be redacted.',
],
],
],
]);
$payload = app(ManagementReportPdfPayloadBuilder::class)->build($pack);
$encoded = json_encode($payload, JSON_THROW_ON_ERROR);
expect($payload['profile'])->toBe(ReportProfileRegistry::CUSTOMER_EXECUTIVE)
->and($encoded)->toContain('Executive summary')
->and($encoded)->toContain('Method summary')
->and($encoded)->not->toContain('Bearer abc.def', 'access_token=secret', 'Internal MSP review')
->and($user)->not->toBeNull();
});
it('Spec379 maps source and disclosure blockers before rendering', function (): void {
spec379ConfigurePdfRenderer();
[$user, , $review, $pack] = spec379CurrentReadyPack();
Queue::fake();
$service = app(ManagementReportPdfService::class);
Storage::disk('exports')->delete((string) $pack->file_path);
expect($service->generationDecision($pack)['reason_code'])->toBe('source_artifact_missing');
Storage::disk('exports')->put((string) $pack->file_path, spec379ZipContents());
$pack->forceFill(['expires_at' => now()->subMinute()])->save();
expect($service->generationDecision($pack->fresh())['reason_code'])->toBe('review_pack_expired');
$pack->forceFill(['expires_at' => now()->addDay()])->save();
$review->forceFill(['current_export_review_pack_id' => null])->save();
expect($service->generationDecision($pack->fresh(['environmentReview']))['reason_code'])->toBe('review_pack_not_current');
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
$pack->forceFill([
'options' => [
'include_pii' => true,
'include_operations' => true,
],
])->save();
$runCount = OperationRun::query()->count();
$reportCount = StoredReport::query()
->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)
->count();
$disclosureDecision = $service->generationDecision($pack->fresh(['environmentReview']));
$result = $service->startGeneration($pack->fresh(['tenant', 'environmentReview']), $user);
expect($disclosureDecision['is_blocked'])->toBeTrue()
->and($disclosureDecision['reason_code'])->toBe('customer_profile_internal_only')
->and($result['mode'])->toBe('blocked')
->and(OperationRun::query()->count())->toBe($runCount)
->and(StoredReport::query()->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF)->count())->toBe($reportCount);
Queue::assertNotPushed(GenerateManagementReportPdfJob::class);
expect(fn () => app(ManagementReportPdfPayloadBuilder::class)->build($pack->fresh(['environmentReview'])))
->toThrow(InvalidArgumentException::class);
});
it('Spec379 marks queued generation as blocked when disclosure changes before the job runs', function (): void {
Queue::fake();
spec379ConfigurePdfRenderer();
[$user, , , $pack] = spec379CurrentReadyPack();
$result = app(ManagementReportPdfService::class)->startGeneration($pack, $user);
/** @var StoredReport $report */
$report = $result['report'];
/** @var OperationRun $run */
$run = $result['operation_run'];
$pack->forceFill([
'options' => [
'include_pii' => true,
'include_operations' => true,
],
])->save();
Http::fake();
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $report->getKey(),
operationRunId: (int) $run->getKey(),
), 'handle']);
$report->refresh();
$run->refresh();
expect($report->status)->toBe(StoredReport::STATUS_FAILED)
->and(data_get($report->payload, 'blocked'))->toBeTrue()
->and($report->file_path)->toBeNull()
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and($run->summary_counts)->toMatchArray([
'total' => 1,
'processed' => 1,
'succeeded' => 0,
'failed' => 0,
])
->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.customer_profile_internal_only');
Http::assertNothingSent();
$audit = AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfGenerationBlocked->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and(data_get($audit?->metadata, 'reason_code'))->toBe('customer_profile_internal_only');
});
it('Spec379 records renderer failure without exposing a ready artifact', function (): void {
Queue::fake();
spec379ConfigurePdfRenderer();
[$user, , , $pack] = spec379CurrentReadyPack();
$result = app(ManagementReportPdfService::class)->startGeneration($pack, $user);
/** @var StoredReport $report */
$report = $result['report'];
/** @var OperationRun $run */
$run = $result['operation_run'];
Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::sequence()
->push('renderer failed', 500, [
'Content-Type' => 'text/plain',
])
->push('%PDF-1.7 rendered spec379 retry', 200, [
'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-renderer-retry',
]),
]);
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $report->getKey(),
operationRunId: (int) $run->getKey(),
), 'handle']);
$report->refresh();
$run->refresh();
expect($report->status)->toBe(StoredReport::STATUS_FAILED)
->and($report->file_path)->toBeNull()
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.renderer_failed');
Queue::fake();
$retry = app(ManagementReportPdfService::class)->startGeneration($pack->fresh(['tenant', 'environmentReview']), $user);
/** @var StoredReport $retryReport */
$retryReport = $retry['report'];
/** @var OperationRun $retryRun */
$retryRun = $retry['operation_run'];
expect($retry['mode'])->toBe('queued')
->and($retryReport->getKey())->toBe($report->getKey())
->and($retryReport->status)->toBe(StoredReport::STATUS_QUEUED)
->and($retryReport->operation_run_id)->toBe((int) $retryRun->getKey())
->and($retryRun->getKey())->not->toBe($run->getKey());
Queue::assertPushed(GenerateManagementReportPdfJob::class);
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $retryReport->getKey(),
operationRunId: (int) $retryRun->getKey(),
), 'handle']);
$retryReport->refresh();
$retryRun->refresh();
expect($retryReport->status)->toBe(StoredReport::STATUS_READY)
->and($retryReport->file_path)->not->toBeNull()
->and($retryRun->status)->toBe(OperationRunStatus::Completed->value)
->and($retryRun->outcome)->toBe(OperationRunOutcome::Succeeded->value);
Http::assertSentCount(2);
});
it('Spec379 records storage failure without exposing a ready artifact', function (): void {
Queue::fake();
spec379ConfigurePdfRenderer();
[$user, , , $pack] = spec379CurrentReadyPack();
$result = app(ManagementReportPdfService::class)->startGeneration($pack, $user);
/** @var StoredReport $report */
$report = $result['report'];
/** @var OperationRun $run */
$run = $result['operation_run'];
Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [
'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-storage-failure',
]),
]);
$fakeDisk = \Mockery::mock(\Illuminate\Contracts\Filesystem\Filesystem::class);
$fakeDisk->shouldReceive('put')
->once()
->andReturnFalse();
Storage::shouldReceive('disk')
->with('exports')
->andReturn($fakeDisk);
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $report->getKey(),
operationRunId: (int) $run->getKey(),
), 'handle']);
$report->refresh();
$run->refresh();
expect($report->status)->toBe(StoredReport::STATUS_FAILED)
->and($report->file_path)->toBeNull()
->and($report->sha256)->toBeNull()
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.storage_failed');
});
it('Spec379 downloads a ready management PDF only through the signed tenant-authorized route', function (): void {
[$user, $tenant, , $pack] = spec379CurrentReadyPack();
$report = spec379ReadyManagementPdf($pack);
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
'source_surface' => 'spec379',
]);
$this->actingAs($user)
->get($url)
->assertOk()
->assertHeader('X-Management-Report-PDF-SHA256', $report->sha256)
->assertDownload();
$audit = AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and($audit?->resource_type)->toBe('stored_report')
->and(data_get($audit?->metadata, 'stored_report_id'))->toBe((int) $report->getKey())
->and(data_get($audit?->metadata, 'source_surface'))->toBe('spec379');
[$otherUser] = createUserWithTenant(role: 'owner');
$this->actingAs($otherUser)
->get($url)
->assertNotFound();
expect($tenant)->not->toBeNull();
});
it('Spec379 enforces scoped authorization for generation and download', function (): void {
spec379ConfigurePdfRenderer();
[$owner, , , $pack] = spec379CurrentReadyPack();
[$readonly] = createUserWithTenant(tenant: $pack->tenant, user: \App\Models\User::factory()->create(), role: 'readonly', clearCapabilityCaches: true);
$report = spec379ReadyManagementPdf($pack);
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report);
try {
app(ManagementReportPdfService::class)->startGeneration($pack, $readonly);
$this->fail('Scoped readonly actor should not start management PDF generation.');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
expect($exception->getStatusCode())->toBe(403);
}
Gate::define(
Capabilities::REVIEW_PACK_VIEW,
fn (User $actor, ManagedEnvironment $tenant): bool => (int) $actor->getKey() !== (int) $readonly->getKey()
|| (int) $tenant->getKey() !== (int) $pack->tenant->getKey(),
);
$this->actingAs($readonly)
->get($url)
->assertForbidden();
[$outsideUser] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true);
$this->actingAs($outsideUser)
->get($url)
->assertNotFound();
expect($owner)->not->toBeNull();
});
it('Spec379 exposes the confirmed Filament action only when generation is available', function (): void {
spec379ConfigurePdfRenderer(runtimeValidated: false);
[$user, $tenant, , $pack] = spec379CurrentReadyPack();
setAdminEnvironmentContext($tenant);
Livewire::actingAs($user)
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
->assertActionVisible('generate_management_report_pdf')
->assertActionDisabled('generate_management_report_pdf');
spec379ConfigurePdfRenderer(runtimeValidated: true);
Livewire::actingAs($user)
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
->assertActionVisible('generate_management_report_pdf')
->assertActionEnabled('generate_management_report_pdf')
->assertActionExists('generate_management_report_pdf', fn (Action $action): bool => $action->isConfirmationRequired())
->mountAction('generate_management_report_pdf')
->assertActionMounted('generate_management_report_pdf');
});
it('Spec379 exposes an unavailable Filament action when disclosure blocks customer PDF generation', function (): void {
spec379ConfigurePdfRenderer();
[$user, $tenant, , $pack] = spec379CurrentReadyPack();
$pack->forceFill([
'options' => [
'include_pii' => true,
'include_operations' => true,
],
])->save();
setAdminEnvironmentContext($tenant);
Livewire::actingAs($user)
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
->assertActionVisible('generate_management_report_pdf')
->assertActionDisabled('generate_management_report_pdf')
->assertActionExists('generate_management_report_pdf', fn (Action $action): bool => $action->getLabel() === __('localization.review.management_report_pdf_blocked'));
});
it('Spec379 regenerates a review-bound pack from the Review Pack detail action', function (): void {
Queue::fake();
[$user, $tenant, $review, $pack] = spec379CurrentReadyPack();
$tenantWidePackCount = ReviewPack::query()
->where('managed_environment_id', (int) $tenant->getKey())
->whereNull('environment_review_id')
->count();
setAdminEnvironmentContext($tenant);
Livewire::actingAs($user)
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
->callAction('regenerate', data: [
'include_pii' => true,
'include_operations' => true,
])
->assertNotified();
$regeneratedPack = ReviewPack::query()
->where('environment_review_id', (int) $review->getKey())
->whereKeyNot((int) $pack->getKey())
->latest('id')
->firstOrFail();
expect($regeneratedPack->options)->toMatchArray([
'include_pii' => true,
'include_operations' => true,
])
->and($regeneratedPack->managed_environment_id)->toBe((int) $tenant->getKey())
->and($regeneratedPack->evidence_snapshot_id)->toBe((int) $review->evidence_snapshot_id)
->and(ReviewPack::query()
->where('managed_environment_id', (int) $tenant->getKey())
->whereNull('environment_review_id')
->count())->toBe($tenantWidePackCount);
Queue::assertPushed(GenerateReviewPackJob::class);
});
it('Spec379 swaps the generate action for the download action once a PDF exists', function (): void {
[$user, $tenant, , $pack] = spec379CurrentReadyPack();
spec379ReadyManagementPdf($pack);
setAdminEnvironmentContext($tenant);
Livewire::actingAs($user)
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
->assertActionVisible('download_management_report_pdf')
->assertActionDoesNotExist('generate_management_report_pdf');
});