149 lines
9.1 KiB
PHP
149 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
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 OrganizationCountryReuseCommandsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $actor;
|
|
|
|
private string $organization;
|
|
|
|
private string $company;
|
|
|
|
private string $submission;
|
|
|
|
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->actingAs($this->actor);
|
|
$this->organization = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SYNTHETIC reuse source', 'organization_type' => 'private', 'source_system' => 'synthetic-reuse-fixture', 'external_id' => 'SYNTHETIC-stable', 'version_no' => 3, 'incorporation_country_code' => 'FR', 'incorporation_country_provenance' => json_encode(['kind' => 'representative_declaration', 'reference' => 'SYNTHETIC source reference'])]);
|
|
DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $this->organization, 'role' => 'viewer']);
|
|
$this->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['residence_country_code' => 'PT']])->assertOk();
|
|
$this->company = $this->postJson('/api/v1/investment/companies', ['version' => 0, 'payload' => ['name' => 'SYNTHETIC private reuse target', 'role' => 'SYNTHETIC representative', 'incorporation_country_code' => 'MZ']])->assertOk()->json('id');
|
|
$this->submission = $this->postJson('/api/v1/investment/submissions', ['company_id' => $this->company])->assertCreated()->json('id');
|
|
}
|
|
|
|
private function command(): array
|
|
{
|
|
return ['version' => 1, 'organization_id' => $this->organization, 'organization_version' => 3, 'operation_key' => (string) Str::uuid()];
|
|
}
|
|
|
|
private function url(?string $id = null): string
|
|
{
|
|
return '/api/v1/investment/companies/'.($id ?? $this->company).'/organization-country';
|
|
}
|
|
|
|
private function target(): \stdClass
|
|
{
|
|
return DB::table('investment_company_declarations')->where('id', $this->company)->first();
|
|
}
|
|
|
|
public function test_read_authority_can_reuse_once_and_replay_keeps_the_original_snapshot_and_encrypted_receipt(): void
|
|
{
|
|
$command = $this->command();
|
|
$submission = DB::table('investment_submissions')->where('id', $this->submission)->first();
|
|
$first = $this->postJson($this->url(), $command)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->json();
|
|
$target = $this->target();
|
|
$auditCount = DB::table('audit_events')->count();
|
|
$this->postJson($this->url(strtoupper($this->company)), [...$command, 'organization_id' => strtoupper($this->organization), 'operation_key' => strtoupper($command['operation_key'])])->assertExactJson($first);
|
|
$this->assertEquals($target, $this->target());
|
|
$this->assertSame($auditCount, DB::table('audit_events')->count());
|
|
$payload = json_decode(Crypt::decryptString($target->payload), true);
|
|
$this->assertSame('FR', $payload['incorporation_country_code']);
|
|
$this->assertSame(3, $payload['organization_country_snapshot']['organization_version']);
|
|
$receipt = json_decode(DB::table('audit_events')->where('source_system', 'investment-workspace-command')->value('after_data'), true);
|
|
$this->assertArrayNotHasKey('result', $receipt);
|
|
$this->assertArrayNotHasKey('country', $receipt);
|
|
$this->assertSame(['version' => 2, 'country' => 'FR'], json_decode(Crypt::decryptString($receipt['result_encrypted']), true));
|
|
$this->assertEquals($submission, DB::table('investment_submissions')->where('id', $this->submission)->first());
|
|
$this->getJson('/api/v1/investment/profile')->assertJsonPath('account.payload.residence_country_code', 'PT')->assertJsonPath('organizations.0.canEdit', false);
|
|
$this->assertSame(1, DB::table('organizations')->count());
|
|
$this->assertSame(1, DB::table('memberships')->count());
|
|
}
|
|
|
|
public function test_replay_reads_its_original_country_after_later_source_and_target_changes_even_if_source_country_is_now_missing(): void
|
|
{
|
|
$first = $this->command();
|
|
$this->postJson($this->url(), $first)->assertOk();
|
|
DB::table('organizations')->where('id', $this->organization)->update(['version_no' => 4, 'incorporation_country_code' => 'ZA']);
|
|
$this->postJson($this->url(), [...$first, 'operation_key' => (string) Str::uuid(), 'version' => 2, 'organization_version' => 4])->assertJsonPath('version', 3)->assertJsonPath('country', 'ZA');
|
|
$target = $this->target();
|
|
DB::table('organizations')->where('id', $this->organization)->update(['version_no' => 5, 'incorporation_country_code' => null]);
|
|
$this->postJson($this->url(), $first)->assertExactJson(['version' => 2, 'country' => 'FR']);
|
|
$this->assertEquals($target, $this->target());
|
|
$this->postJson($this->url(), [...$first, 'operation_key' => (string) Str::uuid(), 'version' => 3, 'organization_version' => 5])->assertUnprocessable();
|
|
$this->assertEquals($target, $this->target());
|
|
$this->assertSame(2, DB::table('audit_events')->where('action', 'organization_country_reused')->count());
|
|
}
|
|
|
|
public function test_stale_versions_malformed_keys_and_changed_intentions_do_not_write(): void
|
|
{
|
|
$command = $this->command();
|
|
$before = $this->target();
|
|
$this->postJson($this->url(), [...$command, 'organization_version' => 2])->assertConflict();
|
|
$this->postJson($this->url(), [...$command, 'version' => 2])->assertConflict();
|
|
$this->postJson($this->url(), [...$command, 'operation_key' => 'bad'])->assertUnprocessable()->assertJsonValidationErrors('operation_key');
|
|
$this->assertEquals($before, $this->target());
|
|
$this->postJson($this->url(), $command)->assertOk();
|
|
$after = $this->target();
|
|
$this->postJson($this->url(), [...$command, 'organization_version' => 4])->assertConflict();
|
|
$this->putJson('/api/v1/investment/profile', ['version' => 1, 'operation_key' => $command['operation_key'], 'payload' => ['residence_country_code' => 'ZA']])->assertConflict();
|
|
$this->assertEquals($after, $this->target());
|
|
}
|
|
|
|
public function test_replay_requires_current_source_access_and_current_ownership_and_can_resume_after_representation_is_restored(): void
|
|
{
|
|
$command = $this->command();
|
|
$this->postJson($this->url(), $command)->assertOk();
|
|
$target = $this->target();
|
|
DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => now()]);
|
|
$this->postJson($this->url(), $command)->assertForbidden();
|
|
DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => null]);
|
|
$this->postJson($this->url(), $command)->assertExactJson(['version' => 2, 'country' => 'FR']);
|
|
DB::table('organizations')->where('id', $this->organization)->update(['deleted_at' => now()]);
|
|
$this->postJson($this->url(), $command)->assertForbidden();
|
|
$this->actingAs(User::factory()->create())->postJson($this->url(), $command)->assertNotFound();
|
|
$this->assertEquals($target, $this->target());
|
|
}
|
|
|
|
public function test_legacy_reuse_without_a_key_is_supported_and_its_country_never_updates_old_submissions(): void
|
|
{
|
|
$command = $this->command();
|
|
unset($command['operation_key']);
|
|
$before = DB::table('investment_submissions')->where('id', $this->submission)->first();
|
|
$this->postJson($this->url(), $command)->assertOk()->assertJsonPath('country', 'FR');
|
|
$new = $this->postJson('/api/v1/investment/submissions', ['company_id' => $this->company])->assertCreated()->json('id');
|
|
$this->getJson('/api/v1/investment/submissions/'.$new)->assertJsonPath('submission.payload.incorporation_country_code', 'FR')->assertJsonPath('submission.payload.organization_country_snapshot.organization_version', 3);
|
|
$this->assertEquals($before, DB::table('investment_submissions')->where('id', $this->submission)->first());
|
|
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'investment-workspace-command')->count());
|
|
}
|
|
|
|
public function test_a_receipt_failure_rolls_back_the_company_and_business_audit_together(): void
|
|
{
|
|
$before = $this->target();
|
|
$auditCount = DB::table('audit_events')->count();
|
|
DB::statement("ALTER TABLE audit_events ADD CONSTRAINT qa_reject_country_reuse_receipt CHECK (source_system <> 'investment-workspace-command')");
|
|
$response = $this->postJson($this->url(), $this->command())->assertStatus(503);
|
|
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
|
|
$this->assertEquals($before, $this->target());
|
|
$this->assertSame($auditCount, DB::table('audit_events')->count());
|
|
}
|
|
}
|