Files
Investidor-V2/tests/Feature/OrganizationCountryCommandsTest.php
T

131 lines
7.6 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\PortalProfileAdministration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Tests\TestCase;
final class OrganizationCountryCommandsTest extends TestCase
{
use RefreshDatabase;
private User $actor;
private string $organization;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
config(['portal.organization_country_enabled' => true, 'portal.profile_access_enabled' => true]);
$this->actor = User::factory()->create();
$this->organization = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SYNTHETIC country master', 'organization_type' => 'private', 'source_system' => 'synthetic-command-fixture', 'external_id' => 'SYNTHETIC-stable-source']);
DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $this->organization, 'role' => 'officer']);
$this->actingAs($this->actor);
}
private function command(): array
{
return ['operation_key' => (string) Str::uuid(), 'version' => 1, 'country' => 'FR', 'source_reference' => 'SYNTHETIC PRIVATE declaration reference'];
}
private function url(?string $id = null): string
{
return '/api/v1/investment/organizations/'.($id ?? $this->organization).'/country';
}
public function test_replay_keeps_country_provenance_version_and_audit_identical(): void
{
$command = $this->command();
$first = $this->putJson($this->url(), $command)->assertOk()->assertJsonPath('version', 2)->json();
$before = DB::table('organizations')->where('id', $this->organization)->first();
$audits = DB::table('audit_events')->get()->toArray();
$this->putJson($this->url(), [...$command, 'operation_key' => strtoupper($command['operation_key'])])->assertExactJson($first);
$this->assertEquals($before, DB::table('organizations')->where('id', $this->organization)->first());
$this->assertEquals($audits, DB::table('audit_events')->get()->toArray());
$this->assertSame(2, DB::table('audit_events')->count());
$receipt = json_decode(DB::table('audit_events')->where('source_system', 'investment-workspace-command')->value('after_data'), true);
$this->assertStringNotContainsString('SYNTHETIC PRIVATE', json_encode($receipt));
$this->assertSame($command['source_reference'], json_decode(Crypt::decryptString($receipt['command_encrypted']), true)['payload']['source_reference']);
}
public function test_original_receipt_survives_a_later_change_without_reverting_current_country(): void
{
$command = $this->command();
$this->putJson($this->url(), $command)->assertOk();
$this->putJson($this->url(), [...$command, 'operation_key' => (string) Str::uuid(), 'version' => 2, 'country' => 'ZA'])->assertJsonPath('version', 3);
$before = DB::table('organizations')->where('id', $this->organization)->first();
$this->putJson($this->url(), $command)->assertExactJson(['version' => 2, 'country' => 'FR']);
$this->assertEquals($before, DB::table('organizations')->where('id', $this->organization)->first());
$this->assertSame('ZA', $before->incorporation_country_code);
}
public function test_changed_reference_country_target_or_operation_cannot_reuse_the_same_key(): void
{
$command = $this->command();
$this->putJson($this->url(), $command)->assertOk();
$this->putJson($this->url(), [...$command, 'source_reference' => 'SYNTHETIC altered reference'])->assertConflict();
$this->putJson($this->url(), [...$command, 'country' => 'ZA'])->assertConflict();
$id = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $id, 'name' => 'SYNTHETIC second master', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $id, 'role' => 'officer']);
$this->putJson($this->url($id), $command)->assertConflict();
$this->putJson('/api/v1/investment/profile', ['version' => 0, 'operation_key' => $command['operation_key'], 'payload' => ['residence_country_code' => 'PT']])->assertConflict();
$this->assertSame(0, DB::table('investment_accounts')->count());
$this->assertSame(2, DB::table('audit_events')->count());
}
public function test_revoked_representation_auditor_and_deleted_organization_cannot_read_a_replay_confirmation(): void
{
$command = $this->command();
$this->putJson($this->url(), $command)->assertOk();
$before = DB::table('organizations')->where('id', $this->organization)->first();
DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => now()]);
$this->putJson($this->url(), $command)->assertForbidden();
DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => null]);
$approver = User::factory()->create();
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $approver, ['user_id' => $this->actor->id, 'role' => 'auditor', 'opportunity_types' => [], 'source' => 'synthetic-country-command-auditor']);
$this->actor->refresh();
$this->actingAs($this->actor)->withSession(['portal.access_revision' => $this->actor->portal_access_revision]);
$this->putJson($this->url(), $command)->assertForbidden();
DB::table('portal_role_assignments')->where('user_id', $this->actor->id)->update(['revoked_at' => now()]);
DB::table('organizations')->where('id', $this->organization)->update(['deleted_at' => now()]);
$this->putJson($this->url(), $command)->assertForbidden();
$after = DB::table('organizations')->where('id', $this->organization)->first();
$this->assertSame($before->incorporation_country_provenance, $after->incorporation_country_provenance);
$this->assertSame(2, $after->version_no);
}
public function test_invalid_country_reference_and_key_do_not_increment_version_and_legacy_requests_still_work(): void
{
$command = $this->command();
foreach ([['country' => 'ZZ'], ['source_reference' => 'x'], ['operation_key' => 'bad']] as $invalid) {
$this->putJson($this->url(), [...$command, ...$invalid])->assertUnprocessable()->assertJsonValidationErrors(array_keys($invalid));
}
$this->assertSame(1, DB::table('organizations')->where('id', $this->organization)->value('version_no'));
$this->assertSame(0, DB::table('audit_events')->count());
unset($command['operation_key']);
$this->putJson($this->url(), $command)->assertOk()->assertJsonPath('version', 2);
$this->assertSame(1, DB::table('audit_events')->count());
}
public function test_current_membership_role_changes_project_the_same_authority_as_the_write_endpoint(): void
{
$this->getJson('/api/v1/investment/profile')->assertOk()->assertJsonCount(1, 'organizations')->assertJsonPath('organizations.0.canEdit', true);
$this->putJson($this->url(), $this->command())->assertOk();
DB::table('memberships')->where('user_id', $this->actor->id)->update(['role' => 'viewer']);
$this->getJson('/api/v1/investment/profile')->assertOk()->assertJsonPath('organizations.0.canEdit', false);
$this->putJson($this->url(), [...$this->command(), 'version' => 2])->assertForbidden();
}
}