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 */ 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'); } }