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

140 lines
10 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityManagement;
use App\Services\OrganizationCountry;
use App\Services\PortalInternalDesignation;
use App\Services\PortalProfileAccess;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class PortalInternalDesignationTest extends TestCase
{
use RefreshDatabase;
private const REASON='Designação sintética interna autorizada pelo responsável; só QA, sem dados ou contas ativas.';
protected function setUp(): void
{
// Guard before RefreshDatabase can write.
$this->assertSame('55441',getenv('DB_PORT'));
$this->assertSame('onboarding_internal_20261011_191450_test',getenv('DB_DATABASE'));
parent::setUp();
$this->assertStringContainsString('onboarding-qa-internal-',(new \ReflectionClass(PortalProfileAccess::class))->getFileName());
$this->assertSame(55441,(int)DB::selectOne('SELECT inet_server_port() AS port')->port);
config(['portal.profile_administration_enabled'=>true,'portal.profile_access_enabled'=>false,'portal.organization_country_enabled'=>true]);
}
private function holder(): User
{
$a=User::factory()->withTwoFactor()->create();$b=User::factory()->withTwoFactor()->create();
DB::table('portal_superuser_pair')->insert(['id'=>1,'first_user_id'=>$a->id,'second_user_id'=>$b->id,'approved_by'=>$b->id]);return $a;
}
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]);
}
private function organizationPayload(): array
{
return ['operation_key'=>(string)Str::uuid(),'name'=>'Tempo Lda','nuit'=>'400993114','contact'=>'+258868010880','director'=>'Osmane Nala','function'=>'portal_owner_operator','source_reference'=>self::REASON];
}
private function register(User $holder): string
{
return $this->mfa($holder)->postJson('/perfis/organizacao-operadora',$this->organizationPayload())->assertCreated()->json('id');
}
private function assignment(User $subject,string $operator,string $role='promoter'): array
{
return ['internal_designation'=>true,'operation_key'=>(string)Str::uuid(),'user_id'=>$subject->id,'role'=>$role,
'operator_organization_id'=>$operator,'organization_id'=>$role==='promoter'?$operator:null,
'opportunity_types'=>OpportunityCanonicalContract::BASE_CATEGORIES,'confirmed_internal_designation'=>true,'reason'=>self::REASON];
}
public function test_master_registration_replay_and_recovery_use_one_identity_without_memberships(): void
{
$h=$this->holder();$payload=$this->organizationPayload();
$id=$this->mfa($h)->postJson('/perfis/organizacao-operadora',$payload)->assertCreated()->json('id');
$this->postJson('/perfis/organizacao-operadora',$payload)->assertCreated()->assertJsonPath('id',$id);
$this->postJson('/perfis/organizacao-operadora',[...$payload,'contact'=>'+258868010881'])->assertConflict();
$this->assertDatabaseCount('organizations',1);$this->assertDatabaseCount('memberships',0);
$this->assertSame(1,DB::table('audit_events')->where('action','operator_organization_registered')->count());
$this->assertSame('+258868010880',app(PortalInternalDesignation::class)->organization($id)['contact']);
$this->get('/perfis/gestao')->assertOk();
}
public function test_existing_comma_alias_preserves_country_original_source_and_audited_previous_values(): void
{
$h=$this->holder();$id=(string)Str::uuid();
DB::table('organizations')->insert(['id'=>$id,'name'=>'Tempo, Lda','source_system'=>'existing-approved-master','organization_type'=>'private','incorporation_country_code'=>'MZ']);
$this->mfa($h)->postJson('/perfis/organizacao-operadora',$this->organizationPayload())->assertCreated()->assertJsonPath('id',$id);
$this->assertDatabaseCount('organizations',1);
$this->assertDatabaseHas('organizations',['id'=>$id,'name'=>'Tempo Lda','source_system'=>'existing-approved-master','incorporation_country_code'=>'MZ']);
$before=json_decode(DB::table('audit_events')->where('action','operator_organization_registered')->first()->after_data,true)['previous_master_values'];
$this->assertSame('Tempo, Lda',$before['name']);
$this->assertSame($id,app(PortalInternalDesignation::class)->organizations()[0]['id']);
}
public function test_three_internal_roles_are_independent_and_do_not_require_external_promoter_documents(): void
{
$h=$this->holder();$operator=$this->register($h);
foreach(['promoter','validator','publisher'] as $role){
$subject=User::factory()->withTwoFactor()->create();$payload=$this->assignment($subject,$operator,$role);
$id=$this->mfa($h)->postJson('/perfis/atribuicoes',$payload)->assertCreated()->json('id');
$this->postJson('/perfis/atribuicoes',$payload)->assertCreated()->assertJsonPath('id',$id);
$this->assertDatabaseHas('portal_role_assignments',['id'=>$id,'role'=>$role,'source'=>'internal-portal-designation','organization_id'=>$role==='promoter'?$operator:null]);
$this->mfa($subject)->get('/minhas-oportunidades')->assertNotFound();
}
$this->assertDatabaseCount('memberships',0);$this->assertDatabaseCount('documents',0);$this->assertDatabaseCount('portal_profile_requests',0);
$this->assertDatabaseCount('portal_role_assignments',3);
$this->assertSame(3,DB::table('audit_events')->where('action','role_granted')->count());
$this->assertDatabaseCount('opportunities',0);
}
public function test_internal_designation_still_enforces_security_and_idempotency(): void
{
$h=$this->holder();$operator=$this->register($h);$subject=User::factory()->withTwoFactor()->create();
$this->mfa($h)->postJson('/perfis/atribuicoes',$this->assignment($h,$operator))->assertForbidden();
$unverified=User::factory()->unverified()->withTwoFactor()->create();
$this->postJson('/perfis/atribuicoes',$this->assignment($unverified,$operator))->assertUnprocessable();
$unenrolled=User::factory()->create();$this->postJson('/perfis/atribuicoes',$this->assignment($unenrolled,$operator))->assertUnprocessable();
$p=$this->assignment($subject,$operator);$this->postJson('/perfis/atribuicoes',[...$p,'confirmed_internal_designation'=>false])->assertUnprocessable();
$id=$this->postJson('/perfis/atribuicoes',$p)->assertCreated()->json('id');
$this->postJson('/perfis/atribuicoes',[...$p,'role'=>'publisher'])->assertConflict();
$this->postJson('/perfis/atribuicoes',[...$p,'operation_key'=>(string)Str::uuid()])->assertConflict();
$this->postJson('/perfis/atribuicoes',['user_id'=>$subject->id,'role'=>'promoter','opportunity_types'=>['investment_project'],'reason'=>self::REASON])->assertUnprocessable();
$this->postJson('/perfis/atribuicoes/'.$id.'/revogar',['reason'=>self::REASON])->assertOk();
$this->assertFalse(app(PortalProfileAccess::class)->has($subject,'promoter'));
$this->postJson('/perfis/atribuicoes',$p)->assertConflict();
$this->actingAs($subject)->withSession(['portal.access_revision'=>0])->getJson('/perfil/promotor')->assertUnauthorized();
}
public function test_internal_promoter_six_drafts_keep_real_promoter_and_do_not_grant_company_management(): void
{
$h=$this->holder();$operator=$this->register($h);$subject=User::factory()->withTwoFactor()->create();
$this->mfa($h)->postJson('/perfis/atribuicoes',$this->assignment($subject,$operator))->assertCreated();
$real=(string)Str::uuid();DB::table('organizations')->insert(['id'=>$real,'name'=>'Synthetic actual promoter QA','organization_type'=>'private']);
config(['portal.profile_access_enabled'=>true,'portal.opportunity_dashboard_access_enabled'=>true]);
$this->mfa($subject);$access=app(PortalProfileAccess::class);
foreach(OpportunityCanonicalContract::BASE_CATEGORIES as $type){
$payload=['creation_key'=>(string)Str::uuid(),'opportunity_category'=>$type,'organization_id'=>$real,'title'=>'Synthetic '.$type,'reason'=>self::REASON];
$r=app(OpportunityManagement::class)->save($subject,$payload);
$this->assertSame($real,$r->organization_id);$this->assertSame($operator,$r->workflow_context['operator_organization_id']);
$this->assertTrue($access->canView($subject,$r));$this->assertTrue($access->canEdit($subject,$r));
$this->assertTrue($access->conflict($subject,$r));
$this->assertSame($r->id,app(OpportunityManagement::class)->save($subject,$payload)->id);
$this->assertSame($real,Opportunity::findOrFail($r->id)->organization_id);
}
$this->assertDatabaseCount('opportunities',6);$this->assertDatabaseCount('memberships',0);
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
app(OrganizationCountry::class)->authorize($subject,$operator,true);
}
public function test_accumulating_roles_cannot_turn_read_access_into_editing_an_unassigned_private_record(): void
{
$h=$this->holder();$operator=$this->register($h);$subject=User::factory()->withTwoFactor()->create();
$this->mfa($h)->postJson('/perfis/atribuicoes',$this->assignment($subject,$operator))->assertCreated();
$this->postJson('/perfis/atribuicoes',$this->assignment($subject,$operator,'validator'))->assertCreated();
config(['portal.profile_access_enabled'=>true]);
$record=new Opportunity;$record->forceFill(['id'=>123,'owner_user_id'=>999,'organization_id'=>$operator,'opportunity_category'=>'investment_project','status'=>'draft','workflow_context'=>['operator_organization_id'=>$operator,'contributors'=>[999]]]);
$access=app(PortalProfileAccess::class);$this->assertTrue($access->canView($subject,$record));$this->assertFalse($access->canEdit($subject,$record));
$only=User::factory()->withTwoFactor()->create();$this->mfa($h)->postJson('/perfis/atribuicoes',$this->assignment($only,$operator))->assertCreated();
$this->assertFalse($access->canView($only,$record));
}
}