withoutVite(); Http::fake(); Mail::fake(); Storage::fake('local'); config(['portal.profile_access_enabled' => true, 'portal.opportunity_specialized_drafts_enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); } /** @param list $responsibilities */ private function actor(string $role, string $org, array $responsibilities = []): User { $actor = User::factory()->create(); $actor->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), ['user_id' => $actor->id, 'role' => $role, 'organization_id' => $role === 'promoter' ? $org : null, 'opportunity_types' => ['real_estate'], 'responsibilities' => $responsibilities, 'source' => 'synthetic-incident-fixture']); return $actor; } private function loginFixture(User $actor): static { return $this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]); } /** @param array $extra */ private function act(User $actor, int $id, string $action, array $extra = []): TestResponse { $record = Opportunity::findOrFail($id); return $this->loginFixture($actor)->postJson('/minhas-oportunidades/'.$id.'/workflow', ['etag' => app(OpportunityManagement::class)->etag($record), 'action' => $action, 'reason' => self::REASON, ...$extra]); } private function failAudit(string $action, bool $incidentToo = false): void { $source = $incidentToo ? "NEW.source_system = 'portal-opportunity-incident' OR " : ''; DB::unprepared("CREATE FUNCTION qa_incident_failure() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN IF ({$source}NEW.action = '{$action}') THEN RAISE EXCEPTION 'PRIVATE SYNTHETIC SQL TOKEN MUST NEVER REACH THE RESPONSE'; END IF; RETURN NEW; END \$\$; CREATE TRIGGER qa_incident_failure BEFORE INSERT ON audit_events FOR EACH ROW EXECUTE FUNCTION qa_incident_failure();"); } public function test_failed_submission_rolls_back_and_records_only_minimal_private_metadata_then_allows_manual_recovery(): void { [$id,$promoter,$validator] = $this->fixture(); $before = Opportunity::findOrFail($id)->getRawOriginal(); $taskCount = DB::table('tasks')->count(); $this->failAudit('workflow_submit'); $response = $this->act($promoter, $id, 'submit')->assertStatus(503)->assertHeader('Cache-Control', 'no-store, private')->assertJsonPath('incident_recorded', true); $reference = $response->json('reference'); $this->assertTrue(Str::isUuid($reference)); $this->assertStringNotContainsString('SQL TOKEN', $response->getContent()); $this->assertSame($before, Opportunity::findOrFail($id)->getRawOriginal()); $this->assertSame($taskCount, DB::table('tasks')->count()); $event = DB::table('audit_events')->where('id', $reference)->first(); $data = json_decode($event->after_data, true)['incident']; $this->assertSame('restricted', $event->data_classification); $this->assertSame($reference, $event->correlation_id); $this->assertSame($id, $data['opportunity_id']); $this->assertSame('submit', $data['operation']); $this->assertSame('database_failure', $data['error_code']); $this->assertArrayNotHasKey('reason', $data); $this->assertArrayNotHasKey('exception', $data); $this->assertArrayNotHasKey('payload', $data); $this->get('/minhas-oportunidades/'.$id.'/view')->assertOk()->assertInertia(fn (Assert $page) => $page->has('workflowIncidents', 1)->where('workflowIncidents.0.reference', $reference)->missing('workflowIncidents.0.attempted_etag')); $this->loginFixture($validator)->get('/minhas-oportunidades/'.$id.'/view')->assertOk()->assertInertia(fn (Assert $page) => $page->has('workflowIncidents', 0)); DB::unprepared('DROP TRIGGER qa_incident_failure ON audit_events; DROP FUNCTION qa_incident_failure();'); $this->act($promoter, $id, 'submit')->assertOk()->assertJsonPath('status', 'submitted'); $this->assertSame(1, DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->count()); $this->assertSame($taskCount + 1, DB::table('tasks')->count()); } public function test_publication_failure_preserves_approved_content_public_projection_tasks_and_original_decisions(): void { [$id,$promoter,$validator,$publisher,$payload,$document] = $this->fixture(); $this->act($promoter, $id, 'submit')->assertOk(); $this->act($validator, $id, 'start_review')->assertOk(); foreach (['property_review', 'registry_urban_review', 'building_compliance_review'] as $gate) { $this->act($validator, $id, 'review_gate', ['gate' => $gate, 'gate_decision' => 'approved', 'evidence_document_ids' => [$document]])->assertOk(); } $this->act($validator, $id, 'approve')->assertOk(); $record = Opportunity::findOrFail($id); $before = $record->getRawOriginal(); $tasks = DB::table('tasks')->orderBy('id')->get()->toJson(); $this->failAudit('workflow_publish'); $this->act($publisher, $id, 'publish', ['confirm_publication' => true, 'preview_hash' => $record->workflow_context['approval']['hash']])->assertStatus(503)->assertJsonPath('incident_recorded', true); $this->assertSame($before, Opportunity::findOrFail($id)->getRawOriginal()); $this->assertSame($tasks, DB::table('tasks')->orderBy('id')->get()->toJson()); $this->assertSame('publish', app(OpportunityTechnicalIncidents::class)->recent($publisher, $record)[0]['operation']); $this->assertSame([], app(OpportunityTechnicalIncidents::class)->recent($promoter, $record)); } public function test_validation_conflicts_and_permission_failures_are_not_technical_incidents(): void { [$id,$promoter,$validator] = $this->fixture(); $this->act($promoter, $id, 'submit', ['reason' => 'short'])->assertUnprocessable(); $this->act($promoter, $id, 'submit', ['etag' => str_repeat('0', 64)])->assertConflict(); $this->act($validator, $id, 'submit')->assertForbidden(); $this->assertSame(0, DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->count()); } public function test_a_second_storage_failure_returns_an_honest_reference_without_leaking_sql_or_claiming_a_saved_incident(): void { [$id,$promoter] = $this->fixture(); $before = Opportunity::findOrFail($id)->getRawOriginal(); $this->failAudit('workflow_submit', true); Log::shouldReceive('warning')->once()->with('Opportunity technical incident could not be stored.', \Mockery::on(fn ($context) => Str::isUuid($context['reference']) && $context['error_code'] === 'database_failure')); $response = $this->act($promoter, $id, 'submit')->assertStatus(503)->assertJsonPath('incident_recorded', false); $this->assertStringNotContainsString('SQL TOKEN', $response->getContent()); $this->assertTrue(Str::isUuid($response->json('reference'))); $this->assertSame($before, Opportunity::findOrFail($id)->getRawOriginal()); $this->assertSame(0, DB::table('audit_events')->where('source_system', 'portal-opportunity-incident')->count()); } public function test_incident_consultation_rechecks_current_authority_and_never_grants_other_record_or_document_access(): void { [$id,$promoter] = $this->fixture(); $this->failAudit('workflow_submit'); $this->act($promoter, $id, 'submit')->assertStatus(503); $draft = $this->loginFixture($promoter)->postJson('/minhas-oportunidades/drafts', ['creation_key' => (string) Str::uuid(), 'organization_id' => Opportunity::findOrFail($id)->organization_id, 'opportunity_category' => 'real_estate', 'reason' => self::REASON])->assertCreated()->json('id'); $this->assertSame([], app(OpportunityTechnicalIncidents::class)->recent($promoter, Opportunity::findOrFail($draft))); DB::table('portal_role_assignments')->where('user_id', $promoter->id)->update(['revoked_at' => now()]); $this->get('/minhas-oportunidades/'.$id.'/view')->assertForbidden(); } public function test_support_profile_receives_only_current_scope_metadata_without_operational_or_document_access(): void { [$id,$promoter] = $this->fixture(); $record = Opportunity::findOrFail($id); $this->failAudit('workflow_submit'); $this->act($promoter, $id, 'submit')->assertStatus(503); $support = $this->actor('technical_support', $record->organization_id); DB::table('portal_role_assignments')->where('user_id', $support->id)->update(['organization_id' => $record->organization_id]); $this->loginFixture($support)->get('/suporte-tecnico/incidentes')->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $page) => $page->component('profiles/technical-support')->has('incidents.data', 1)->missing('incidents.data.0.attempted_etag')->missing('incidents.data.0.opportunity_id')); $this->get('/minhas-oportunidades/'.$id.'/view')->assertForbidden(); $this->act($support, $id, 'publish')->assertForbidden(); $otherOrganization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $otherOrganization, 'name' => 'SYNTHETIC other organization', 'organization_type' => 'private']); DB::table('portal_role_assignments')->where('user_id', $support->id)->update(['organization_id' => $otherOrganization]); $this->get('/suporte-tecnico/incidentes')->assertOk()->assertInertia(fn (Assert $page) => $page->has('incidents.data', 0)); DB::table('portal_role_assignments')->where('user_id', $support->id)->update(['organization_id' => null]); DB::table('opportunities')->where('id', $id)->update(['organization_id' => $otherOrganization]); $this->get('/suporte-tecnico/incidentes')->assertOk()->assertInertia(fn (Assert $page) => $page->has('incidents.data', 0)); } public function test_support_scope_revocation_expiry_and_auditor_restriction_are_effective(): void { [$id,$promoter] = $this->fixture(); $record = Opportunity::findOrFail($id); $this->failAudit('workflow_submit'); $this->act($promoter, $id, 'submit')->assertStatus(503); $support = $this->actor('technical_support', 'unused'); $this->loginFixture($support)->get('/suporte-tecnico/incidentes')->assertOk(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), ['user_id' => $support->id, 'role' => 'auditor', 'opportunity_types' => ['real_estate']]); $this->get('/suporte-tecnico/incidentes')->assertForbidden(); DB::table('portal_role_assignments')->where('user_id', $support->id)->where('role', 'auditor')->update(['revoked_at' => now()]); $this->get('/suporte-tecnico/incidentes')->assertOk(); $this->travel(2)->hours(); DB::table('portal_role_assignments')->where('user_id', $support->id)->where('role', 'technical_support')->update(['expires_at' => now()->subHour()]); $this->get('/suporte-tecnico/incidentes')->assertForbidden(); } public function test_support_grant_uses_existing_independent_administration_and_is_audited(): void { $admin = User::factory()->create(); $second = User::factory()->create(); DB::table('portal_superuser_pair')->insert(['id' => 1, 'first_user_id' => $admin->id, 'second_user_id' => $second->id, 'approved_by' => $second->id, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); $admin->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save(); $subject = User::factory()->create(); $this->loginFixture($admin); $input = ['user_id' => $subject->id, 'role' => 'technical_support', 'opportunity_types' => ['real_estate'], 'reason' => self::REASON]; $this->postJson('/perfis/atribuicoes', $input)->assertCreated(); $this->assertTrue(app(PortalProfileAccess::class)->has($subject, 'technical_support')); $this->assertSame(1, DB::table('audit_events')->where('action', 'role_granted')->count()); $this->postJson('/perfis/atribuicoes', [...$input, 'user_id' => $admin->id])->assertForbidden(); $this->postJson('/perfis/atribuicoes', [...$input, 'responsibilities' => ['technical_reviewer']])->assertUnprocessable(); $this->assertFalse(app(PortalProfileAccess::class)->canCreate($subject, null, 'real_estate')); $this->assertFalse(app(ScopedPortalAdministration::class)->administrative($subject)); } /** @return array{int,User,User,User,array,string} */ private function fixture(): array { $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA organization', 'organization_type' => 'private']); $promoter = $this->actor('promoter', $org); DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'user_id' => $promoter->id, 'organization_id' => $org, 'role' => 'officer']); $validator = $this->actor('validator', $org, ['technical_reviewer', 'legal_reviewer', 'validation_coordinator']); $publisher = $this->actor('publisher', $org, ['publication_coordinator']); $version = (string) Str::uuid(); DB::table('reference_catalog_versions')->insert(['id' => $version, 'catalog_code' => 'qa-only-'.$org, 'version_no' => 1, 'source_system' => 'synthetic-policy-fixture', 'source_version' => 'qa', 'source_reference' => 'Not official', 'status' => 'published']); $ids = []; $parent = null; foreach (range(1, 3) as $i) { $sector = DB::table('sectors')->insertGetId(['name' => 'QA '.$i, 'slug' => 'qa-'.$org.'-'.$i, 'is_active' => true]); $id = (string) Str::uuid(); DB::table('economic_activities')->insert(['id' => $id, 'parent_id' => $parent, 'sector_id' => $sector, 'catalog_version_id' => $version, 'code' => 'qa-'.$i, 'label_pt_mz' => 'QA '.$i, 'status' => 'published', 'valid_from' => today()]); $ids[] = [$id, $sector]; $parent = $id; } $contact = (string) Str::uuid(); $investor = (string) Str::uuid(); DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'Synthetic QA contact']); DB::table('investors')->insert(['id' => $investor, 'organization_id' => $org, 'legal_name' => 'Synthetic QA entity', 'investor_type' => 'institutional', 'status' => 'active']); DB::table('investor_contacts')->insert(['investor_id' => $investor, 'contact_id' => $contact]); $draft = $this->loginFixture($promoter)->postJson('/minhas-oportunidades/drafts', ['creation_key' => (string) Str::uuid(), 'organization_id' => $org, 'opportunity_category' => 'real_estate', 'reason' => self::REASON])->assertCreated()->json(); $id = $draft['id']; $location = (string) Str::uuid(); $provinceId = (string) Str::uuid(); $districtId = (string) Str::uuid(); DB::table('geo_locations')->insert([['id' => $provinceId, 'code' => $provinceId, 'name' => 'Synthetic QA province', 'location_type' => 'province', 'parent_id' => null, 'data_classification' => 'internal'], ['id' => $districtId, 'code' => $districtId, 'name' => 'Synthetic QA district', 'location_type' => 'district', 'parent_id' => $provinceId, 'data_classification' => 'internal']]); $province = DB::table('geo_locations')->where('id', $provinceId)->first(); $district = DB::table('geo_locations')->where('id', $districtId)->first(); DB::table('opportunity_location_drafts')->insert(['id' => $location, 'opportunity_id' => $id, 'version' => 1, 'created_by' => $promoter->id, 'payload' => json_encode(['country_code' => 'MZ', 'province_id' => $province->id, 'district_id' => $district->id, 'role' => 'headquarters', 'is_primary' => true])]); $document = (string) Str::uuid(); $file = (string) Str::uuid(); $bytes = 'Synthetic policy fixture, not a real scanner result.'; $key = 'opportunity-contract-test/'.$file; Storage::disk('local')->put($key, Crypt::encryptString($bytes)); DB::table('documents')->insert(['id' => $file, 'file_name' => 'qa-fixture.txt', 'storage_key' => $key, 'storage_encryption' => 'laravel', 'media_type' => 'text/plain', 'sha256' => hash('sha256', $bytes), 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_engine' => 'synthetic-policy-fixture', 'malware_scanned_sha256' => hash('sha256', $bytes), 'uploaded_by' => $promoter->id]); DB::table('opportunity_documents')->insert(['id' => $document, 'opportunity_id' => $id, 'document_id' => $file, 'version' => 1, 'created_by' => $promoter->id, 'payload' => json_encode(['title' => 'QA', 'category' => 'qa', 'visibility' => 'internal'])]); $payload = ['opportunity_category' => 'real_estate', 'official_title' => 'Imóvel sintético para ensaio QA', 'promoter_id' => $org, 'promoter_type' => 'private', 'internal_contact_id' => $contact, 'sector_id' => $ids[0][1], 'subsector_id' => $ids[1][0], 'economic_activity_id' => $ids[2][0], 'location_mode' => 'single', 'geographic_scope' => 'district', 'locations' => [$location], 'executive_summary' => trim(str_repeat('Resumo sintético QA. ', 20)), 'demand_statement' => trim(str_repeat('Necessidade sintética QA. ', 6)), 'documents' => [$document], 'evidences' => [['title' => 'QA', 'evidence_type' => 'qa', 'document_id' => $document]], 'publication_languages' => [['language_code' => 'pt-MZ', 'title' => 'QA', 'summary' => 'QA']], 'visibility' => 'internal', 'currency' => 'MZN', 'truth_declaration' => true, 'real_estate' => ['transaction_type' => 'sale', 'property_type' => 'warehouse', 'construction_status' => 'completed', 'property_condition' => 'good', 'land_area' => 1000, 'built_area' => 500, 'area_unit' => 'sqm', 'utilities' => [['type' => 'water', 'status' => 'existing']], 'occupancy_status' => 'vacant', 'title_type' => 'QA', 'registration_number' => 'QA-'.$id, 'encumbrances' => [['type' => 'none']], 'valuation' => ['value' => 1000, 'date' => '2026-10-10', 'method' => 'qa', 'appraiser' => 'QA', 'document_id' => $document], 'price' => 1000, 'use_license_document_id' => $document]]; $authority = (string) Str::uuid(); DB::table('authorities')->insert(['id' => $authority, 'catalog_version_id' => $version, 'organization_id' => $org, 'code' => 'QA-'.$id, 'authority_type' => 'qa-synthetic', 'label_pt_mz' => 'Entidade sintética QA', 'source_reference' => 'Synthetic policy fixture; not official', 'valid_from' => '2026-01-01', 'status' => 'published']); $payload['real_estate']['utilities'] = array_map(fn ($type) => ['type' => $type, 'status' => 'existing'], ['electricity', 'water', 'sanitation', 'telecommunications', 'access']); $payload['real_estate'] += ['title_document_id' => $document, 'title_holder_name' => 'Synthetic QA organization', 'issuing_authority_id' => $authority]; $this->loginFixture($promoter)->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => $draft['etag'], 'canonical' => $payload, 'reason' => self::REASON])->assertOk(); return [$id, $promoter, $validator, $publisher, $payload, $document]; } }