Files
Investidor-V2/tests/Feature/TerritorialEditorialWorkflowTest.php
T

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