Files
Investidor-V2/tests/Feature/InvestmentGuidesTest.php

187 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\InvestmentGuides;
use App\Services\InvestorContext;
use App\Services\VirtualDesk;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Tests\TestCase;
final class InvestmentGuidesTest extends TestCase
{
use RefreshDatabase;
private User $owner;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
config(['investor-path.enabled' => true, 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false, 'portal.notifications_enabled' => false]);
Http::preventStrayRequests();
$this->owner = User::factory()->create();
}
private function dossier(): string
{
return $this->actingAs($this->owner)->postJson('/investimentos', ['title' => 'SYNTHETIC capitalisation', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
}
private function url(string $id): string
{
return '/investimentos/'.$id.'/guias/capitalizacao';
}
/** @return array<string,mixed> */
private function input(string $id, array $extra = []): array
{
return [...['version' => DB::table('desk_cases')->where('id', $id)->value('version'), 'operation_key' => (string) Str::uuid(), 'sha256' => app(InvestmentGuides::class)->guide('capitalizacao')['sha256'], 'instruments' => ['private_equity', 'debt'], 'note' => 'SYNTHETIC private preparation'], ...$extra];
}
public function test_public_source_versioned_guide_requires_neither_login_nor_ia_and_does_not_capture_private_notes(): void
{
$this->getJson('/guias-investimento/capitalizacao?note=PRIVATE')->assertOk()->assertHeader('Cache-Control', 'no-store, private')
->assertJsonPath('note', '')->assertJsonPath('case', null)->assertJsonCount(4, 'guide.instruments')->assertJsonCount(5, 'guide.sources')
->assertJsonPath('guide.sources.bni.sha256', 'fbeaab84c4cad22ee63e5c1dfb61ea3206333885ddd80c8001d37a3f49d2820e')
->assertJsonPath('guide.sources.ifc_apply.locator', 'Eligibility; Investment Proposals, pontos 1–7');
$this->get('/guias-investimento/capitalizacao')->assertOk()->assertInertia(fn ($page) => $page->component('investment/guide'));
$this->assertDatabaseCount('desk_cases', 0);
$this->assertDatabaseCount('organizations', 0);
$this->getJson('/guias-investimento/invented')->assertNotFound();
}
public function test_instrument_choices_are_coded_and_source_pin_survives_normal_authentication_and_dossier_choice(): void
{
$guide = app(InvestmentGuides::class)->guide('capitalizacao');
$selection = ['slug' => 'capitalizacao', 'sha256' => $guide['sha256'], 'instruments' => ['private_equity', 'debt', 'debt']];
$this->getJson('/guias-investimento/capitalizacao?'.http_build_query($selection))->assertOk()->assertJsonPath('instruments', ['debt', 'private_equity']);
$this->getJson('/guias-investimento/capitalizacao?instruments[0]=invented')->assertUnprocessable();
$this->getJson('/guias-investimento/capitalizacao?instruments=debt')->assertUnprocessable();
$target = '/investimentos?'.http_build_query(['investment_guide' => $selection]);
$this->get($target)->assertRedirect('/login');
$this->assertSame(url($target), session('url.intended'));
$id = $this->dossier();
$this->getJson($target)->assertOk()->assertJsonPath('guideSelection.instruments', ['debt', 'private_equity']);
$this->getJson($this->url($id).'?'.http_build_query(['current' => 1, ...$selection]))->assertOk()->assertJsonPath('workingCopy', true)->assertJsonPath('saved', null);
$new = $this->postJson('/investimentos', ['title' => 'SYNTHETIC chosen guide destination', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'investment_guide' => $selection])->assertCreated()->json();
$this->getJson($new['guide_href'])->assertOk()->assertJsonPath('instruments', ['debt', 'private_equity'])->assertJsonPath('saved', null);
$this->assertSame(0, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
$before = DB::table('desk_cases')->count();
$this->postJson('/investimentos', ['title' => 'SYNTHETIC invalid guide', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'investment_guide' => [...$selection, 'sha256' => str_repeat('0', 64)]])->assertConflict();
$this->assertSame($before, DB::table('desk_cases')->count());
}
public function test_private_reference_is_encrypted_scoped_versioned_idempotent_and_does_not_advance_any_finance_stage(): void
{
$id = $this->dossier();
$url = $this->url($id);
$steps = DB::table('journey_steps')->orderBy('id')->get()->toJson();
$before = app(VirtualDesk::class)->decode(DB::table('desk_cases')->where('id', $id)->value('payload'));
$input = $this->input($id, ['guide' => ['title' => 'FORGED guarantee'], 'instruments' => ['debt', 'private_equity']]);
$this->postJson($url, $input)->assertOk()->assertJsonPath('revision', 1);
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
$this->postJson($url, [...$input, 'note' => 'SYNTHETIC changed body'])->assertConflict();
$this->postJson($url, [...$input, 'operation_key' => (string) Str::uuid()])->assertConflict();
$this->postJson($url, $this->input($id))->assertOk()->assertJsonPath('unchanged', true);
$r = $this->getJson($url)->assertOk()->assertJsonPath('saved.scope', 'dossier_reference')->assertJsonPath('note', 'SYNTHETIC private preparation')->json();
$this->assertSame(app(InvestmentGuides::class)->guide('capitalizacao'), $r['guide']);
$stored = DB::table('desk_cases')->where('id', $id)->first();
$this->assertStringNotContainsString('SYNTHETIC private preparation', $stored->payload);
$payload = app(VirtualDesk::class)->decode($stored->payload);
unset($payload['investment_guides']);
$this->assertSame($before, $payload);
$event = DB::table('desk_events')->where('case_id', $id)->where('action', InvestmentGuides::EVENT)->first();
$this->assertStringNotContainsString('SYNTHETIC private preparation', $event->payload);
$this->assertStringNotContainsString('SYNTHETIC private preparation', DB::table('audit_events')->where('entity_id', $id)->get()->toJson());
$this->assertSame($steps, DB::table('journey_steps')->orderBy('id')->get()->toJson());
$this->assertDatabaseCount('desk_requests', 0);
$this->assertDatabaseCount('investment_company_declarations', 0);
$this->assertSame(1, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
$this->getJson($url.'?'.http_build_query(['current' => 1, 'selection' => 1, 'sha256' => $r['guide']['sha256']]))->assertOk()->assertJsonPath('instruments', [])->assertJsonPath('workingCopy', true)->assertJsonPath('saved.instruments', ['debt', 'private_equity']);
DB::table('desk_cases')->where('id', $id)->update(['closed_at' => now()]);
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
$this->postJson($url, $this->input($id))->assertConflict();
}
public function test_source_editions_and_notes_remain_recoverable_after_updates_or_withdrawal_and_replays_use_original_choices(): void
{
$id = $this->dossier();
$url = $this->url($id);
$input = $this->input($id);
$this->postJson($url, $input)->assertOk();
$first = $this->getJson($url)->json();
config(['investment-guides.capitalizacao.version' => 'SYNTHETIC future', 'investment-guides.capitalizacao.instruments.debt' => null]);
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
$this->getJson($url)->assertJsonPath('olderEdition', true)->assertJsonPath('guide', $first['guide']);
$this->postJson($url, [...$input, 'version' => $first['case']['version'], 'operation_key' => (string) Str::uuid()])->assertConflict();
config(['investment-guides.capitalizacao.instruments' => ['private_equity' => $first['guide']['instruments']['private_equity']]]);
$this->getJson($url.'?current=1')->assertOk()->assertJsonPath('unavailableInstruments', [$first['guide']['instruments']['debt']['title']])->assertJsonPath('note', $first['note']);
$this->postJson($url, $this->input($id, ['instruments' => ['private_equity'], 'note' => 'SYNTHETIC new edition']))->assertOk()->assertJsonPath('revision', 2);
$this->getJson($url.'?revision='.$first['history'][0]['id'])->assertOk()->assertJsonPath('guide', $first['guide'])->assertJsonPath('note', $first['note']);
config(['investment-guides.capitalizacao' => null]);
$this->getJson('/guias-investimento/capitalizacao')->assertNotFound();
$this->getJson($url)->assertOk()->assertJsonPath('currentAvailable', false)->assertJsonPath('note', 'SYNTHETIC new edition');
$this->getJson($url.'?current=1')->assertConflict();
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
}
public function test_pagination_preserves_every_revision_and_rejects_another_dossiers_history_identifier(): void
{
$id = $this->dossier();
$url = $this->url($id);
for ($i = 1; $i <= 26; $i++) {
$this->postJson($url, $this->input($id, ['note' => 'SYNTHETIC note '.$i]))->assertOk();
}
$r = $this->getJson($url)->assertJsonCount(25, 'history')->json();
$old = $this->getJson($url.'?history_before='.$r['historyNext'])->assertJsonCount(1, 'history')->json();
$revision = $old['history'][0]['id'];
$this->getJson($url.'?revision='.$revision)->assertOk()->assertJsonPath('saved.revision', 1)->assertJsonPath('historical', true);
$this->getJson($url.'?revision='.$revision.'&current=1')->assertUnprocessable();
$other = $this->dossier();
$this->getJson($this->url($other).'?revision='.$revision)->assertNotFound();
}
public function test_context_changes_are_explicit_without_rewriting_the_previous_reference(): void
{
$id = $this->dossier();
$url = $this->url($id);
$this->postJson($url, $this->input($id))->assertOk();
$r = $this->getJson($url)->assertJsonPath('contextChanged', false)->json();
$case = DB::table('desk_cases')->where('id', $id)->first();
$payload = app(VirtualDesk::class)->decode($case->payload);
$payload['investor_path']['context'] = app(InvestorContext::class)->state($case);
$payload['investor_path']['context']['revision'] = 99;
$payload['investor_path']['context']['fields']['project_title'] = 'SYNTHETIC changed scope';
DB::table('desk_cases')->where('id', $id)->update(['payload' => app(VirtualDesk::class)->encode($payload)]);
$this->getJson($url)->assertJsonPath('contextChanged', true)->assertJsonPath('saved.context', $r['saved']['context']);
}
public function test_revocation_and_read_only_membership_are_effective_before_replay_or_validation(): void
{
$id = $this->dossier();
$url = $this->url($id);
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC financing organization', 'organization_type' => 'private']);
DB::table('desk_cases')->where('id', $id)->update(['organization_id' => $org]);
$editor = User::factory()->create();
$viewer = User::factory()->create();
foreach ([[$this->owner, 'officer'], [$editor, 'officer'], [$viewer, 'viewer']] as [$actor, $role]) {
DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'organization_id' => $org, 'user_id' => $actor->id, 'role' => $role]);
}
$input = $this->input($id);
$this->actingAs($editor)->postJson($url, $input)->assertOk();
$this->actingAs($viewer)->getJson($url)->assertOk()->assertJsonPath('canWrite', false);
$this->postJson($url, $input)->assertForbidden();
DB::table('memberships')->where('organization_id', $org)->where('user_id', $editor->id)->update(['deleted_at' => now()]);
$this->actingAs($editor)->getJson($url)->assertForbidden();
$this->postJson($url, $input)->assertForbidden();
$this->postJson($url, [])->assertForbidden();
$this->assertSame(1, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
}
}