Files
Investidor-V2/tests/Feature/OpportunityResourcesTest.php
T
nala 60784a2ed2
tests / ci (push) Canceled after 0s
oportunidades 2
2026-10-09 08:48:42 +02:00

100 lines
7.7 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\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, 'name' => 'QA province', 'location_type' => 'province', 'parent_id'=>null, 'data_classification' => 'public'], ['id' => $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();
}
}