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

184 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\DevelopmentCorridor;
use App\Models\EconomicCluster;
use App\Models\InvestmentDossier;
use App\Models\Project;
use App\Models\User;
use App\Services\PortalEditorialWorkflow;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\Support\AuthenticatesQaActors;
use Tests\TestCase;
final class PortfolioEditorialWorkflowTest extends TestCase
{
use AuthenticatesQaActors, DatabaseTransactions;
private User $author;
private User $reviewer;
private InvestmentDossier $dossier;
protected function setUp(): void
{
parent::setUp();
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
if (! Schema::hasColumn('portal_editorial_editions', 'dossier_id')) {
(require database_path('migrations/2026_11_13_230000_extend_editorial_editions_to_portfolio.php'))->up();
}
$this->withoutVite();
[$this->author, $this->reviewer] = User::factory()->count(2)->create()->all();
config(['portal.managers' => [$this->author->email, $this->reviewer->email], 'portal.profile_access_enabled' => false,
'editorial_workflow.enabled' => true, 'editorial_workflow.portfolio' => true]);
$this->makeDossier('project');
}
private function makeDossier(string $kind): void
{
$model = match ($kind) {
'project' => Project::class, 'cluster' => EconomicCluster::class, 'corridor' => DevelopmentCorridor::class,
};
$entity = $model::create(['code' => 'ED-QA-'.bin2hex(random_bytes(6)), 'name' => 'Contexto sintético', 'status' => 'active', ...($kind === 'cluster' ? ['sector' => 'Energia'] : [])]);
$this->dossier = InvestmentDossier::create([$kind.'_id' => $entity->id, 'title' => 'Ficha pública sintética '.$kind, 'source' => 'Fonte sintética QA', 'content' => ['description' => 'Texto público original'],
'version_no' => 1, 'publication_status' => 'published', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Publicação sintética aceite em QA', 'published_at' => now()]);
}
private function edit(string $text = 'Alteração privada deliberada', int $status = 302, ?int $expected = null): ?\stdClass
{
$this->actingAsWithMfa($this->author);
$record = app(PortalEditorialWorkflow::class)->workingPortfolio($this->dossier->fresh());
$this->put('/management/portfolio/'.$record->id, ['version_no' => $expected ?? $record->version_no, 'title' => 'Novo título privado', 'source' => 'Fonte sintética revista', 'content' => ['description' => $text]])->assertStatus($status);
return app(PortalEditorialWorkflow::class)->latestPortfolio($record->id);
}
private function action(User $actor, \stdClass $edition, string $action, int $status = 200, array $extra = []): ?\stdClass
{
$this->actingAsWithMfa($actor);
$this->postJson('/management/editorial/portfolio/'.$this->dossier->id.'/editions/'.$edition->id,
['version' => $edition->version, 'action' => $action, 'reason' => 'Decisão fundamentada de demonstração em QA', ...$extra])->assertStatus($status);
return DB::table('portal_editorial_editions')->where('id', $edition->id)->first();
}
private function approve(\stdClass $edition): \stdClass
{
$edition = $this->action($this->author, $edition, 'submit', 200, ['assigned_to' => $this->reviewer->id]);
$edition = $this->action($this->reviewer, $edition, 'start_review');
return $this->action($this->reviewer, $edition, 'approve');
}
public function test_public_project_cluster_and_corridor_fiches_survive_private_edit_review_and_approval(): void
{
foreach (['project', 'cluster', 'corridor'] as $kind) {
if ($kind !== 'project') {
$this->makeDossier($kind);
}
$before = $this->dossier->fresh()->getAttributes();
$edition = $this->edit();
$this->assertSame($before, $this->dossier->fresh()->getAttributes());
$this->get('/carteira/'.$this->dossier->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('record.content.description', 'Texto público original'));
$this->get('/management/portfolio/'.$this->dossier->id)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $p) => $p->where('record.content.description', 'Alteração privada deliberada')->where('editorialEnabled', true));
$this->edit('Edição obsoleta', 409, 1);
$edition = $this->approve($edition);
$this->assertSame($before, $this->dossier->fresh()->getAttributes());
$this->action($this->author, $edition, 'publish', 403);
$this->action($this->reviewer, $edition, 'publish');
$this->get('/carteira/'.$this->dossier->id)->assertInertia(fn (Assert $p) => $p->where('record.content.description', 'Alteração privada deliberada'));
$this->assertSame($this->dossier->id, $this->dossier->fresh()->id);
$this->assertSame($this->reviewer->id, $this->dossier->fresh()->reviewer_id);
if ($kind === 'corridor') {
$published = app(PortalEditorialWorkflow::class)->latestPortfolio($this->dossier->id);
$this->action($this->reviewer, $published, 'archive');
$this->get('/carteira/'.$this->dossier->id)->assertNotFound();
}
}
}
public function test_changed_context_and_edits_reset_decisions_and_old_publication_remains(): void
{
$edition = $this->approve($this->edit());
Project::whereKey($this->dossier->project_id)->update(['name' => 'Contexto revisto']);
$this->getJson('/management/editorial/portfolio/'.$this->dossier->id)->assertJsonPath('edition.fresh', false);
$this->action($this->reviewer, $edition, 'publish', 409);
$new = $this->edit('Nova edição após alteração do contexto');
$this->assertNull($new->approval);
$this->assertNull($new->legal_validation);
$this->action($this->reviewer, $edition, 'publish', 409);
$this->get('/carteira/'.$this->dossier->id)->assertInertia(fn (Assert $p) => $p->where('record.content.description', 'Texto público original'));
$this->getJson('/management/editorial/portfolio/'.$this->dossier->id.'?edition='.$edition->id)->assertJsonPath('current', false)->assertJsonPath('edition.id', $edition->id);
}
public function test_permissions_api_notice_scope_and_legacy_bypass_fail_closed(): void
{
$edition = $this->edit();
$this->post('/management/portfolio/'.$this->dossier->id.'/publish', ['version_no' => 1, 'approval_evidence' => 'Tentativa legada sintética'])->assertConflict();
$edition = $this->action($this->author, $edition, 'submit', 200, ['assigned_to' => $this->reviewer->id]);
$this->actingAsWithMfa($this->reviewer);
$this->getJson('/management/editorial')->assertOk()->assertJsonPath('portfolioUrl', '/management/portfolio')->assertJsonPath('portfolioPending', 1)
->assertJsonPath('notices.0.href', '/management/editorial/portfolio/'.$this->dossier->id.'?edition='.$edition->id);
$this->get('/gestao-conteudos')->assertInertia(fn (Assert $p) => $p->where('access.portfolio.index', '/management/portfolio'));
$other = User::factory()->create();
$this->actingAs($other)->getJson('/management/editorial/portfolio/'.$this->dossier->id)->assertForbidden();
$this->action($other, $edition, 'start_review', 403);
$this->actingAsWithMfa($this->reviewer);
session()->forget('portal.mfa_at');
$this->postJson('/management/editorial/portfolio/'.$this->dossier->id.'/editions/'.$edition->id, ['version' => $edition->version, 'action' => 'start_review', 'reason' => 'Motivo sintético QA'])->assertForbidden();
config(['editorial_workflow.portfolio' => false]);
$this->actingAsWithMfa($this->author);
$this->put('/management/portfolio/'.$this->dossier->id, ['version_no' => 1, 'title' => 'Não escrever', 'source' => 'Fonte QA', 'content' => ['description' => 'Não escrever']])->assertStatus(503);
$this->get('/carteira/'.$this->dossier->id)->assertOk();
$this->actingAsWithMfa($this->reviewer);
$this->getJson('/management/editorial')->assertJsonCount(0, 'notices');
}
public function test_revoked_reviewer_and_foreign_edition_cannot_publish_and_private_archive_preserves_public(): void
{
$firstId = $this->dossier->id;
$edition = $this->approve($this->edit());
$this->makeDossier('cluster');
$this->action($this->reviewer, $edition, 'publish', 404);
$this->getJson('/management/editorial/portfolio/'.$this->dossier->id.'?edition='.$edition->id)->assertNotFound();
$this->dossier = InvestmentDossier::findOrFail($firstId);
config(['portal.managers' => [$this->author->email]]);
// The author cannot publish, and a different authorised publisher must recheck the old reviewer.
$publisher = User::factory()->create();
config(['portal.managers' => [$this->author->email, $publisher->email]]);
$this->action($publisher, $edition, 'publish', 403);
$this->action($this->author, $edition, 'archive');
$this->get('/carteira/'.$this->dossier->id)->assertOk();
$this->assertSame('Texto público original', $this->dossier->fresh()->content['description']);
}
public function test_every_private_contributor_is_excluded_and_portfolio_history_cannot_be_removed(): void
{
$this->edit();
$this->actingAsWithMfa($this->reviewer);
$this->put('/management/portfolio/'.$this->dossier->id, ['version_no' => 2, 'title' => 'Colaboração privada', 'source' => 'Fonte sintética', 'content' => ['description' => 'Revisão deliberada pelo colaborador']])->assertRedirect();
$edition = app(PortalEditorialWorkflow::class)->latestPortfolio($this->dossier->id);
$edition = $this->action($this->author, $edition, 'submit');
$edition = $this->action($this->reviewer, $edition, 'start_review');
$this->action($this->reviewer, $edition, 'approve', 403);
$this->action($this->author, $edition, 'approve', 403);
Project::whereKey($this->dossier->project_id)->update(['name' => 'Contexto alterado; é necessária correcção']);
$this->action($this->reviewer, $edition, 'return');
$this->get('/management/portfolio/'.$this->dossier->id)->assertInertia(fn (Assert $p) => $p->where('record.content.description', 'Revisão deliberada pelo colaborador'));
try {
(require database_path('migrations/2026_11_13_230000_extend_editorial_editions_to_portfolio.php'))->down();
$this->fail('Histórico não vazio não pode ser eliminado.');
} catch (\RuntimeException $exception) {
$this->assertStringContainsString('Preserve portfolio', $exception->getMessage());
}
$this->assertTrue(Schema::hasColumn('portal_editorial_editions', 'dossier_id'));
$this->assertSame(3, DB::table('portal_editorial_editions')->where('dossier_id', $this->dossier->id)->count());
}
}