174 lines
13 KiB
PHP
174 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use App\Services\EditorialReading;
|
|
use App\Services\LegacyContent;
|
|
use App\Services\Malware\MalwareScanner;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Tests\TestCase;
|
|
|
|
class LegacyEditorialWorkflowTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
|
|
private User $editor;
|
|
|
|
private $resources;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$db = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
|
$this->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('<div onclick="evil()"><table><tr><td>Conteúdo</td></tr></table><script>evil()</script><a href="javascript:evil()">Ligação</a></div>', true);
|
|
$this->assertStringContainsString('<table>', $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);
|
|
}
|
|
}
|
|
}
|