'investor', 'fullName' => 'Pessoa sintética QA', 'email' => 'qa-entry-source@example.invalid', 'country' => 'PT', 'declared_origin_country_code' => 'ZA', 'sectorInterest' => 'Ensaio da origem do formulário', 'message' => 'Descrição sintética QA para testar a rastreabilidade do formulário.', 'privacyConsent' => true, 'submission_key' => $key]; $this->post('http://localhost/apoio', $payload, ['Referer' => 'http://localhost/formularios/investidor?context=qa'])->assertRedirect(); $row = InterestRequest::where('email', $payload['email'])->firstOrFail(); $this->assertSame('/formularios/investidor', $row->source_page); $this->assertSame('ZA', $row->metadata['declared_origin_country_code']); $this->post('http://localhost/apoio', $payload, ['Referer' => 'http://localhost/apoio'])->assertRedirect(); $this->assertDatabaseCount('interest_requests', 1); $this->assertSame('/formularios/investidor', $row->fresh()->source_page); foreach (['https://foreign.example/formularios/empresa', 'http://localhost/management/interests', 'http://localhost:9999/formularios/investidor'] as $referer) { $this->post('http://localhost/apoio', [...$payload, 'submission_key' => (string) Str::uuid()], ['Referer' => $referer])->assertRedirect(); $this->assertSame('/apoio', InterestRequest::latest('id')->first()->source_page); } } protected function setUp(): void { parent::setUp(); $this->app->bind(MalwareScanner::class, fn () => new class implements MalwareScanner { public function scan(string $path, int $fileSizeBytes, string $storageRoot): array { return ['result' => 'clean', 'reason_code' => null, 'engine' => 'ClamAV-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => new \DateTimeImmutable('2026-10-05T00:00:00Z'), 'duration_ms' => 1, 'detail' => null]; } public function health(): array { return ['available' => true, 'engine' => 'ClamAV-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => new \DateTimeImmutable('2026-10-05T00:00:00Z'), 'signature_age_hours' => 0.0, 'reason_code' => null]; } }); } public function test_public_sections_render_and_desk_requires_login(): void { foreach (['sectores', 'como-investir', 'legislacao', 'documentos', 'apoio', 'parcerias', 'simulador-fiscal', 'Simulador', 'formularios/investidor', 'formularios/empresa'] as $path) { $this->get('/'.$path)->assertOk()->assertInertia(fn (Assert $page) => $page->component($path === 'parcerias' ? 'partnerships/catalogue' : 'portal/services')); } $this->get('/balcao-virtual')->assertRedirect('/login'); $this->get('/management/portal-services')->assertRedirect('/login'); } public function test_document_management_writes_redirect_to_the_management_get_route(): void { $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager); // A stale Referer can point at the write-only resource endpoint. Both // validation failures and successful writes must finish at a real GET. $this->from('/management/portal-services/resources/4') ->post('/management/portal-services/resources/4', ['published' => 'invalid']) ->assertRedirect(route('services.manage')) ->assertSessionHasErrors('published'); $this->get(route('services.manage'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('portal/manage')); $this->from('/management/portal-services/resources/4') ->post('/management/portal-services/resources', []) ->assertRedirect(route('services.manage')) ->assertSessionHasErrors('document_id'); $this->get('/management/portal-services/resources/4')->assertStatus(405); } public function test_support_receipt_is_idempotent_private_and_requires_consent(): void { $owner = User::factory()->create(); $payload = ['kind' => 'company', 'fullName' => 'Pessoa de Teste', 'email' => 'test@example.com', 'country' => 'Moçambique', 'company' => 'Empresa de teste', 'sectorInterest' => 'Agricultura', 'message' => 'Pretendo orientação sobre este investimento.', 'privacyConsent' => true, 'submission_key' => (string) Str::uuid()]; $this->actingAs($owner)->post('/apoio', [...$payload, 'privacyConsent' => false])->assertSessionHasErrors('privacyConsent'); $this->post('/apoio', $payload)->assertSessionHasNoErrors()->assertSessionHas('success'); $this->post('/apoio', $payload)->assertSessionHasNoErrors(); $this->assertSame(1, InterestRequest::count()); $this->assertDatabaseCount('outbox_events', 1); $this->get('/balcao-virtual')->assertRedirect('/balcao-virtual/painel'); $this->get('/balcao-virtual/painel')->assertInertia(fn (Assert $page) => $page->has('legacyRequests', 1)); $this->actingAs(User::factory()->create())->get('/balcao-virtual/painel')->assertInertia(fn (Assert $page) => $page->has('legacyRequests', 0)); $this->post('/apoio', $payload)->assertStatus(409); } public function test_home_remains_available_when_document_catalogue_schema_is_incompatible(): void { DB::shouldReceive('table')->once()->with('portal_resources as r') ->andThrow(new QueryException('pgsql', 'select missing catalogue columns', [], new \PDOException('SQLSTATE[42703]'))); $this->get('/')->assertOk()->assertInertia(fn (Assert $page) => $page ->component('PortalInvestidor') ->has('essentialResources', 0)); } public function test_partnership_requires_moderation_and_hides_owner_identity(): void { $owner = User::factory()->create(); $this->actingAs($owner)->post('/parcerias', ['title' => 'Parceria de teste', 'sector' => 'Agricultura', 'province' => 'Gaza', 'offer' => 'Capacidade de processamento de teste.', 'seeking' => 'Parceiro de logística para teste.', 'publication_consent' => true])->assertSessionHasNoErrors(); $id = DB::table('portal_partnerships')->value('id'); $this->get('/parcerias')->assertInertia(fn (Assert $page) => $page->has('results.data', 0)); $this->post('/management/portal-services/partnerships/'.$id, ['published' => true])->assertForbidden(); $moderator = User::factory()->create(); config(['portal.managers' => [$moderator->email]]); $this->actingAs($moderator)->post('/management/portal-services/partnerships/'.$id, ['published' => true])->assertRedirect('/parcerias/'.$id); $this->get('/parcerias')->assertInertia(fn (Assert $page) => $page->has('results.data', 0)); $version = DB::table('portal_partnerships')->where('id', $id)->value('version'); $this->post('/parcerias/'.$id.'/estado', ['version' => $version, 'action' => 'publish', 'reason' => 'Divulgação autorizada e revista pelo moderador QA.'])->assertSessionHasNoErrors(); $this->get('/parcerias')->assertInertia(fn (Assert $page) => $page->has('results.data', 1)->missing('results.data.0.user_id')); $version = DB::table('portal_partnerships')->where('id', $id)->value('version'); $this->post('/parcerias/'.$id.'/estado', ['version' => $version, 'action' => 'suspend', 'reason' => 'Retirada de divulgação na revisão QA.'])->assertSessionHasNoErrors(); $this->get('/parcerias')->assertInertia(fn (Assert $page) => $page->has('results.data', 0)); } public function test_document_download_rechecks_classification_checksum_and_publication(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent('exemplo.txt', 'Documento sintético de teste.'), 'issuer' => 'Entidade sintética de QA', 'original_source' => 'https://example.gov.mz/source'])->assertSessionHasNoErrors(); $document = DB::table('documents')->first(); $this->assertSame('active', $document->status); $this->assertSame('clean', $document->malware_scan_status); $this->assertSame($document->sha256, $document->malware_scanned_sha256); $this->assertSame('internal', $document->data_classification); $this->assertSame($manager->id, $document->uploaded_by); $this->assertSame(strlen('Documento sintético de teste.'), (int) $document->size_bytes); $this->get('/management/documents/'.$document->id.'/download')->assertOk()->assertDownload('exemplo.txt'); auth()->logout(); $this->get('/management/documents/'.$document->id.'/download')->assertRedirect('/login'); $this->actingAs($manager); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)); $id = $this->registerDocumentResource($document->id, 'Guia de teste'); $this->assertDatabaseHas('portal_resources', ['id' => $id, 'published' => false, 'rights_status' => 'authorized']); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasErrors('published'); // A malware scan releases technical quarantine without changing privacy or editorial publication. DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasNoErrors(); $this->get('/documentos/'.$id.'/download')->assertOk()->assertDownload('exemplo.txt'); $this->get('/documentos/'.$id.'?back=%2Fdocumentos%3Ftopic%3DInvestimento')->assertInertia(fn (Assert $page) => $page ->component('portal/document')->where('resource.title', 'Guia de teste')->where('backUrl', '/documentos?topic=Investimento') ->has('resource.links', 3)->missing('resource.storage_key')->missing('resource.sha256')); $this->get('/')->assertInertia(fn (Assert $page) => $page->has('essentialResources', 1)); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 1)->where('resources.data.0.title', 'Guia de teste')->missing('resources.data.0.storage_key')); $this->get('/documentos?q=Guia%20de%20teste&document_type=guide')->assertInertia(fn (Assert $page) => $page ->component('portal/services')->has('resources.data', 1)->where('resources.data.0.title', 'Guia de teste')); $this->getJson('/api/v1/documents/recommendations?module=simulator&activity_id=CAE-TEST&stage=review')->assertOk()->assertJsonPath('data.0.title', 'Guia de teste'); $this->put('/management/portal-services/resources/'.$id, [...$this->documentResourcePayload($document->id, 'Guia de teste revisto'), 'metadata_version' => 2, 'description' => 'Resumo editado pelo gestor.']) ->assertRedirect(route('services.manage'))->assertSessionHasNoErrors(); $this->assertDatabaseHas('portal_resources', ['id' => $id, 'published' => true, 'title' => 'Guia de teste revisto']); $this->assertDatabaseHas('portal_resource_revisions', ['resource_id' => $id, 'revision_no' => 3, 'change_type' => 'metadata_updated']); DB::table('documents')->where('id', $document->id)->update(['data_classification' => 'internal']); $this->get('/documentos/'.$id.'/download')->assertNotFound(); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)); $this->get('/')->assertInertia(fn (Assert $page) => $page->has('essentialResources', 0)); DB::table('documents')->where('id', $document->id)->update(['data_classification' => 'public']); Storage::disk('local')->put($document->storage_key, 'Conteúdo alterado'); $this->get('/documentos/'.$id.'/download')->assertNotFound(); } public function test_document_search_combines_exact_activity_and_territory_filters_without_duplicates(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager); $provinceA = (string) Str::uuid(); $provinceB = (string) Str::uuid(); foreach ([[$provinceA, 'Província QA A'], [$provinceB, 'Província QA B']] as [$id, $name]) { DB::table('geo_locations')->insert(['id' => $id, 'name' => $name, 'location_type' => 'province', 'code' => strtoupper(Str::random(4)), 'data_classification' => 'public', 'version_no' => 1, 'source_system' => 'qa', 'created_at' => now(), 'updated_at' => now()]); } $districtA = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $districtA, 'name' => 'Distrito QA A1', 'location_type' => 'district', 'parent_id' => $provinceA, 'code' => strtoupper(Str::random(4)), 'data_classification' => 'public', 'version_no' => 1, 'source_system' => 'qa', 'created_at' => now(), 'updated_at' => now()]); $records = []; foreach ([['A', $provinceA, 'provincial'], ['District', $districtA, 'district'], ['B', $provinceB, 'provincial'], ['National', null, 'national'], ['Unknown', null, 'unknown']] as [$suffix, $territory, $scope]) { $file = UploadedFile::fake()->createWithContent('doc-'.$suffix.'.txt', 'document content '.$suffix); $this->post('/management/portal-services/upload', ['file' => $file])->assertSessionHasNoErrors(); $document = DB::table('documents')->where('file_name', 'doc-'.$suffix.'.txt')->first(); $id = $this->registerDocumentResource($document->id, 'Document '.$suffix, [ 'territory_id' => $territory, 'territorial_scope' => $scope, 'associations' => [ ['target_type' => 'module', 'target_key' => 'simulator', 'relationship' => 'supports', 'applicability' => 'evidence', 'basis_reference' => 'Registo documental QA, secção 1.'], ['target_type' => 'module', 'target_key' => 'project_dossier', 'relationship' => 'supports', 'applicability' => 'evidence', 'basis_reference' => 'Registo documental QA, secção 2.'], ['target_type' => 'activity', 'target_key' => 'CAE-TEST', 'relationship' => 'applies_to', 'applicability' => 'evidence', 'basis_reference' => 'Registo documental QA, secção 3.'], ['target_type' => 'stage', 'target_key' => 'review', 'relationship' => 'supports', 'applicability' => 'evidence', 'basis_reference' => 'Registo documental QA, secção 4.'], ], ]); DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors(); $records[$suffix] = $id; } $query = '/documentos?'.http_build_query(['territory_id' => $provinceA, 'activity' => ['CAE-TEST'], 'module' => ['simulator'], 'stage' => ['review']]); $this->get($query)->assertOk()->assertInertia(fn (Assert $page) => $page->has('resources.data', 3) ->where('resources.data.0.id', $records['A'])->where('resources.data.1.id', $records['District'])->where('resources.data.2.id', $records['National'])); $this->get('/documentos?'.http_build_query(['territory_id' => $provinceA, 'activity' => ['NOT-FOUND']])) ->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)->where('resources.total', 0)); $this->assertDatabaseCount('portal_resource_links', 20); } public function test_legislation_with_an_identified_source_can_be_published_for_consultation_without_claiming_confirmation(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent('proposal.txt', 'Proposal document content.')])->assertSessionHasNoErrors(); $document = DB::table('documents')->first(); $payload = [...$this->documentResourcePayload($document->id, 'Proposta legislativa QA'), 'document_type' => 'legislation', 'legislation' => true, 'source_confirmation' => 'identified', 'legal_status' => 'proposed']; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $id = (int) DB::table('portal_resources')->value('id'); DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors(); $this->get('/legislacao')->assertInertia(fn (Assert $page) => $page->where('resources.data.0.legal_status', 'proposed') ->where('resources.data.0.source_confirmation', 'identified')); } public function test_recorded_user_authorization_publishes_only_eligible_document_scope_in_one_manager_action(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->post('/management/portal-services/disclosure-authorization/apply')->assertRedirect('/login'); $this->actingAs($manager); $resourceIds = []; $titles = [ 1 => 'Lei n.º 22/2022, de 28 de Dezembro — alteração ao Código do IVA (republicação)', 2 => 'Boletim da República n.º 248/2025 — Suplemento n.º 1', 3 => 'Boletim da República n.º 89, I Série (08-06-2017) — aviso de rectificação da Lei n.º 13/2016', 4 => 'Codificador da Divisão Político-Administrativa (INE, 07-06-2012)', ]; foreach ([1, 2, 3, 4] as $number) { $this->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent("reference-$number.pdf", "document bytes $number"), 'original_source' => 'https://example.gov.mz/reference', ])->assertSessionHasNoErrors(); $document = DB::table('documents')->where('file_name', "reference-$number.pdf")->firstOrFail(); $resourceIds[] = $this->registerDocumentResource($document->id, $titles[$number], [ 'document_type' => $number === 3 ? 'legislation' : 'reference_data', 'legislation' => $number === 3, 'source_confirmation' => $number === 3 ? 'identified' : 'official_confirmed', 'legal_status' => $number === 3 ? 'unknown' : 'historical', 'rights_status' => 'unconfirmed', 'data_classification' => 'internal', ]); DB::table('documents')->where('id', $document->id)->update([ 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $document->sha256, ]); } $this->assertCount(4, $resourceIds); DB::table('audit_events')->insert([ 'source_system' => 'user-instruction', 'external_id' => 'document-repository-disclosure-authorization-2026-10-06-v1', 'data_classification' => 'internal', 'entity_type' => 'portal_document_repository', 'entity_id' => null, 'action' => 'public_disclosure_authorized', 'actor_user_id' => null, 'after_data' => json_encode(['resource_ids' => $resourceIds], JSON_THROW_ON_ERROR), ]); $this->post('/management/portal-services/disclosure-authorization/apply') ->assertRedirect(route('services.manage'))->assertSessionHasNoErrors(); $this->assertDatabaseCount('portal_resource_revisions', 8); $this->assertDatabaseCount('audit_events', 13); $this->assertSame(4, DB::table('portal_resources')->where('published', true)->count()); $this->assertSame(4, DB::table('documents')->where('data_classification', 'public')->where('rights_status', 'authorized')->count()); $this->assertDatabaseHas('portal_resources', ['id' => $resourceIds[2], 'published' => true, 'source_confirmation' => 'identified', 'legal_status' => 'unknown']); $this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[0], 'target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'review_status' => 'confirmed', 'applicability' => 'evidence']); $this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[1], 'target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_10_2025', 'review_status' => 'pending', 'applicability' => 'evidence']); $this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[2], 'target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_13_2016_rectification_2017', 'review_status' => 'pending', 'applicability' => 'evidence']); $this->assertDatabaseHas('audit_events', ['source_system' => 'portal-services', 'entity_type' => 'portal_resources', 'actor_user_id' => $manager->id, 'action' => 'published_under_user_authorization']); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 4)); $this->get('/documentos?q=22%2F2022')->assertInertia(fn (Assert $page) => $page ->has('resources.data', 1)->where('resources.data.0.id', $resourceIds[0])); $this->get('/documentos?category=Guia&document_type=legislation')->assertInertia(fn (Assert $page) => $page ->has('resources.data', 1)->where('resources.data.0.id', $resourceIds[2])); $this->get('/documentos/'.$resourceIds[2])->assertInertia(fn (Assert $page) => $page->component('portal/document') ->where('resource.source_confirmation', 'identified')->where('resource.legal_status', 'unknown')); $this->get('/documentos/'.$resourceIds[0].'/download')->assertOk()->assertDownload('reference-1.pdf'); $this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[0], 'target_type' => 'module', 'target_key' => 'simulator']); $this->post('/management/portal-services/disclosure-authorization/apply')->assertSessionHasNoErrors(); $this->assertDatabaseCount('portal_resource_revisions', 8); } public function test_qa_library_preserves_an_original_without_inventing_a_topic_or_auto_publishing(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email], 'portal.legacy_content_qa_enabled' => false]); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('original-image-reference.txt', 'Synthetic original without a thematic classification.'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $payload = [...$this->documentResourcePayload($document->id, 'Original sem assunto'), 'topic' => '', 'category' => 'media', 'associations' => [], 'data_classification' => 'public']; $this->post('/management/portal-services/resources', $payload)->assertSessionHasErrors('topic'); $this->assertDatabaseCount('portal_resources', 0); config(['portal.legacy_content_qa_enabled' => true]); $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $resource = DB::table('portal_resources')->firstOrFail(); $this->assertSame('', $resource->topic); $this->assertFalse($resource->published); $this->assertDatabaseCount('portal_resource_links', 0); $this->get('/documentos/'.$resource->id.'/download')->assertNotFound(); $this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors(); $this->get('/documentos/'.$resource->id.'/download')->assertOk(); } public function test_manager_classifies_a_reviewed_document_public_before_separate_publication(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('public-guide.txt', 'A guide whose source and rights are reviewed.'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $payload = [...$this->documentResourcePayload($document->id, 'Guia público revisto'), 'rights_status' => 'authorized', 'source_confirmation' => 'identified', 'data_classification' => 'public']; $this->post('/management/portal-services/resources', [...$payload, 'rights_status' => 'unconfirmed'])->assertUnprocessable(); $this->assertDatabaseCount('portal_resources', 0); $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $resource = DB::table('portal_resources')->firstOrFail(); $this->assertDatabaseHas('documents', ['id' => $document->id, 'data_classification' => 'public', 'rights_status' => 'authorized']); $this->assertDatabaseHas('portal_resources', ['id' => $resource->id, 'published' => false, 'approved_by' => null]); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)); $this->get('/documentos/'.$resource->id.'/download')->assertNotFound(); $this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors(); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 1)->where('resources.data.0.title', 'Guia público revisto')); $this->get('/documentos/'.$resource->id.'/download')->assertOk()->assertDownload('public-guide.txt'); } public function test_public_pdf_preview_is_available_only_after_publication_and_hash_verification(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->create('consultation.pdf', 10, 'application/pdf'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $payload = [...$this->documentResourcePayload($document->id, 'Ficha PDF'), 'rights_status' => 'authorized', 'source_confirmation' => 'identified', 'data_classification' => 'public']; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $id = (int) DB::table('portal_resources')->value('id'); $this->get('/documentos/'.$id.'/view')->assertNotFound(); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors(); $this->get('/documentos/'.$id.'/view')->assertOk()->assertHeader('Content-Type', 'application/pdf')->assertHeader('Content-Disposition', 'inline; filename=consultation.pdf'); $this->get('/documentos/'.$id)->assertInertia(fn (Assert $page) => $page->where('resource.view_url', route('services.document.view', ['id' => $id]))); Storage::disk('local')->put($document->storage_key, 'replaced bytes'); $this->get('/documentos/'.$id.'/view')->assertNotFound(); } public function test_threat_and_unavailable_results_stay_quarantined_with_hash_history(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->bindMalwareResult('infected', 'threat_detected'); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('infected-fixture.txt', 'synthetic infected scanner result fixture'), ])->assertSessionHasNoErrors(); $infected = DB::table('documents')->where('file_name', 'infected-fixture.txt')->firstOrFail(); $this->assertSame('quarantined', $infected->status); $this->assertSame('infected', $infected->malware_scan_status); $this->assertSame($infected->sha256, $infected->malware_scanned_sha256); $this->assertDatabaseHas('document_malware_scans', ['document_id' => $infected->id, 'file_sha256' => $infected->sha256, 'result' => 'infected', 'reason_code' => 'threat_detected']); $this->bindMalwareResult('inconclusive', 'scanner_unavailable'); $this->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('unscanned-fixture.txt', 'synthetic scanner unavailable fixture'), ])->assertSessionHasNoErrors(); $unavailable = DB::table('documents')->where('file_name', 'unscanned-fixture.txt')->firstOrFail(); $this->assertSame('quarantined', $unavailable->status); $this->assertSame('inconclusive', $unavailable->malware_scan_status); $this->assertSame('scanner_unavailable', $unavailable->malware_failure_reason); $this->assertSame(0, DB::table('portal_resources')->count()); } public function test_manager_can_prepare_private_editorial_draft_before_malware_scan_finishes(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->bindMalwareResult('inconclusive', 'scanner_unavailable'); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('pending-private.txt', 'Private pending document.'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $this->assertSame('quarantined', $document->status); $this->post('/management/portal-services/resources', $this->documentResourcePayload($document->id, 'Rascunho privado')) ->assertSessionHasNoErrors(); $resource = DB::table('portal_resources')->firstOrFail(); $this->assertFalse((bool) $resource->published); $this->assertSame('internal', DB::table('documents')->where('id', $document->id)->value('data_classification')); $this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasErrors('published'); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)); $this->get('/documentos/'.$resource->id)->assertNotFound(); $this->get('/documentos/'.$resource->id.'/download')->assertNotFound(); } public function test_identical_upload_is_rejected_and_required_applicability_needs_a_basis(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager); $contents = 'same document body'; $this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent('original.txt', $contents)])->assertSessionHasNoErrors(); $document = DB::table('documents')->first(); $this->from('/management/portal-services')->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent('duplicate.txt', $contents)]) ->assertSessionHasErrors('file'); $payload = $this->documentResourcePayload($document->id, 'Manual QA'); $payload['associations'] = [['target_type' => 'procedure', 'target_key' => 'licence-test', 'relationship' => 'applies_to', 'applicability' => 'required', 'condition_text' => '', 'basis_reference' => '']]; $this->from('/management/portal-services')->post('/management/portal-services/resources', $payload)->assertSessionHasErrors('associations.0.basis_reference'); $this->assertDatabaseCount('documents', 1); $payload['associations'] = []; $payload['issuer'] = 'Gazeta oficial QA'; $payload['document_number'] = 'BR 44'; $payload['source_reference'] = 'Referência documental sintética de QA.'; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $firstId = (int) DB::table('portal_resources')->value('id'); $this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent('new-version.txt', 'different version body')])->assertSessionHasNoErrors(); $secondDocument = DB::table('documents')->where('file_name', 'new-version.txt')->first(); $this->post('/management/portal-services/resources', [...$payload, 'document_id' => $secondDocument->id, 'title' => 'Manual QA v2'])->assertSessionHasNoErrors(); $this->assertDatabaseHas('portal_resources', ['title' => 'Manual QA v2', 'duplicate_of_resource_id' => $firstId]); $this->assertDatabaseCount('portal_resources', 2); $this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0)); } public function test_requirement_links_use_known_technical_keys_and_expose_only_confirmed_citations_as_evidence(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('evidence.txt', 'Documentary source for QA.'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $payload = $this->documentResourcePayload($document->id, 'Evidência fiscal QA'); $payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'confirmed', 'condition_text' => 'Evidência documental, sem activar cálculo.', 'basis_reference' => 'Lei 22/2022, art. 17(1), página 3.']; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $id = (int) DB::table('portal_resources')->where('title', 'Evidência fiscal QA')->value('id'); $this->assertDatabaseHas('portal_resource_links', ['resource_id' => $id, 'target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'review_status' => 'confirmed']); $payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'legal.tax.anything', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'confirmed', 'basis_reference' => 'Sem chave aprovada.']; $this->from('/management/portal-services')->post('/management/portal-services/resources', $payload) ->assertSessionHasErrors('associations.4.target_key'); $this->assertDatabaseCount('documents', 1); } public function test_public_recommendations_return_only_confirmed_requirement_citations(): void { Storage::fake('local'); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/portal-services/upload', [ 'file' => UploadedFile::fake()->createWithContent('public-evidence.txt', 'Documentary evidence fixture.'), ])->assertSessionHasNoErrors(); $document = DB::table('documents')->firstOrFail(); $payload = $this->documentResourcePayload($document->id, 'Fontes fiscais QA'); $payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'confirmed', 'basis_reference' => 'Citação fixture, artigo 17(1), página 3.']; $payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_10_2025', 'relationship' => 'cites', 'applicability' => 'evidence', 'review_status' => 'pending', 'basis_reference' => 'Citação ainda não conferida no fixture.']; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); $id = (int) DB::table('portal_resources')->where('title', 'Fontes fiscais QA')->value('id'); DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']); $this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors(); $this->getJson('/api/v1/documents/recommendations?module=simulator')->assertOk() ->assertJsonPath('data.0.documentary_evidence.0.key', 'fiscal.iva.lei_22_2022') ->assertJsonPath('data.0.documentary_evidence.0.status', 'citation_checked_application_pending') ->assertJsonMissing(['key' => 'fiscal.iva.lei_10_2025']); } private function registerDocumentResource(string $documentId, string $title, array $overrides = []): int { $payload = [...$this->documentResourcePayload($documentId, $title), ...$overrides]; $this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors(); return (int) DB::table('portal_resources')->where('document_id', $documentId)->where('title', $title)->value('id'); } private function documentResourcePayload(string $documentId, string $title): array { return ['document_id' => $documentId, 'title' => $title, 'description' => 'Resumo sintético de controlo de qualidade.', 'category' => 'Guia', 'topic' => 'Investimento', 'document_type' => 'guide', 'issuer' => 'Entidade sintética de QA', 'document_number' => null, 'document_date' => null, 'source_url' => 'https://example.gov.mz/source', 'language' => 'pt', 'version_label' => 'QA-1', 'document_group_id' => null, 'territorial_scope' => 'national', 'territory_id' => null, 'source_confirmation' => 'identified', 'legal_status' => 'not_applicable', 'rights_status' => 'authorized', 'source_reference' => 'Fonte sintética apenas para testes isolados.', 'legislation' => false, 'essential' => true, 'independent_review' => false, 'applicable_from' => '', 'applicable_until' => '', 'associations' => [ ['target_type' => 'module', 'target_key' => 'simulator', 'relationship' => 'supports', 'applicability' => 'evidence', 'condition_text' => '', 'basis_reference' => 'Teste sintético, secção 1.'], ['target_type' => 'activity', 'target_key' => 'CAE-TEST', 'relationship' => 'applies_to', 'applicability' => 'evidence', 'condition_text' => '', 'basis_reference' => 'Teste sintético, secção 2.'], ['target_type' => 'stage', 'target_key' => 'review', 'relationship' => 'supports', 'applicability' => 'evidence', 'condition_text' => '', 'basis_reference' => 'Teste sintético, secção 3.'], ]]; } public function test_amount_filters_do_not_mix_currencies_or_expose_drafts(): void { $sector = Sector::create(['name' => 'Sector de teste', 'slug' => 'sector-teste', 'is_active' => true]); foreach ([['USD', 'published'], ['MZN', 'published'], ['USD', 'draft']] as [$currency, $status]) { Opportunity::create(['sector_id' => $sector->id, 'reference' => (string) Str::uuid(), 'slug' => (string) Str::uuid(), 'title' => 'Teste de dimensão', 'status' => $status, 'published_at' => now(), 'estimated_investment_amount' => 500, 'estimated_investment_currency' => $currency]); } $this->getJson('/api/v1/opportunities/search?min_amount=100')->assertUnprocessable(); $this->getJson('/api/v1/opportunities/search?currency=USD&min_amount=100&max_amount=600')->assertOk()->assertJsonPath('total', 1); $this->getJson('/api/v1/opportunities/search?currency=USD&min_amount=600&max_amount=100')->assertUnprocessable(); } private function bindMalwareResult(string $result, ?string $reason): void { $this->app->instance(MalwareScanner::class, new class($result, $reason) implements MalwareScanner { public function __construct(private readonly string $result, private readonly ?string $reason) {} public function scan(string $path, int $fileSizeBytes, string $storageRoot): array { return ['result' => $this->result, 'reason_code' => $this->reason, 'engine' => 'ClamAV-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => new \DateTimeImmutable('2026-10-05T00:00:00Z'), 'duration_ms' => 1, 'detail' => null]; } public function health(): array { return ['available' => true, 'engine' => 'ClamAV-Test-Double', 'engine_version' => 'fixture', 'signatures_at' => new \DateTimeImmutable('2026-10-05T00:00:00Z'), 'signature_age_hours' => 0.0, 'reason_code' => null]; } }); } }