133 lines
10 KiB
PHP
133 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Jobs\ScanDocumentForMalware;
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
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\Support\CleanScannerFixture;
|
|
use Tests\TestCase;
|
|
|
|
class OpportunityResourcesTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function setupDraft(): array
|
|
{
|
|
Storage::fake('local');
|
|
Queue::fake();
|
|
$user = User::factory()->create();
|
|
config(['portal.managers' => [$user->email]]);
|
|
$id = $this->actingAs($user)->postJson('/management/opportunities/drafts', ['creation_key' => (string) Str::uuid(), 'reason' => 'Rascunho de recursos em QA isolada'])->assertCreated()->json('id');
|
|
|
|
return [$user, Opportunity::findOrFail($id)];
|
|
}
|
|
|
|
private function payload(Opportunity $opportunity, array $payload = []): array
|
|
{
|
|
return ['etag' => app(OpportunityManagement::class)->etag($opportunity), 'creation_key' => (string) Str::uuid(), 'payload' => $payload, 'reason' => 'Preparação e conferência em QA isolada'];
|
|
}
|
|
|
|
public function test_encrypted_original_scan_pending_recovery_replacement_and_logical_lifecycle(): void
|
|
{
|
|
[$user,$o] = $this->setupDraft();
|
|
$bytes = "%PDF-1.7\nSynthetic QA document\n%%EOF";
|
|
$result = $this->post('/management/opportunities/'.$o->id.'/resources/documents', [...$this->payload($o, ['title' => 'QA source', 'classification' => 'confidential']), 'file' => UploadedFile::fake()->createWithContent('qa.pdf', $bytes)], ['Accept' => 'application/json'])->assertCreated()->json();
|
|
$r = DB::table('opportunity_documents')->where('id', $result['id'])->first();
|
|
$d = DB::table('documents')->where('id', $r->document_id)->first();
|
|
$this->assertSame('pending', $d->malware_scan_status);
|
|
$this->assertSame($bytes, Crypt::decryptString(Storage::disk('local')->get($d->storage_key)));
|
|
$this->assertNotSame($bytes, Storage::disk('local')->get($d->storage_key));
|
|
Queue::assertPushed(ScanDocumentForMalware::class);
|
|
$this->get('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id.'/download')->assertStatus(423);
|
|
$this->get('/management/opportunities/'.$o->id.'/resources')->assertOk();
|
|
// Only a labelled QA fixture scans synthetic bytes; never evidence for an active file.
|
|
(new ScanDocumentForMalware($d->id))->handle(new CleanScannerFixture);
|
|
$this->assertDatabaseHas('documents', ['id' => $d->id, 'malware_engine' => 'QA-Test-Double', 'malware_scan_status' => 'clean']);
|
|
$this->get('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id.'/download')->assertOk()->assertContent($bytes);
|
|
$replacement = [...$this->payload($o->fresh(), ['title' => 'QA changed', 'edition' => '2']), 'version' => 1, 'file' => UploadedFile::fake()->createWithContent('qa-v2.pdf', "%PDF-1.7\nQA second version\n%%EOF")];
|
|
$this->post('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id, $replacement, ['Accept' => 'application/json'])->assertOk();
|
|
$current = DB::table('opportunity_documents')->where('id', $r->id)->first();
|
|
$this->assertNotSame($d->id, $current->document_id);
|
|
$this->assertDatabaseHas('documents', ['id' => $d->id, 'deleted_at' => null]);
|
|
$this->assertTrue(Storage::disk('local')->exists($d->storage_key));
|
|
$this->post('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id, $replacement, ['Accept' => 'application/json'])->assertConflict();
|
|
$life = ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 2, 'action' => 'delete', 'confirm' => true, 'reason' => 'Exclusão lógica de anexo apenas QA'];
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id.'/lifecycle', $life)->assertOk();
|
|
$this->assertSoftDeleted('opportunity_documents', ['id' => $r->id]);
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/documents/'.$r->id.'/lifecycle', [...$life, 'etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 3, 'action' => 'restore'])->assertOk();
|
|
$this->assertDatabaseHas('opportunity_documents', ['id' => $r->id, 'deleted_at' => null, 'version' => 4]);
|
|
$this->assertSame(4, DB::table('audit_events')->where('source_system', 'opportunity-resources')->count());
|
|
$this->actingAs(User::factory()->create())->get('/management/opportunities/'.$o->id.'/resources')->assertForbidden();
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o->fresh(), ['title' => 'Denied']))->assertForbidden();
|
|
}
|
|
|
|
public function test_multiple_locations_without_coordinates_and_invalid_hierarchy_and_geometry(): void
|
|
{
|
|
[$user,$o] = $this->setupDraft();
|
|
$province = (string) Str::uuid();
|
|
$district = (string) Str::uuid();
|
|
DB::table('geo_locations')->insert([['id' => $province, 'code' => $province, 'name' => 'QA province', 'location_type' => 'province', 'parent_id' => null, 'data_classification' => 'public'], ['id' => $district, 'code' => $district, 'name' => 'QA district', 'location_type' => 'district', 'parent_id' => $province, 'data_classification' => 'public']]);
|
|
$p = ['title' => 'QA production', 'province_id' => $province, 'district_id' => $district, 'role' => 'production', 'is_primary' => true, 'allocation_percentage' => 60, 'territorial_purpose' => 'implementation'];
|
|
$id = $this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o, $p))->assertCreated()->json('id');
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o->fresh(), $p))->assertUnprocessable();
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o->fresh(), [...$p, 'role' => 'market', 'is_primary' => false, 'allocation_percentage' => 40]))->assertCreated();
|
|
$this->assertSame(2, DB::table('opportunity_location_drafts')->count());
|
|
$this->assertSame(0, DB::table('opportunity_locations')->count());
|
|
$this->get('/management/opportunities/'.$o->id.'/resources')->assertOk();
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o->fresh(), ['title' => 'Bad geo', 'geometry' => ['type' => 'Point', 'coordinates' => [300, 100]]]))->assertUnprocessable()->assertJsonValidationErrors('geometry');
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/locations', $this->payload($o->fresh(), ['title' => 'Gap', 'district_id' => $district]))->assertUnprocessable()->assertJsonValidationErrors('district_id');
|
|
$this->assertDatabaseHas('opportunity_location_drafts', ['id' => $id, 'deleted_at' => null]);
|
|
}
|
|
|
|
public function test_invalid_upload_and_cross_opportunity_links_do_not_create_resources(): void
|
|
{
|
|
[$user,$o] = $this->setupDraft();
|
|
$this->post('/management/opportunities/'.$o->id.'/resources/images', [...$this->payload($o, ['title' => 'Bad image']), 'file' => UploadedFile::fake()->createWithContent('bad.png', 'invalid')], ['Accept' => 'application/json'])->assertUnprocessable();
|
|
$this->postJson('/management/opportunities/'.$o->id.'/resources/images', $this->payload($o, ['title' => 'QA image', 'location_id' => (string) Str::uuid()]))->assertUnprocessable()->assertJsonValidationErrors('location_id');
|
|
$this->assertSame(0, DB::table('opportunity_images')->count());
|
|
$this->actingAs(User::factory()->create())->get('/management/opportunity-resources-install')->assertForbidden();
|
|
}
|
|
|
|
public function test_image_processing_is_explicit_and_every_derivative_requires_its_own_scan(): void
|
|
{
|
|
[$user,$o] = $this->setupDraft();
|
|
$chunk = fn ($name, $bytes) => pack('N', strlen($bytes)).$name.$bytes.pack('N', crc32($name.$bytes));
|
|
$png = "\x89PNG\r\n\x1a\n".$chunk('IHDR', pack('NNCCCCC', 500, 500, 8, 2, 0, 0, 0)).$chunk('IDAT', gzcompress(str_repeat("\0", 500 * 1501))).$chunk('IEND', '');
|
|
$result = $this->post('/management/opportunities/'.$o->id.'/resources/images', [...$this->payload($o, ['title' => 'Synthetic QA image', 'alt_text' => 'Synthetic white square for QA', 'caption' => 'Synthetic image not used in active data', 'rights_status' => 'owned', 'is_cover' => true]), 'file' => UploadedFile::fake()->createWithContent('qa.png', $png)], ['Accept' => 'application/json'])->assertCreated()->json();
|
|
$image = DB::table('opportunity_images')->where('id', $result['id'])->first();
|
|
(new ScanDocumentForMalware($image->document_id))->handle(new CleanScannerFixture);
|
|
$url = '/management/opportunities/'.$o->id.'/resources/images/'.$image->id.'/prepare';
|
|
$input = ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'version' => 1, 'reason' => 'Processamento de imagem sintética em QA'];
|
|
if (! function_exists('imagecreatefromstring')) {
|
|
$this->postJson($url, $input)->assertStatus(423);
|
|
$this->assertDatabaseHas('opportunity_images', ['id' => $image->id, 'version' => 1]);
|
|
|
|
return;
|
|
}
|
|
$this->postJson($url, $input)->assertOk();
|
|
$payload = json_decode(DB::table('opportunity_images')->where('id', $image->id)->value('payload'), true);
|
|
$this->assertCount(2, $payload['derivatives']);
|
|
foreach ($payload['derivatives'] as $derivative) {
|
|
$document = DB::table('documents')->where('id', $derivative['document_id'])->first();
|
|
$this->assertSame('pending', $document->malware_scan_status);
|
|
$webp = Crypt::decryptString(Storage::disk('local')->get($document->storage_key));
|
|
$this->assertStringNotContainsString('EXIF', $webp);
|
|
$this->assertSame('image/webp', getimagesizefromstring($webp)['mime']);
|
|
(new ScanDocumentForMalware($document->id))->handle(new CleanScannerFixture);
|
|
}
|
|
$preview = URL::temporarySignedRoute('management.opportunity-image-preview', now()->addMinutes(10), ['id' => $o->id, 'resourceId' => $image->id, 'documentId' => $payload['derivatives'][0]['document_id']]);
|
|
$this->get($preview)->assertOk()->assertHeader('Content-Type', 'image/webp');
|
|
$this->actingAs(User::factory()->create())->get($preview)->assertForbidden();
|
|
}
|
|
}
|