assertTrue(IsolatedQaDatabase::allowsSchemaChanges()); [$this->editor,$this->reviewer,$this->publisher] = User::factory()->count(3)->create()->all(); config(['territorial.enabled' => true, 'editorial_workflow.territorial_versions' => true, 'portal.profile_access_enabled' => false, 'portal.managers' => [$this->editor->email, $this->reviewer->email, $this->publisher->email], 'territorial.editors' => [$this->editor->email], 'territorial.reviewers' => [$this->reviewer->email], 'territorial.publishers' => [$this->publisher->email]]); $this->actingAsWithMfa($this->reviewer); $this->actingAsWithMfa($this->publisher); $this->actingAsWithMfa($this->editor); $source = (string) Str::uuid(); $this->id = (string) Str::uuid(); $code = 'QA-'.Str::random(12); DB::table('geo_sources')->insert(['id' => $source, 'name' => 'Synthetic source', 'reference' => 'Synthetic QA only', 'checksum' => str_repeat('c', 64), 'source_version' => 'qa', 'collected_at' => today(), 'status' => 'approved', 'responsible_entity' => 'Synthetic entity', 'license' => 'Synthetic licence', 'editor_id' => $this->editor->id, 'reviewer_id' => $this->reviewer->id]); DB::table('geo_locations')->insert(['id' => $this->id, 'name' => 'Public original', 'code' => $code, 'location_type' => 'country', 'data_classification' => 'public', 'version_no' => 1]); DB::table('geo_territory_profiles')->insert(['geo_location_id' => $this->id, 'source_id' => $source, 'valid_from' => '2020-01-01', 'status' => 'published', 'editor_id' => $this->editor->id, 'reviewer_id' => $this->reviewer->id, 'publisher_id' => $this->publisher->id]); $this->fields = ['name' => 'Private proposal', 'code' => $code, 'location_type' => 'country', 'parent_id' => null, 'source_id' => $source, 'valid_from' => '2020-01-01', 'valid_to' => null, 'version_no' => 1, 'reason' => 'Synthetic editorial proposal']; } private function save(int $version = 1): void { $this->actingAsWithMfa($this->editor)->patchJson('/api/v1/geo/territories/'.$this->id, [...$this->fields, 'version_no' => $version], ['Idempotency-Key' => (string) Str::uuid()])->assertOk()->assertJsonPath('version_no', $version + 1); } private function action(string $action, int $version, int $status = 200): void { $actor = match ($action) { 'pending_review' => $this->editor,'in_review','approved','rejected' => $this->reviewer,default => $this->publisher }; $this->actingAsWithMfa($actor)->postJson('/api/v1/geo/territories/'.$this->id.'/transitions', ['action' => $action, 'version_no' => $version, 'reason' => 'Synthetic reason for '.$action], ['Idempotency-Key' => (string) Str::uuid()])->assertStatus($status); } public function test_publication_is_preserved_through_exact_private_review_until_explicit_publication(): void { $before = DB::table('geo_locations')->find($this->id); $profile = DB::table('geo_territory_profiles')->where('geo_location_id', $this->id)->first(); $this->save(); $this->getJson('/api/v1/geo/drafts?limit=100')->assertOk(); $this->assertSame('Private proposal', app(TerritorialRegistry::class)->working($this->id)['location']['name']); $this->getJson('/api/v1/geo/drafts/'.$this->id)->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertJsonPath('working.location.name', 'Private proposal')->assertJsonPath('public.location.name', 'Public original')->assertJsonCount(2, 'history'); $this->withoutVite(); $this->get('/management/territorial/editorial')->assertOk(); foreach (['pending_review' => 2, 'in_review' => 3, 'approved' => 4] as $action => $version) { $this->action($action, $version); } $this->assertEquals($before, DB::table('geo_locations')->find($this->id)); $this->assertEquals($profile, DB::table('geo_territory_profiles')->where('geo_location_id', $this->id)->first()); $this->assertTrue(app(TerritorialRegistry::class)->published()->where('g.id', $this->id)->exists()); $this->action('published', 5); $this->assertSame('Private proposal', DB::table('geo_locations')->find($this->id)->name); $this->assertSame('published', DB::table('geo_territory_profiles')->where('geo_location_id', $this->id)->value('status')); $this->assertDatabaseHas('geo_territory_revisions', ['geo_location_id' => $this->id, 'version_no' => 6]); $this->save(6); $this->action('archived', 7); $this->assertTrue(app(TerritorialRegistry::class)->published()->where('g.id', $this->id)->exists()); } public function test_changed_evidence_and_concurrent_edits_cannot_reuse_approval(): void { $this->save(); $this->action('pending_review', 2); $this->action('in_review', 3); $this->action('approved', 4); DB::table('geo_sources')->where('id', $this->fields['source_id'])->update(['reference' => 'Changed synthetic source']); $this->action('published', 5, 409); $this->assertSame('Public original', DB::table('geo_locations')->find($this->id)->name); $this->action('rejected', 5, 409); $this->save(5); $this->action('published', 6, 409); config(['editorial_workflow.territorial_versions' => false]); $this->actingAsWithMfa($this->editor)->patchJson('/api/v1/geo/territories/'.$this->id, [...$this->fields, 'version_no' => 6], ['Idempotency-Key' => (string) Str::uuid()])->assertStatus(503); } public function test_explicit_withdrawal_preserves_the_pending_private_review(): void { $this->save(); $this->action('pending_review', 2); $this->action('in_review', 3); $this->action('approved', 4); $this->action('withdrawn', 5); $state = app(TerritorialRegistry::class)->working($this->id); $this->assertSame('approved', $state['profile']['status']); $this->assertTrue($state['fresh']); $this->assertFalse(app(TerritorialRegistry::class)->published()->where('g.id', $this->id)->exists()); $this->action('published', 6); $this->assertTrue(app(TerritorialRegistry::class)->published()->where('g.id', $this->id)->exists()); } public function test_replays_recheck_fresh_mfa_and_revoked_reviewer_blocks_publication(): void { $key = ['Idempotency-Key' => (string) Str::uuid()]; $this->actingAsWithMfa($this->editor)->patchJson('/api/v1/geo/territories/'.$this->id, $this->fields, $key)->assertOk(); $this->withSession(['portal.mfa_at' => time() - 1000])->patchJson('/api/v1/geo/territories/'.$this->id, $this->fields, $key)->assertForbidden(); $this->action('pending_review', 2); $this->action('in_review', 3); $this->action('approved', 4); config(['territorial.reviewers' => []]); $this->action('published', 5, 409); $this->assertSame('Public original', DB::table('geo_locations')->find($this->id)->name); } public function test_private_previews_and_review_cannot_be_obtained_by_unassigned_accounts_or_contributors(): void { $this->save(); config(['territorial.reviewers' => [$this->reviewer->email, $this->editor->email]]); $this->action('pending_review', 2); $this->actingAsWithMfa($this->editor)->postJson('/api/v1/geo/territories/'.$this->id.'/transitions', ['action' => 'in_review', 'version_no' => 3, 'reason' => 'Synthetic independent decision'], ['Idempotency-Key' => (string) Str::uuid()])->assertForbidden(); $outsider = User::factory()->create(); $this->actingAsWithMfa($outsider)->getJson('/api/v1/geo/drafts/'.$this->id)->assertForbidden(); $this->actingAsWithMfa($this->editor)->getJson('/api/v1/geo/territories/country/'.$this->id)->assertOk()->assertJsonPath('territory.name', 'Public original'); } }