assertSame('simulator_fiscal_delta_20261021_test', $db->name); $this->assertSame(55441, (int) $db->port); $this->withoutVite(); $identities = json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true); $this->resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereIn('d.external_id', array_column($identities, 'external_id'))->orderBy('r.id')->get(['r.*']); $this->assertCount(28, $this->resources); $this->editor = User::factory()->create(); config(['portal.managers' => [$this->editor->email]]); $this->actingAs($this->editor); } private function payload(object $r, string $body): array { return ['metadata_version' => (int) $r->metadata_version, 'body' => $body, 'image_mode' => 'original', 'image_resource_id' => null]; } public function test_all_28_open_from_existing_originals_and_private_previews_without_reimport_or_save(): void { $before = DB::table('portal_resource_revisions')->count(); $service = app(EditorialReading::class); foreach ($this->resources as $r) { $original = $service->original($r); $this->assertNotEmpty($original['content_html']); $text = fn (string $html) => preg_replace('/\s+/u', '', html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8')); $this->assertSame($text($original['content_html']), $text(app(LegacyContent::class)->sanitize($original['content_html'], true)), 'Reader must preserve the original text, including inline spans and table cells.'); $this->assertStringStartsWith('https://investemoz.tempo.co.mz/', $service->reading($r, null, true)['image_source_url']); $this->get('/management/portal-services/resources/'.$r->id.'/article')->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $p) => $p->where('source.legacy_id', $original['legacy_id'])->where('content.body', $service->content($r, $original)['body'])); $this->get('/management/portal-services/resources/'.$r->id.'/article/preview')->assertOk()->assertHeader('X-Robots-Tag', 'noindex, nofollow')->assertInertia(fn (Assert $p) => $p->where('article.private_preview', true)->where('article.source_url', $original['source_url'])->where('article.legacy_id', $original['legacy_id'])); $this->get('/artigos/'.$r->id)->assertNotFound(); } $this->assertSame($before, DB::table('portal_resource_revisions')->count()); } public function test_review_changes_save_recover_versions_and_stale_previews_preserve_original(): void { $r = $this->resources->first(fn ($item) => ! $item->editorial_content); $original = app(EditorialReading::class)->original($r); $sha = DB::table('documents')->where('id', $r->document_id)->value('sha256'); $payload = $this->payload($r, 'Correcção editorial sintética QA; preservar o original e não publicar.'); $this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertOk()->assertJsonPath('article.body', $payload['body']); $this->assertNull(DB::table('portal_resources')->where('id', $r->id)->value('editorial_content')); $this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertSessionHasNoErrors()->assertRedirect(); $this->get('/management/portal-services/resources/'.$r->id.'/article')->assertInertia(fn (Assert $p) => $p->where('resource.metadata_version', (int) $r->metadata_version + 1)->where('content.body', $payload['body'])); $this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertConflict(); $this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertConflict(); $payload['metadata_version']++; $payload['body'] .= ' Segunda revisão.'; $this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertSessionHasNoErrors()->assertRedirect(); $this->get('/management/portal-services/resources/'.$r->id.'/article/preview')->assertInertia(fn (Assert $p) => $p->where('article.body', $payload['body'])->where('article.source_url', $original['source_url'])); $this->assertSame($sha, DB::table('documents')->where('id', $r->document_id)->value('sha256')); $this->assertSame($original, app(EditorialReading::class)->original($r)); $this->assertFalse((bool) DB::table('portal_resources')->where('id', $r->id)->value('published')); $this->assertGreaterThanOrEqual(2, DB::table('portal_resource_revisions')->where('resource_id', $r->id)->count()); } public function test_publication_remains_explicit_and_reader_works_before_first_editorial_save(): void { $r = $this->resources->first(fn ($item) => ! $item->editorial_content); $original = app(EditorialReading::class)->original($r); $this->post('/management/portal-services/resources/'.$r->id, ['published' => true])->assertSessionHasErrors(); $document = DB::table('documents')->find($r->document_id); $metadata = ['document_id' => $r->document_id, 'title' => $r->title, 'category' => $r->category, 'topic' => $r->topic, 'issuer' => $document->issuer, 'document_type' => $r->document_type, 'language' => 'pt', 'description' => 'Arquivo editorial QA. Sem oportunidade concreta ou regra activa.', 'source_url' => $r->source_url, 'source_reference' => $r->source_reference, 'source_confirmation' => 'identified', 'legal_status' => 'historical', 'rights_status' => 'authorized', 'data_classification' => 'public', 'metadata_version' => $r->metadata_version, 'associations' => [], 'territorial_scope' => 'unknown', 'independent_review' => false, 'legislation' => false, 'essential' => false]; $this->put('/management/portal-services/resources/'.$r->id, $metadata)->assertSessionHasNoErrors()->assertRedirect(); $this->get('/artigos/'.$r->id)->assertNotFound(); $this->post('/management/portal-services/resources/'.$r->id, ['published' => true])->assertSessionHasNoErrors()->assertRedirect(); Auth::logout(); $this->app['auth']->forgetGuards(); $this->get('/artigos/'.$r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('article.legacy_id', $original['legacy_id'])->where('article.private_preview', false)); $this->get('/documentos/'.$r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('articleUrl', '/artigos/'.$r->id)); $this->actingAs($this->editor); $current = DB::table('portal_resources')->find($r->id); $this->put('/management/portal-services/resources/'.$r->id.'/article', $this->payload($current, 'Edição sintética depois da publicação; revisão expressa necessária.'))->assertRedirect(); $this->get('/artigos/'.$r->id)->assertNotFound(); } public function test_named_editorial_scope_reuses_28_exact_original_identities_and_mfa_without_global_grant(): void { $actor = $this->editor; $other = User::factory()->create(); $actor->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save(); config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'homologation_editorial.accounts' => [$actor->email]]); DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]); $this->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]); $this->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertOk(); $this->assertFalse($actor->can('manage-portal')); DB::table('documents')->where('id', $this->resources->first()->document_id)->update(['external_id' => 'destination-upload-qa-identity', 'source_system' => 'portal-services']); $this->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertOk(); $this->get('/management/portal-services/resources/38/article/preview')->assertForbidden(); $this->withSession(['portal.mfa_at' => time() - 901])->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertForbidden(); $this->actingAs($other)->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertForbidden(); } public function test_structured_original_is_sanitized_and_unavailable_images_are_explicit(): void { $html = app(LegacyContent::class)->sanitize('
Conteúdo
Ligação
', true); $this->assertStringContainsString('', $html); $this->assertStringContainsString('Conteúdo', $html); $this->assertStringNotContainsString('evil', $html); $this->assertStringNotContainsString('onclick', $html); $r = $this->resources->first(fn ($item) => ! $item->editorial_content); $data = app(EditorialReading::class)->reading($r, null, true); if (! $data['image_url']) { $this->assertNotEmpty($data['image_notice']); } $payload = $this->payload($r, app(EditorialReading::class)->content($r, app(EditorialReading::class)->original($r))['body']); $payload['image_mode'] = 'selected'; $payload['image_resource_id'] = 999999; $this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertUnprocessable(); } public function test_team_selects_a_real_scanned_image_and_withdrawal_never_exposes_private_bytes(): void { config(['queue.default' => 'sync', 'services.malware.scanner' => 'clamd', 'services.malware.socket' => null, 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]); $this->assertTrue(app(MalwareScanner::class)->health()['available']); $name = 'synthetic-editorial-'.Str::uuid().'.png'; $bytes = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jVmkAAAAASUVORK5CYII='); $this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent($name, $bytes)])->assertSessionHasNoErrors(); $doc = DB::table('documents')->where('file_name', $name)->first(); $this->assertNotNull($doc); try { $this->assertSame('clean', $doc->malware_scan_status); $metadata = ['document_id' => $doc->id, 'title' => 'Imagem sintética QA', 'issuer' => 'Synthetic QA', 'category' => 'education', 'topic' => 'legacy-archive', 'document_type' => 'unknown', 'language' => 'pt', 'source_url' => 'https://example.invalid/qa-image.png', 'source_reference' => 'Imagem sintética para ensaio transaccional, não é original do legado.', 'source_confirmation' => 'identified', 'legal_status' => 'not_applicable', 'rights_status' => 'authorized', 'data_classification' => 'public', 'territorial_scope' => 'unknown', 'independent_review' => false, 'legislation' => false, 'essential' => false, 'associations' => []]; $this->post('/management/portal-services/resources', $metadata)->assertSessionHasNoErrors(); $image = DB::table('portal_resources')->where('document_id', $doc->id)->first(); $this->post('/management/portal-services/resources/'.$image->id, ['published' => true])->assertSessionHasNoErrors(); $r = $this->resources->first(); $payload = $this->payload($r, 'Texto sintético de prévia com selecção de imagem.'); $payload['image_mode'] = 'selected'; $payload['image_resource_id'] = $image->id; $this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertOk()->assertJsonPath('article.image_url', '/documentos/'.$image->id.'/view'); $this->post('/management/portal-services/resources/'.$image->id, ['published' => false])->assertSessionHasNoErrors(); $this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertUnprocessable(); Auth::logout(); $this->app['auth']->forgetGuards(); $this->get('/documentos/'.$image->id.'/view')->assertNotFound(); } finally { Storage::disk('local')->delete($doc->storage_key); } } }