getFileName(); $this->assertStringContainsString('onboarding-qa-', $path); $this->assertSame('55441', (string) DB::connection()->getConfig('port')); $this->assertStringStartsWith('onboarding_', DB::connection()->getDatabaseName()); config(['portal.profile_access_enabled' => false, 'portal.profile_administration_enabled' => true]); } private function holder(): User { $first = User::factory()->withTwoFactor()->create(); $second = User::factory()->withTwoFactor()->create(); DB::table('portal_superuser_pair')->insert(['id' => 1, 'first_user_id' => $first->id, 'second_user_id' => $second->id, 'approved_by' => $second->id]); return $first; } private function mfa(User $user): static { return $this->actingAs($user)->withSession(['portal.mfa_actor' => $user->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $user->fresh()->portal_access_revision]); } public function test_preparation_routes_need_explicit_flag_holder_and_recent_mfa(): void { $holder = $this->holder(); $ordinary = User::factory()->create(); config(['portal.profile_administration_enabled' => false]); $this->mfa($holder)->get('/perfis/gestao')->assertNotFound(); config(['portal.profile_administration_enabled' => true]); $this->mfa($holder)->get('/perfis/gestao')->assertOk()->assertInertia(fn (Assert $page) => $page ->component('profiles/administration')->where('profileAccess.administration', '/perfis/gestao') ->where('profileAccess.review', null)->where('profileAccess.create', null)); $this->withSession(['portal.mfa_at' => time() - 901])->get('/perfis/gestao')->assertForbidden(); $this->actingAs($ordinary)->get('/perfis/gestao')->assertForbidden(); $unverified = User::factory()->unverified()->create(); $this->actingAs($unverified)->get('/perfil/promotor')->assertRedirect('/email/verify'); } public function test_assignments_are_recoverable_and_audited_while_operations_stay_closed(): void { $holder = $this->holder(); $validator = User::factory()->withTwoFactor()->create(); $publisher = User::factory()->withTwoFactor()->create(); foreach (['validator' => $validator, 'publisher' => $publisher] as $role => $subject) { $id = $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $subject->id, 'role' => $role, 'organization_id' => null, 'opportunity_types' => OpportunityCanonicalContract::BASE_CATEGORIES, 'reason' => self::REASON])->assertCreated()->json('id'); $this->assertDatabaseHas('portal_role_assignments', ['id' => $id, 'user_id' => $subject->id, 'role' => $role, 'organization_id' => null]); $this->assertDatabaseHas('audit_events', ['actor_user_id' => $holder->id, 'action' => 'role_granted']); $this->mfa($subject)->get('/minhas-oportunidades')->assertNotFound(); $this->postJson('/minhas-oportunidades/drafts', ['creation_key' => (string) Str::uuid(), 'opportunity_category' => 'investment_project'])->assertNotFound(); $this->assertFalse(app(PortalProfileAccess::class)->operational($subject, 'publish')); } $this->mfa($holder)->get('/perfis/gestao')->assertInertia(fn (Assert $page) => $page->has('assignments', 2)); $this->assertDatabaseCount('opportunities', 0); } public function test_unverified_unenrolled_and_self_assignments_are_denied(): void { $holder = $this->holder(); foreach ([User::factory()->unverified()->withTwoFactor()->create(), User::factory()->create()] as $subject) { $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $subject->id, 'role' => 'validator', 'opportunity_types' => OpportunityCanonicalContract::BASE_CATEGORIES, 'reason' => self::REASON])->assertUnprocessable(); } $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $holder->id, 'role' => 'publisher', 'opportunity_types' => OpportunityCanonicalContract::BASE_CATEGORIES, 'reason' => self::REASON])->assertForbidden(); $this->assertDatabaseCount('portal_role_assignments', 0); } public function test_promoter_request_remains_documented_owned_and_idempotent_during_preparation(): void { $holder = $this->holder(); $promoter = User::factory()->withTwoFactor()->create(); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA organization', 'organization_type' => 'private']); DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'user_id' => $promoter->id, 'organization_id' => $org, 'role' => 'officer']); $document = (string) Str::uuid(); // Authorization fixture only: not evidence of an actual scanner or legitimate representation. DB::table('documents')->insert(['id' => $document, 'file_name' => 'synthetic-authorization.txt', 'storage_key' => 'qa/'.$document, 'media_type' => 'text/plain', 'uploaded_by' => $promoter->id, 'status' => 'active', 'malware_scan_status' => 'clean', 'sha256' => str_repeat('b', 64), 'malware_scanned_sha256' => str_repeat('b', 64)]); $payload = ['creation_key' => (string) Str::uuid(), 'representation_type' => 'company', 'organization_id' => $org, 'opportunity_types' => OpportunityCanonicalContract::BASE_CATEGORIES, 'document_ids' => [$document], 'reason' => self::REASON]; $id = $this->mfa($promoter)->postJson('/perfil/promotor', $payload)->assertCreated()->json('id'); $this->postJson('/perfil/promotor', $payload)->assertCreated()->assertJsonPath('id', $id); $this->getJson('/perfil/promotor/'.$id)->assertOk()->assertJsonPath('status', 'pending'); $decision = ['version' => 1, 'decision' => 'approved', 'reason' => self::REASON, 'approved_types' => OpportunityCanonicalContract::BASE_CATEGORIES, 'verified_identity' => true, 'verified_representation' => true, 'verified_documents' => true]; $this->postJson('/perfis/pedidos/'.$id.'/decisao', $decision)->assertForbidden(); $this->mfa($holder)->postJson('/perfis/pedidos/'.$id.'/decisao', $decision)->assertOk(); $this->assertDatabaseCount('portal_profile_requests', 1); $this->assertDatabaseHas('portal_role_assignments', ['user_id' => $promoter->id, 'role' => 'promoter', 'organization_id' => $org]); $this->assertFalse(app(PortalProfileAccess::class)->canCreate($promoter, $org, 'investment_project')); $this->mfa($promoter)->get('/minhas-oportunidades/create')->assertNotFound(); $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $promoter->id, 'role' => 'promoter', 'opportunity_types' => ['investment_project'], 'reason' => self::REASON])->assertUnprocessable(); } public function test_revocation_and_mfa_protection_apply_before_operational_opening(): void { $holder = $this->holder(); $validator = User::factory()->withTwoFactor()->create(); $id = $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $validator->id, 'role' => 'validator', 'opportunity_types' => ['investment_project'], 'reason' => self::REASON])->assertCreated()->json('id'); $this->mfa($validator)->deleteJson('/user/two-factor-authentication')->assertForbidden(); $this->assertNotNull($validator->fresh()->two_factor_secret); $this->mfa($holder)->postJson('/perfis/atribuicoes/'.$id.'/revogar', ['reason' => self::REASON])->assertOk(); $this->assertFalse(app(PortalProfileAccess::class)->has($validator, 'validator')); $this->actingAs($validator)->withSession(['portal.access_revision' => 0])->getJson('/perfil/promotor')->assertUnauthorized(); } public function test_preparation_flag_does_not_unlock_private_attachments_or_legacy_bypass(): void { $holder = $this->holder(); $validator = User::factory()->withTwoFactor()->create(); $this->mfa($holder)->postJson('/perfis/atribuicoes', ['user_id' => $validator->id, 'role' => 'validator', 'opportunity_types' => ['investment_project'], 'reason' => self::REASON])->assertCreated(); $record = new Opportunity; $record->forceFill(['owner_user_id' => 123, 'opportunity_category' => 'investment_project', 'id' => 123, 'status' => 'draft']); $this->assertFalse(app(PortalProfileAccess::class)->canView($validator, $record)); $this->assertFalse(app(PortalProfileAccess::class)->canView($holder, $record)); $this->assertFalse(app(PortalProfileAccess::class)->legacyWriteAllowed($holder)); $this->assertFalse(app(PortalProfileAccess::class)->legacyWriteAllowed($validator)); $this->mfa($validator)->get('/minhas-oportunidades/123/resources')->assertNotFound(); $this->assertDatabaseCount('opportunities', 0); } }