Files
Investidor-V2/tools/portal-onboarding-governance-test.php
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

151 lines
9.6 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityCanonicalContract;
use App\Services\PortalProfileAccess;
use App\Services\PortalProfileAdministration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class PortalProfilePreparationTest extends TestCase
{
use RefreshDatabase;
private const REASON = 'Preparação sintética independente em QA; não é uma atribuição no destino.';
protected function setUp(): void
{
parent::setUp();
$path = (new \ReflectionClass(PortalProfileAccess::class))->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);
}
}