146 lines
8.5 KiB
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');
|
|
}
|
|
}
|