184 lines
11 KiB
PHP
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());
|
|
}
|
|
}
|