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

198 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\DemographicEvidence;
use App\Services\PortalEditorialWorkflow;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\Support\AuthenticatesQaActors;
use Tests\TestCase;
final class DemographyEditorialWorkflowTest extends TestCase
{
use AuthenticatesQaActors, DatabaseTransactions;
private User $editor;
private User $reviewer;
private string $batch;
private string $legacy;
private string $province;
protected function setUp(): void
{
parent::setUp();
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
$this->withoutVite();
Storage::fake('local');
[$this->editor, $this->reviewer] = User::factory()->count(2)->create()->all();
config(['portal.managers' => [$this->editor->email, $this->reviewer->email], 'portal.profile_access_enabled' => false, 'demography.enabled' => true,
'editorial_workflow.enabled' => true, 'editorial_workflow.demography' => true, 'editorial_workflow.independent_approval.kinds' => ['demography']]);
$this->actingAsWithMfa($this->editor);
$this->actingAsWithMfa($this->reviewer);
$country = (string) Str::uuid();
$this->province = (string) Str::uuid();
foreach ([[$country, null, 'country'], [$this->province, $country, 'province']] as [$id, $parent, $type]) {
DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => 'SYNTHETIC EDITORIAL '.$type, 'code' => $id, 'location_type' => $type,
'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id, 'data_classification' => 'public']);
}
$this->legacy = $this->batch('approved', '100.125');
$this->batch = $this->batch('imported', '110.250');
}
private function batch(string $status, string $value): string
{
$id = (string) Str::uuid();
$original = 'SYNTHETIC PREIMPORTED QA ONLY '.$id;
$preview = json_encode(['source' => 'SYNTHETIC QA ONLY', 'record_count' => 1]);
Storage::disk('local')->put('demography/'.$id.'/original.xlsx', $original);
Storage::disk('local')->put('demography/'.$id.'/preview.json', $preview);
$sha = hash('sha256', $original);
DB::table('demographic_imports')->insert(['id' => $id, 'sha256' => $sha, 'preview_sha256' => hash('sha256', $preview), 'file_name' => 'SYNTHETIC-EDITORIAL.xlsx',
'source' => 'SYNTHETIC QA ONLY', 'publication_version' => 'Exemplar sintético, não é fonte INE real', 'status' => $status, 'prepared_by' => $this->editor->id,
'report' => json_encode(['record_count' => 1, 'issue_counts' => ['unit_not_supplied' => 1], 'issues' => [], 'territories' => [], 'scan' => ['result' => 'clean', 'engine' => 'SYNTHETIC QA ONLY', 'sha256' => $sha]])]);
DB::table('demographic_observations')->insert(['import_id' => $id, 'geo_location_id' => $this->province, 'year' => 2026, 'indicator' => 'population', 'sex' => 'all', 'residence' => 'all', 'age_group' => 'all',
'unit' => 'pessoas', 'data_kind' => 'projected', 'original_value' => $value, 'numeric_value' => $value, 'sheet' => '2026', 'cell' => 'B4', 'validation_status' => 'warning']);
return $id;
}
private function draft(int $expected = 0, int $status = 201): ?\stdClass
{
$this->actingAsWithMfa($this->editor);
$this->postJson('/management/editorial/demography/'.$this->batch.'/drafts', ['expected_revision' => $expected])->assertStatus($status);
return app(PortalEditorialWorkflow::class)->latestDemography($this->batch);
}
private function action(\stdClass $edition, string $action, User $actor, int $status = 200): \stdClass
{
$this->actingAsWithMfa($actor);
$this->postJson('/management/editorial/demography/'.$this->batch.'/editions/'.$edition->id, ['version' => $edition->version, 'action' => $action,
'assigned_to' => $this->reviewer->id, 'reason' => 'Conferência sintética e publicação explícita em QA'])->assertStatus($status);
return DB::table('portal_editorial_editions')->where('id', $edition->id)->first();
}
private function approve(\stdClass $edition): \stdClass
{
$edition = $this->action($edition, 'submit', $this->editor);
$edition = $this->action($edition, 'start_review', $this->reviewer);
return $this->action($edition, 'approve', $this->reviewer);
}
public function test_approval_does_not_publish_new_series_and_legacy_values_are_preserved_through_publication_and_archive(): void
{
$legacy = DB::table('demographic_imports')->where('id', $this->legacy)->first();
$before = DB::table('demographic_observations')->whereIn('import_id', [$this->legacy, $this->batch])->orderBy('id')->get()->toJson();
$edition = $this->draft();
$this->draft(0, 409);
$edition = $this->approve($edition);
$service = app(DemographicEvidence::class);
$this->assertSame('no_data', $service->analyse([$this->province], 2026, 2026, 'general', $this->batch)['state']);
$this->assertSame('approved', $edition->state);
$this->assertSame('imported', DB::table('demographic_imports')->where('id', $this->batch)->value('status'));
$this->action($edition, 'publish', $this->reviewer);
$evidence = $service->analyse([$this->province], 2026, 2026, 'general', $this->batch);
$this->assertSame('available', $evidence['state']);
$this->assertSame('110.250', $evidence['territories'][0]['series'][0]['values'][0]['original_value']);
$this->assertSame(1, $evidence['territories'][0]['series'][0]['issue_counts']['unit_not_supplied']);
$this->assertEquals($legacy, DB::table('demographic_imports')->where('id', $this->legacy)->first());
$this->assertSame($before, DB::table('demographic_observations')->whereIn('import_id', [$this->legacy, $this->batch])->orderBy('id')->get()->toJson());
try {
(require database_path('migrations/2026_11_13_233000_extend_editorial_editions_to_demography.php'))->down();
$this->fail('Não eliminar histórico de publicação demográfica.');
} catch (\RuntimeException $exception) {
$this->assertStringContainsString('Preserve demographic', $exception->getMessage());
}
$published = app(PortalEditorialWorkflow::class)->latestDemography($this->batch);
$this->action($published, 'archive', $this->reviewer);
$this->assertSame('no_data', $service->analyse([$this->province], 2026, 2026, 'general', $this->batch)['state']);
$this->assertSame('approved', DB::table('demographic_imports')->where('id', $this->legacy)->value('status'));
$this->assertSame($before, DB::table('demographic_observations')->whereIn('import_id', [$this->legacy, $this->batch])->orderBy('id')->get()->toJson());
}
public function test_changed_observations_or_original_block_publication_and_cannot_be_blessed_by_a_new_draft(): void
{
$edition = $this->approve($this->draft());
DB::table('demographic_observations')->where('import_id', $this->batch)->update(['numeric_value' => '999.999']);
$this->action($edition, 'publish', $this->reviewer, 409);
$this->getJson('/management/editorial/demography/'.$this->batch)->assertJsonPath('edition.fresh', false);
$this->draft(1, 409);
DB::table('demographic_observations')->where('import_id', $this->batch)->update(['numeric_value' => '110.250']);
Storage::disk('local')->put('demography/'.$this->batch.'/original.xlsx', 'SYNTHETIC CHANGED ORIGINAL');
$this->action($edition, 'publish', $this->reviewer, 409);
$this->assertSame('imported', DB::table('demographic_imports')->where('id', $this->batch)->value('status'));
}
public function test_changed_territorial_context_requires_a_new_decision_without_overwriting_numbers(): void
{
$edition = $this->approve($this->draft());
DB::table('geo_locations')->where('id', $this->province)->update(['name' => 'SYNTHETIC REVISED TERRITORY']);
$this->action($edition, 'publish', $this->reviewer, 409);
$next = $this->draft(1);
$this->assertNull($next->approval);
$this->assertSame('110.250', DB::table('demographic_observations')->where('import_id', $this->batch)->value('numeric_value'));
$next = $this->approve($next);
$this->action($next, 'publish', $this->reviewer);
}
public function test_authority_mfa_independence_original_download_and_legacy_bypass_are_enforced(): void
{
$edition = $this->draft();
$this->postJson('/management/demography/'.$this->batch.'/transition', ['action' => 'approved', 'reason' => 'Tentativa por operação antiga'])->assertConflict();
$this->get('/management/editorial/demography/'.$this->batch.'/original')->assertOk()->assertDownload('SYNTHETIC-EDITORIAL.xlsx');
$edition = $this->action($edition, 'submit', $this->editor);
$edition = $this->action($edition, 'start_review', $this->reviewer);
$this->action($edition, 'approve', $this->editor, 403);
$this->actingAs(User::factory()->create())->getJson('/management/editorial/demography/'.$this->batch)->assertForbidden();
$this->getJson('/management/editorial/demography/'.$this->batch.'/original')->assertForbidden();
$this->actingAsWithMfa($this->reviewer);
session()->forget('portal.mfa_at');
$this->postJson('/management/editorial/demography/'.$this->batch.'/editions/'.$edition->id, ['version' => $edition->version, 'action' => 'approve', 'reason' => 'Tentativa sem MFA fresco'])->assertForbidden();
$edition = $this->action($edition, 'approve', $this->reviewer);
$publisher = User::factory()->create();
config(['portal.managers' => [$this->editor->email, $publisher->email]]);
$this->action($edition, 'publish', $publisher, 403);
config(['editorial_workflow.demography' => false]);
$this->actingAsWithMfa($this->editor)->postJson('/management/demography/'.$this->batch.'/transition', ['action' => 'approved', 'reason' => 'Tentativa após desactivação'])->assertStatus(503);
}
public function test_legacy_public_series_need_no_new_approval_and_notices_do_not_grant_access(): void
{
$this->actingAsWithMfa($this->editor);
$this->postJson('/management/editorial/demography/'.$this->legacy.'/drafts', ['expected_revision' => 0])->assertConflict();
$this->assertSame(0, DB::table('portal_editorial_editions')->where('demographic_import_id', $this->legacy)->count());
$edition = $this->action($this->draft(), 'submit', $this->editor);
$this->actingAsWithMfa($this->reviewer)->getJson('/management/editorial')->assertJsonPath('demographyPending', 1)->assertJsonPath('demographyUrl', '/management/demography')
->assertJsonPath('notices.0.href', '/management/editorial/demography/'.$this->batch.'?edition='.$edition->id);
config(['portal.managers' => [$this->editor->email]]);
$this->getJson('/management/editorial')->assertForbidden();
}
public function test_131400_observations_are_fingerprinted_and_published_with_bounded_php_memory(): void
{
DB::table('demographic_observations')->where('import_id', $this->batch)->delete();
DB::statement("INSERT INTO demographic_observations(import_id,geo_location_id,year,indicator,sex,residence,age_group,unit,data_kind,original_value,numeric_value,sheet,cell,validation_status) SELECT ?,?,2026,'population','all','all','SYNTHETIC-QA-'||i,'pessoas','projected','1','1','QA','B4','valid' FROM generate_series(1,131400) i", [$this->batch, $this->province]);
$report = json_decode(DB::table('demographic_imports')->where('id', $this->batch)->value('report'), true, 512, JSON_THROW_ON_ERROR);
$report['record_count'] = 131400;
$preview = json_encode(['source' => 'SYNTHETIC QA ONLY', 'record_count' => 131400], JSON_THROW_ON_ERROR);
Storage::disk('local')->put('demography/'.$this->batch.'/preview.json', $preview);
DB::table('demographic_imports')->where('id', $this->batch)->update(['report' => json_encode($report, JSON_THROW_ON_ERROR), 'preview_sha256' => hash('sha256', $preview)]);
$edition = $this->approve($this->draft());
$this->action($edition, 'publish', $this->reviewer);
$this->assertSame('approved', DB::table('demographic_imports')->where('id', $this->batch)->value('status'));
$this->assertSame(131400, DB::table('demographic_observations')->where('import_id', $this->batch)->count());
$this->assertLessThan(256 * 1024 * 1024, memory_get_peak_usage(true));
}
}