withoutVite(); Http::fake(); Mail::fake(); Queue::fake(); Storage::fake('local'); config(['portal.profile_access_enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $this->actor = User::factory()->create(); $this->support = User::factory()->create(); $approver = User::factory()->create(); $organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $organization, 'name' => 'SYNTHETIC upload QA organization', 'organization_type' => 'private']); DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'user_id' => $this->actor->id, 'organization_id' => $organization, 'role' => 'officer']); foreach ([[$this->actor, 'promoter'], [$this->support, 'technical_support']] as [$user,$role]) { app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $approver, ['user_id' => $user->id, 'role' => $role, 'organization_id' => $organization, 'opportunity_types' => ['investment_project'], 'source' => 'synthetic-resource-incident-fixture']); } $id = (string) Str::uuid(); $this->opportunity = new Opportunity(['reference' => $id, 'slug' => $id, 'title' => 'PRIVATE SYNTHETIC upload investment', 'status' => 'draft']); $this->opportunity->forceFill(['owner_user_id' => $this->actor->id, 'organization_id' => $organization, 'opportunity_category' => 'investment_project', 'management_version' => 4, 'workflow_context' => ['working_state' => 'draft']])->save(); $this->actingAs($this->actor); } private function upload(): array { $input = ['etag' => app(OpportunityManagement::class)->etag($this->opportunity->fresh()), 'creation_key' => (string) Str::uuid(), 'reason' => 'PRIVATE SYNTHETIC scan upload reason', 'payload' => ['title' => 'PRIVATE SYNTHETIC scan original', 'classification' => 'confidential'], 'file' => UploadedFile::fake()->createWithContent('PRIVATE-synthetic.pdf', "%PDF-1.7\nSYNTHETIC scan bytes\n%%EOF")]; $id = $this->post('/minhas-oportunidades/'.$this->opportunity->id.'/resources/documents', $input, ['Accept' => 'application/json'])->assertCreated()->json('id'); $resource = DB::table('opportunity_documents')->where('id', $id)->first(); return [$resource, DB::table('documents')->where('id', $resource->document_id)->first()]; } private function queued(): ScanDocumentForMalware { $captured = null; Queue::assertPushed(ScanDocumentForMalware::class, function ($job) use (&$captured) { $captured = $job; return true; }); $this->assertInstanceOf(ScanDocumentForMalware::class, $captured); return $captured; } private function scannerResult(string $state = 'clean'): array { return ['result' => $state, 'reason_code' => match ($state) { 'clean' => null, 'infected' => 'threat_detected', default => 'scanner_unavailable' }, 'engine' => 'QA-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => null, 'duration_ms' => 1, 'detail' => 'PRIVATE SYNTHETIC engine detail']; } private function scanner(\Closure $callback): MalwareScanner { return new class($callback) implements MalwareScanner { public function __construct(private \Closure $callback) {} public function scan(string $path, int $fileSizeBytes, string $storageRoot): array { return ($this->callback)($path, $fileSizeBytes, $storageRoot); } public function health(): array { return ['available' => true, 'engine' => 'QA-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => null, 'signature_age_hours' => null, 'reason_code' => null]; } }; } private function roots(): int { return DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->count(); } private function freshJob(ScanDocumentForMalware $job): ScanDocumentForMalware { return unserialize(serialize($job), ['allowed_classes' => [ScanDocumentForMalware::class]]); } private function delivery(int $attempt, string $uuid): QueueJob { $job = Mockery::mock(QueueJob::class); $job->shouldReceive('attempts')->andReturn($attempt); $job->shouldReceive('uuid')->andReturn($uuid); return $job; } public function test_dispatch_failure_keeps_the_saved_resource_and_notifies_without_impersonating_the_initiator(): void { Bus::shouldReceive('dispatch')->once()->with(Mockery::type(ScanDocumentForMalware::class))->andThrow(new \RuntimeException('PRIVATE queue credentials and body')); [$resource,$document] = $this->upload(); $this->assertSame(1, DB::table('documents')->count()); $this->assertSame(5, $this->opportunity->fresh()->management_version); $this->assertSame('failed', $document->malware_scan_status); $this->assertSame('quarantined', $document->status); $this->assertStringContainsString('SYNTHETIC scan bytes', Crypt::decryptString(Storage::disk('local')->get($document->storage_key))); $event = DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->first(); $data = json_decode($event->after_data, true)['incident']; $this->assertNull($event->actor_user_id); $this->assertSame($this->actor->id, $data['reported_for_user_id']); $this->assertSame('scan_dispatch_unconfirmed', $data['error_code']); $this->assertSame('scan_document', $data['operation']); $this->assertSame(5, $data['observed_version']); $this->assertStringNotContainsString('PRIVATE', $event->after_data); $this->assertSame('scan_document', app(OpportunityTechnicalIncidents::class)->recent($this->actor, $this->opportunity->fresh())[0]['operation']); $this->actingAs($this->support)->get('/suporte-tecnico/incidentes?unread=1')->assertOk()->assertInertia(fn (Assert $page) => $page->where('incidents.total', 1)->where('incidents.data.0.error_code', 'scan_dispatch_unconfirmed')); $this->get('/minhas-oportunidades/'.$this->opportunity->id.'/resources')->assertForbidden(); } public function test_an_uncertain_dispatch_after_a_sync_result_never_quarantines_an_already_clean_document(): void { Bus::shouldReceive('dispatch')->once()->with(Mockery::type(ScanDocumentForMalware::class))->andReturnUsing(function ($job) { $job->handle(new CleanScannerFixture); throw new \RuntimeException('PRIVATE dispatch acknowledgement lost'); }); [$resource,$document] = $this->upload(); $this->assertSame('clean', $document->malware_scan_status); $this->assertSame('active', $document->status); $this->assertSame(trim($document->sha256), $document->malware_scanned_sha256); $this->assertSame(1, DB::table('document_malware_scans')->count()); $this->assertSame(1, $this->roots()); $this->assertSame(5, $this->opportunity->fresh()->management_version); } public function test_dispatch_is_deferred_until_commit_and_a_rollback_never_enqueues_the_resource(): void { DB::beginTransaction(); $this->upload(); Queue::assertNothingPushed(); DB::rollBack(); $this->assertSame(0, DB::table('documents')->count()); $this->assertSame(0, $this->roots()); DB::beginTransaction(); $this->upload(); Queue::assertNothingPushed(); DB::commit(); $job = $this->queued(); $this->assertSame($this->actor->id, $job->opportunityContext['reported_for_user_id']); $this->assertSame(5, $job->opportunityContext['observed_version']); $this->assertSame(0, $this->roots()); } public function test_inconclusive_scan_is_quarantined_has_one_system_incident_and_replay_does_not_rescan(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $before = Storage::disk('local')->get($document->storage_key); $business = $this->opportunity->fresh()->getRawOriginal(); $calls = 0; $scanner = $this->scanner(function () use (&$calls) { $calls++; return $this->scannerResult('inconclusive'); }); $job->handle($scanner); $this->freshJob($job)->handle($scanner); $job->failed(new \RuntimeException('PRIVATE late failure')); $current = DB::table('documents')->where('id', $document->id)->first(); $this->assertSame(1, $calls); $this->assertSame('inconclusive', $current->malware_scan_status); $this->assertSame('quarantined', $current->status); $this->assertSame(1, $current->malware_scan_attempts); $this->assertSame(1, DB::table('document_malware_scans')->count()); $this->assertSame(1, $this->roots()); $this->assertSame($before, Storage::disk('local')->get($document->storage_key)); $this->assertSame([$document->storage_key], Storage::disk('local')->allFiles()); $this->assertSame($business, $this->opportunity->fresh()->getRawOriginal()); $root = DB::table('audit_events')->where('id', $job->runIdentifier())->first(); $data = json_decode($root->after_data, true)['incident']; $this->assertNull($root->actor_user_id); $this->assertSame('scan_inconclusive', $data['error_code']); $this->assertStringNotContainsString('PRIVATE', $root->after_data); app(OpportunityTechnicalIncidents::class)->recordScan($job->opportunityContext, $job->runIdentifier(), 'scan_worker_failed'); $this->assertEquals($root, DB::table('audit_events')->where('id', $job->runIdentifier())->first()); $this->get('/minhas-oportunidades/'.$this->opportunity->id.'/resources/documents/'.$resource->id.'/download')->assertStatus(423); } public function test_a_late_inconclusive_result_cannot_replace_a_newer_clean_scan(): void { [$resource,$document] = $this->upload(); $old = $this->queued(); $new = new ScanDocumentForMalware($document->id, $old->opportunityContext); $old->handle($this->scanner(function () use ($new) { $new->handle(new CleanScannerFixture); return $this->scannerResult('inconclusive'); })); $current = DB::table('documents')->where('id', $document->id)->first(); $this->assertSame('clean', $current->malware_scan_status); $this->assertSame('active', $current->status); $this->assertSame(2, $current->malware_scan_attempts); $this->assertSame(trim($current->sha256), $current->malware_scanned_sha256); $this->assertSame(1, DB::table('document_malware_scans')->count()); $this->assertSame(0, $this->roots()); $this->assertSame(1, DB::table('audit_events')->where('action', 'scan_result_discarded')->where('correlation_id', $old->runIdentifier())->count()); $this->freshJob($old)->failed(new \RuntimeException('PRIVATE old failure')); $this->assertEquals($current, DB::table('documents')->where('id', $document->id)->first()); } public function test_same_queue_execution_different_deliveries_cannot_apply_an_old_failure_to_the_current_attempt(): void { [$resource,$document] = $this->upload(); $old = $this->queued(); $fresh = $this->freshJob($old); $retry = $this->freshJob($old); $uuid = (string) Str::uuid(); $old->setJob($this->delivery(1, $uuid)); $fresh->setJob($this->delivery(1, $uuid)); $retry->setJob($this->delivery(2, $uuid)); $old->handle($this->scanner(function () use ($retry, $fresh, $document) { try { $retry->handle($this->scanner(fn () => throw new \RuntimeException('PRIVATE current worker failed'))); } catch (\RuntimeException $error) { $this->assertSame('PRIVATE current worker failed', $error->getMessage()); } $fresh->failed(new \RuntimeException('PRIVATE previous delivery failure')); $this->assertSame('scanning', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(0, $this->roots()); $retry->failed(new \RuntimeException('PRIVATE current terminal failure')); return $this->scannerResult('clean'); })); $this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(2, DB::table('documents')->where('id', $document->id)->value('malware_scan_attempts')); $this->assertSame(1, DB::table('document_malware_scans')->count()); $this->assertSame(1, $this->roots()); } public function test_infected_result_and_its_original_are_preserved_against_late_callbacks_and_queued_jobs(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $before = Storage::disk('local')->get($document->storage_key); $job->handle($this->scanner(fn () => $this->scannerResult('infected'))); $current = DB::table('documents')->where('id', $document->id)->first(); $this->freshJob($job)->failed(new \RuntimeException('PRIVATE stale failure')); (new ScanDocumentForMalware($document->id, $job->opportunityContext))->handle($this->scanner(fn () => throw new \RuntimeException('Scanner must not be called for infected bytes'))); $this->assertEquals($current, DB::table('documents')->where('id', $document->id)->first()); $this->assertSame('infected', $current->malware_scan_status); $this->assertSame('quarantined', $current->status); $this->assertSame($before, Storage::disk('local')->get($document->storage_key)); $this->assertSame(0, $this->roots()); $this->assertSame(1, DB::table('document_malware_scans')->count()); } public function test_document_record_changes_during_scan_are_inconclusive_and_history_keeps_the_attempted_hash(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $before = Storage::disk('local')->get($document->storage_key); $job->handle($this->scanner(function () use ($document) { DB::table('documents')->where('id', $document->id)->update(['sha256' => str_repeat('b', 64)]); return $this->scannerResult('clean'); })); $current = DB::table('documents')->where('id', $document->id)->first(); $this->assertSame('inconclusive', $current->malware_scan_status); $this->assertSame('document_record_changed_during_scan', $current->malware_failure_reason); $this->assertNull($current->malware_scanned_sha256); $this->assertSame(trim($document->sha256), DB::table('document_malware_scans')->value('file_sha256')); $this->assertSame($before, Storage::disk('local')->get($document->storage_key)); $this->assertSame('scan_inconclusive', json_decode(DB::table('audit_events')->where('id', $job->runIdentifier())->value('after_data'), true)['incident']['error_code']); } public function test_fresh_serialized_failure_callback_owns_only_its_attempt_and_an_explicit_retry_remains_possible(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $fresh = $this->freshJob($job); try { $job->handle($this->scanner(fn () => throw new \RuntimeException('PRIVATE engine failure'))); } catch (\RuntimeException $error) { $this->assertSame('PRIVATE engine failure', $error->getMessage()); } $fresh->failed(new \RuntimeException('PRIVATE terminal failure')); $fresh->failed(new \RuntimeException('PRIVATE duplicate callback')); $this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(1, $this->roots()); $this->assertSame(1, DB::table('document_malware_scans')->count()); $fresh->handle(new CleanScannerFixture); $this->assertSame('clean', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(2, DB::table('document_malware_scans')->count()); $this->assertSame(1, $this->roots()); } public function test_legacy_serialized_payload_uses_the_real_queue_uuid_in_a_fresh_failure_callback(): void { [$resource,$document] = $this->upload(); $class = ScanDocumentForMalware::class; $payload = 'O:'.strlen($class).':"'.$class.'":1:{s:10:"documentId";s:36:"'.$document->id.'";}'; $job = unserialize($payload, ['allowed_classes' => [$class]]); $fresh = unserialize($payload, ['allowed_classes' => [$class]]); $uuid = (string) Str::uuid(); $this->assertInstanceOf($class, $job); $this->assertNull($job->executionId); $job->setJob($this->delivery(1, $uuid)); $fresh->setJob($this->delivery(1, $uuid)); try { $job->handle($this->scanner(fn () => throw new \RuntimeException('SYNTHETIC legacy worker failure'))); } catch (\RuntimeException) { } $fresh->failed(new \RuntimeException('SYNTHETIC legacy terminal callback')); $this->assertSame($uuid, $fresh->runIdentifier()); $this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(0, $this->roots()); } public function test_captured_organization_is_not_replaced_when_the_opportunity_moves_before_the_worker_runs(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $captured = $job->opportunityContext; $other = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $other, 'name' => 'SYNTHETIC other scan organization', 'organization_type' => 'private']); $this->opportunity->forceFill(['organization_id' => $other])->save(); $job->handle($this->scanner(fn () => $this->scannerResult('inconclusive'))); $data = json_decode(DB::table('audit_events')->where('id', $job->runIdentifier())->value('after_data'), true)['incident']; $this->assertSame($captured['organization_id'], $data['organization_id']); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $this->actor, ['user_id' => $this->support->id, 'role' => 'technical_support', 'organization_id' => $other, 'opportunity_types' => ['investment_project']]); $this->actingAs($this->support)->get('/suporte-tecnico/incidentes')->assertOk()->assertInertia(fn (Assert $page) => $page->where('incidents.total', 0)->where('profileAccess.technicalSupportUnread', 0)); $this->assertSame(1, $this->roots()); } public function test_incident_persistence_failure_does_not_release_the_document_or_fail_a_completed_inconclusive_scan(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); DB::statement("ALTER TABLE audit_events ADD CONSTRAINT qa_reject_scan_incident CHECK (source_system <> 'portal-opportunity-incident')"); $job->handle($this->scanner(fn () => $this->scannerResult('inconclusive'))); $this->assertSame('quarantined', DB::table('documents')->where('id', $document->id)->value('status')); $this->assertSame('inconclusive', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(1, DB::table('document_malware_scans')->count()); $this->assertSame(0, $this->roots()); } public function test_generic_document_jobs_do_not_invent_an_opportunity_recipient(): void { [$resource,$document] = $this->upload(); $job = new ScanDocumentForMalware($document->id); $job->handle($this->scanner(fn () => $this->scannerResult('inconclusive'))); $this->assertSame(0, $this->roots()); $this->assertSame('inconclusive', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); } public function test_a_fresh_terminal_callback_preserves_the_attempted_byte_descriptor_after_a_record_change(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); $fresh = $this->freshJob($job); try { $job->handle($this->scanner(fn () => throw new \RuntimeException('SYNTHETIC worker interrupted'))); } catch (\RuntimeException) { } DB::table('documents')->where('id', $document->id)->update(['sha256' => str_repeat('b', 64), 'size_bytes' => 999]); $fresh->failed(new \RuntimeException('SYNTHETIC terminal callback after record change')); $result = DB::table('document_malware_scans')->first(); $this->assertSame(trim($document->sha256), $result->file_sha256); $this->assertSame($document->size_bytes, $result->file_size_bytes); $this->assertSame('inconclusive', $result->result); $this->assertSame('document_record_changed_during_scan', $result->reason_code); $this->assertSame('quarantined', DB::table('documents')->where('id', $document->id)->value('status')); $this->assertSame('unknown', $result->engine); } public function test_failed_decryption_cleans_its_temporary_file_and_retains_the_original_and_attempt_context(): void { [$resource,$document] = $this->upload(); $job = $this->queued(); Storage::disk('local')->put($document->storage_key, 'PRIVATE SYNTHETIC invalid envelope'); try { $job->handle(new CleanScannerFixture); $this->fail('Invalid envelope must not be scanned as clean.'); } catch (DecryptException) { } $this->freshJob($job)->failed(new \RuntimeException('PRIVATE terminal decryption failure')); $this->assertSame([$document->storage_key], Storage::disk('local')->allFiles()); $this->assertSame('PRIVATE SYNTHETIC invalid envelope', Storage::disk('local')->get($document->storage_key)); $this->assertSame('failed', DB::table('documents')->where('id', $document->id)->value('malware_scan_status')); $this->assertSame(1, $this->roots()); } }