'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]); if (! app(MalwareScanner::class)->health()['available']) { $this->markTestSkipped('Real ClamD required; operational evidence remains pending.'); } $user = User::factory()->create(); config(['portal.managers' => [$user->email]]); $id = $this->actingAs($user)->postJson('/management/opportunities/drafts', ['creation_key' => (string) Str::uuid(), 'reason' => 'Operações documentais reais em QA isolada'])->assertCreated()->json('id'); return Opportunity::findOrFail($id); } private function input(Opportunity $o, array $payload): array { return ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'creation_key' => (string) Str::uuid(), 'payload' => $payload, 'reason' => 'Operação e recuperação documental em QA isolada']; } private function scan(string $id): void { (new ScanDocumentForMalware($id))->handle(app(MalwareScanner::class)); $d = DB::table('documents')->where('id', $id)->first(); $this->assertSame('clean', $d->malware_scan_status); $this->assertSame('ClamAV', $d->malware_engine); $this->assertNotSame('unknown', $d->malware_engine_version); $this->assertSame(trim($d->sha256), $d->malware_scanned_sha256); $this->assertNotNull($d->malware_signatures_at); $this->assertDatabaseHas('document_malware_scans', ['document_id' => $id, 'result' => 'clean', 'engine' => 'ClamAV']); } public function test_real_original_replacement_quarantine_retry_download_delete_restore_and_authorization(): void { $o = $this->setupDraft(); $bytes = "%PDF-1.7\nSynthetic QA only\n%%EOF"; $url = '/management/opportunities/'.$o->id.'/resources/documents'; $r = $this->post($url, [...$this->input($o, ['title' => 'Synthetic QA document']), 'file' => UploadedFile::fake()->createWithContent('qa.pdf', $bytes)], ['Accept' => 'application/json'])->assertCreated()->json(); $doc = DB::table('opportunity_documents')->where('id', $r['id'])->value('document_id'); $download = $url.'/'.$r['id'].'/download'; $this->get($download)->assertStatus(423); config(['services.malware.port' => 1]); (new ScanDocumentForMalware($doc))->handle(app(MalwareScanner::class)); $this->assertDatabaseHas('documents', ['id' => $doc, 'status' => 'quarantined', 'malware_scan_status' => 'inconclusive']); $this->get($download)->assertStatus(423); config(['services.malware.port' => 3310]); $this->scan($doc); $this->get($download)->assertOk()->assertContent($bytes); $old = DB::table('documents')->where('id', $doc)->first(); $this->assertNotSame($bytes, Storage::disk('local')->get($old->storage_key)); $newBytes = "%PDF-1.7\nSecond synthetic QA edition\n%%EOF"; $replace = [...$this->input($o, ['title' => 'Synthetic second edition', 'edition' => '2']), 'version' => 1, 'file' => UploadedFile::fake()->createWithContent('qa-v2.pdf', $newBytes)]; $this->post($url.'/'.$r['id'], $replace, ['Accept' => 'application/json'])->assertOk(); $this->post($url.'/'.$r['id'], $replace, ['Accept' => 'application/json'])->assertConflict(); $next = DB::table('opportunity_documents')->where('id', $r['id'])->value('document_id'); $this->assertNotSame($doc, $next); $this->assertSame($bytes, Crypt::decryptString(Storage::disk('local')->get($old->storage_key))); $this->get($download)->assertStatus(423); $this->scan($next); $this->get($download)->assertContent($newBytes); $life = ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 2, 'action' => 'delete', 'confirm' => true, 'reason' => 'Exclusão lógica do recurso apenas em QA']; $this->postJson($url.'/'.$r['id'].'/lifecycle', $life)->assertOk(); $this->get($download)->assertNotFound(); $this->postJson($url.'/'.$r['id'].'/lifecycle', [...$life, 'etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 3, 'action' => 'restore'])->assertOk(); $this->get($download)->assertContent($newBytes); $this->assertDatabaseHas('documents', ['id' => $doc, 'deleted_at' => null]); $this->actingAs(User::factory()->create())->get($download)->assertForbidden(); } public function test_three_derivatives_have_independent_real_scans_signed_previews_and_integrity_checks(): void { $o = $this->setupDraft(); $this->assertTrue(function_exists('imagewebp'), 'GD/WebP required; rejection does not demonstrate derivatives.'); $image = imagecreatetruecolor(1600, 1600); ob_start(); imagepng($image); $bytes = ob_get_clean(); imagedestroy($image); $base = '/management/opportunities/'.$o->id.'/resources/images'; $r = $this->post($base, [...$this->input($o, ['title' => 'Synthetic QA image', 'alt_text' => 'Synthetic square for QA only', 'caption' => 'Synthetic image no active data', 'rights_status' => 'owned']), 'file' => UploadedFile::fake()->createWithContent('qa.png', $bytes)], ['Accept' => 'application/json'])->assertCreated()->json(); $doc = DB::table('opportunity_images')->where('id', $r['id'])->value('document_id'); $this->postJson($base.'/'.$r['id'].'/prepare', ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 1, 'reason' => 'Preparar derivadas sintéticas em QA'])->assertStatus(423); $this->scan($doc); $input = ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 1, 'reason' => 'Preparar derivadas sintéticas em QA']; $this->postJson($base.'/'.$r['id'].'/prepare', $input)->assertOk(); $this->postJson($base.'/'.$r['id'].'/prepare', $input)->assertConflict(); $p = json_decode(DB::table('opportunity_images')->where('id', $r['id'])->value('payload'), true); $this->assertCount(3, $p['derivatives']); $this->assertSame([320, 640, 1280], array_column($p['derivatives'], 'width')); foreach ($p['derivatives'] as $derived) { $id = $derived['document_id']; $url = URL::temporarySignedRoute('management.opportunity-image-preview', now()->addMinutes(10), ['id' => $o->id, 'resourceId' => $r['id'], 'documentId' => $id]); $this->get($url)->assertStatus(423); $this->scan($id); $d = DB::table('documents')->where('id', $id)->first(); $webp = Crypt::decryptString(Storage::disk('local')->get($d->storage_key)); $this->assertSame('image/webp', getimagesizefromstring($webp)['mime']); $this->assertStringNotContainsString('EXIF', $webp); $this->get($url)->assertOk()->assertHeader('Content-Type', 'image/webp')->assertContent($webp); $this->get(explode('?', $url)[0])->assertForbidden(); Storage::disk('local')->put($d->storage_key, Crypt::encryptString($webp.'tampered QA')); $this->get($url)->assertNotFound(); Storage::disk('local')->put($d->storage_key, Crypt::encryptString($webp)); } $this->assertSame(4, DB::table('document_malware_scans')->where('result', 'clean')->count()); $this->get('/management/opportunities/'.$o->id.'/resources')->assertOk(); $this->actingAs(User::factory()->create())->get($url)->assertForbidden(); } }