142 lines
8.4 KiB
PHP
142 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use App\Services\TerritorialRegistry;
|
|
use App\Support\IsolatedQaDatabase;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
final class TerritorialEditorialWorkflowTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors, DatabaseTransactions;
|
|
|
|
private User $editor;
|
|
|
|
private User $reviewer;
|
|
|
|
private User $publisher;
|
|
|
|
private string $id;
|
|
|
|
private array $fields;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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');
|
|
}
|
|
}
|