Files
Investidor-V2/tests/Feature/OpportunityRealDocumentOperationsTest.php
T
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

133 lines
8.2 KiB
PHP

<?php
namespace Tests\Feature;
use App\Jobs\ScanDocumentForMalware;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\Malware\MalwareScanner;
use App\Services\OpportunityManagement;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use Tests\TestCase;
class OpportunityRealDocumentOperationsTest extends TestCase
{
use RefreshDatabase;
private function setupDraft(): Opportunity
{
Storage::fake('local');
Queue::fake(); // Inspect quarantine first; execute the real job explicitly below.
config(['services.malware.scanner' => '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)->assertStatus(423);
$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)->assertConflict();
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();
}
}