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