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

146 lines
8.5 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\Str;
use Tests\TestCase;
class TerritorialRegistryTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config(['territorial.enabled' => true]);
}
private function create(string $url, array $data)
{
return $this->postJson($url, $data, ['Idempotency-Key' => (string) Str::uuid()]);
}
private function patchTerritory(string $url, array $data)
{
return $this->patchJson($url, $data, ['Idempotency-Key' => (string) Str::uuid()]);
}
public function test_writes_require_idempotency_and_replays_recheck_current_permissions(): void
{
$user = $this->editor();
$this->actingAs($user);
$source = $this->source();
$this->postJson('/api/v1/geo/territories', $this->territory($source))->assertUnprocessable()->assertHeader('Content-Type', 'application/problem+json');
$key = ['Idempotency-Key' => (string) Str::uuid()];
$first = $this->postJson('/api/v1/geo/territories', $this->territory($source), $key)->assertOk()->json('id');
$this->postJson('/api/v1/geo/territories', $this->territory($source), $key)->assertOk()->assertJsonPath('id', $first);
$this->postJson('/api/v1/geo/territories', [...$this->territory($source), 'name' => 'Changed'], $key)->assertConflict();
config(['portal.managers' => []]);
$this->postJson('/api/v1/geo/territories', $this->territory($source), $key)->assertForbidden();
$this->assertDatabaseCount('geo_locations', 1);
}
private function editor(): User
{
$user = User::factory()->create();
$user->forceFill(['two_factor_secret' => Crypt::encryptString('SYNTHETIC'), 'two_factor_confirmed_at' => now()])->save();
config(['portal.managers' => [$user->email]]);
config(['territorial.editors' => [$user->email]]);
return $user;
}
private function source(): string
{
return $this->create('/api/v1/geo/sources', ['name' => 'Synthetic source', 'reference' => 'Synthetic reference only',
'checksum' => str_repeat('a', 64), 'source_version' => 'test', 'collected_at' => today()->toDateString()])->assertCreated()->json('id');
}
/** @return array<string,mixed> */
private function territory(string $source, string $type = 'country', ?string $parent = null): array
{
return ['name' => 'Synthetic '.$type, 'code' => 'TEST-'.$type, 'location_type' => $type, 'parent_id' => $parent,
'source_id' => $source, 'valid_from' => today()->toDateString(), 'reason' => 'Synthetic territorial record'];
}
public function test_drafts_are_private_and_hierarchy_duplicates_and_stale_versions_are_rejected(): void
{
$this->actingAs($this->editor());
$source = $this->source();
$country = $this->create('/api/v1/geo/territories', $this->territory($source))->assertOk()->json('id');
$this->getJson('/api/v1/geo/sources')->assertOk()->assertJsonPath('data.0.id', $source);
$this->getJson('/api/v1/geo/drafts')->assertOk()->assertJsonPath('data.0.id', $country);
$this->create('/api/v1/geo/territories', $this->territory($source))->assertUnprocessable();
$this->create('/api/v1/geo/territories', $this->territory($source, 'district', $country))->assertUnprocessable();
$province = $this->create('/api/v1/geo/territories', $this->territory($source, 'province', $country))->assertOk()->json('id');
$this->getJson('/api/v1/geo/provinces')->assertOk()->assertJsonCount(0, 'data');
$this->patchTerritory('/api/v1/geo/territories/'.$province, [...$this->territory($source, 'province', $country), 'version_no' => 1])->assertOk()->assertJsonPath('version_no', 2);
$this->patchTerritory('/api/v1/geo/territories/'.$province, [...$this->territory($source, 'province', $country), 'version_no' => 1])->assertConflict();
$this->assertDatabaseCount('geo_territory_revisions', 3);
}
public function test_revoking_ancestor_source_hides_descendants_and_filters_are_scoped(): void
{
$this->actingAs($this->editor());
$source = $this->source();
$country = $this->create('/api/v1/geo/territories', $this->territory($source))->json('id');
$province = $this->create('/api/v1/geo/territories', $this->territory($source, 'province', $country))->json('id');
// Synthetic published fixture only; no production approval is inferred from this test.
$reviewer = User::factory()->create();
$publisher = User::factory()->create();
DB::table('geo_sources')->where('id', $source)->update(['status' => 'approved', 'responsible_entity' => 'Synthetic entity', 'license' => 'Synthetic licence']);
DB::table('geo_territory_profiles')->update(['status' => 'published', 'reviewer_id' => $reviewer->id, 'publisher_id' => $publisher->id]);
DB::table('geo_locations')->update(['data_classification' => 'public']);
$this->getJson('/api/v1/geo/provinces?country_id='.$country.'&limit=1')->assertOk()->assertJsonPath('data.0.id', $province);
$this->getJson('/api/v1/geo/provinces?country_id='.Str::uuid())->assertJsonCount(0, 'data');
DB::table('geo_territory_profiles')->where('geo_location_id', $country)->update(['status' => 'archived']);
$this->getJson('/api/v1/geo/provinces')->assertJsonCount(0, 'data');
$this->getJson('/api/v1/geo/territories/province/'.$province)->assertNotFound();
}
public function test_authentication_mfa_and_empty_rollback(): void
{
$this->getJson('/api/v1/geo/countries')->assertUnauthorized();
$user = User::factory()->create();
config(['portal.managers' => [$user->email]]);
$this->actingAs($user)->create('/api/v1/geo/sources', [])->assertForbidden();
$migration = require database_path('migrations/2026_09_28_150000_territorial_registry.php');
$migration->down();
$migration->up();
$this->actingAs($this->editor());
$this->source();
$this->expectException(\RuntimeException::class);
$migration->down();
}
public function test_editor_reviewer_publisher_workflow_and_source_revocation(): void
{
$editor = $this->editor();
$reviewer = $this->editor();
$publisher = $this->editor();
config(['portal.managers' => [$editor->email, $reviewer->email, $publisher->email],
'territorial.editors' => [$editor->email], 'territorial.reviewers' => [$reviewer->email, $editor->email],
'territorial.publishers' => [$publisher->email, $editor->email]]);
$this->actingAs($editor);
$source = $this->create('/api/v1/geo/sources', ['name' => 'Synthetic complete source', 'reference' => 'Synthetic source reference',
'checksum' => str_repeat('b', 64), 'source_version' => 'test', 'collected_at' => today()->toDateString(),
'responsible_entity' => 'Synthetic entity', 'license' => 'Synthetic test permission'])->json('id');
$id = $this->create('/api/v1/geo/territories', $this->territory($source))->json('id');
$url = '/api/v1/geo/territories/'.$id.'/transitions';
$this->create($url, ['version_no' => 1, 'action' => 'pending_review', 'reason' => 'Synthetic review requested'])->assertOk();
$this->create($url, ['version_no' => 2, 'action' => 'approved', 'reason' => 'Synthetic self review attempt'])->assertForbidden();
$this->actingAs($reviewer);
$this->create('/api/v1/geo/sources/'.$source.'/transitions', ['action' => 'approved', 'approval_reference' => 'Synthetic source review evidence'])->assertOk();
$this->create($url, ['version_no' => 2, 'action' => 'approved', 'reason' => 'Synthetic independent review'])->assertOk();
$this->actingAs($editor)->create($url, ['version_no' => 3, 'action' => 'published', 'reason' => 'Synthetic self publication attempt'])->assertForbidden();
$this->actingAs($publisher)->create($url, ['version_no' => 3, 'action' => 'published', 'reason' => 'Synthetic independent publication'])->assertOk();
$this->getJson('/api/v1/geo/countries')->assertJsonPath('data.0.id', $id);
$this->create('/api/v1/geo/sources/'.$source.'/transitions', ['action' => 'revoked', 'approval_reference' => 'Synthetic source revoked'])->assertOk();
$this->getJson('/api/v1/geo/countries')->assertJsonCount(0, 'data');
}
}