198 lines
13 KiB
PHP
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));
|
|
}
|
|
}
|