133 lines
8.2 KiB
PHP
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)->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();
|
|
}
|
|
}
|