190 lines
10 KiB
PHP
190 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\EconomicCluster;
|
|
use App\Models\InvestmentDossier;
|
|
use App\Models\User;
|
|
use App\Services\ClusterCompetitiveness;
|
|
use App\Services\ContentManagement;
|
|
use App\Support\IsolatedQaDatabase;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
final class ClusterEditorialWorkflowTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors, DatabaseTransactions;
|
|
|
|
private User $author;
|
|
|
|
private User $reviewer;
|
|
|
|
private EconomicCluster $cluster;
|
|
|
|
private array $draft;
|
|
|
|
private int $version = 0;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
|
|
$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.cluster_competitiveness' => true, 'editorial_workflow.independent_approval.kinds' => ['cluster_competitiveness']]);
|
|
$this->cluster = EconomicCluster::create(['code' => 'ED-QA-'.bin2hex(random_bytes(6)), 'name' => 'Cluster sintético QA', 'sector' => 'Energia', 'status' => 'planned']);
|
|
$this->draft = ['version' => 1, 'clusterId' => $this->cluster->id, 'sector' => 'Energia', 'region' => 'Local sintético', 'forces' => []];
|
|
foreach (ClusterCompetitiveness::FORCES as $force) {
|
|
$this->draft['forces'][$force] = ['pressure' => 'medium', 'evidence' => 'Exemplo sintético', 'source' => 'Fonte sintética QA p. 2', 'period' => '2026',
|
|
'rationale' => 'Hipótese sintética', 'limitations' => 'Não são dados reais', 'opportunity' => 'Ideia de demonstração'];
|
|
}
|
|
}
|
|
|
|
private function url(): string
|
|
{
|
|
return '/management/clusters/'.$this->cluster->id.'/competitiveness';
|
|
}
|
|
|
|
private function action(User $actor, string $action, array $extra = [], int $status = 200): array
|
|
{
|
|
$this->actingAsWithMfa($actor);
|
|
$response = $this->postJson($this->url().'/'.$action, ['expected_version' => $this->version, 'reason' => 'Decisão sintética fundamentada em QA', ...$extra])->assertStatus($status);
|
|
if ($status !== 200) {
|
|
return [];
|
|
}
|
|
$this->version++;
|
|
|
|
return $response->assertJsonPath('version', $this->version)->json();
|
|
}
|
|
|
|
private function approved(): array
|
|
{
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft]);
|
|
$this->action($this->author, 'submitted');
|
|
$this->action($this->reviewer, 'review_started');
|
|
|
|
return $this->action($this->reviewer, 'approved');
|
|
}
|
|
|
|
public function test_separate_approval_publication_drafts_restore_archive_and_public_projection(): void
|
|
{
|
|
$dossier = InvestmentDossier::create(['cluster_id' => $this->cluster->id, 'title' => 'Dossier QA', 'source' => 'Fonte sintética', 'content' => ['sector' => 'Energia'],
|
|
'publication_status' => 'published', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Publicação sintética em QA', 'published_at' => now()]);
|
|
$approved = $this->approved();
|
|
$service = app(ClusterCompetitiveness::class);
|
|
$this->assertNull($service->publicData($this->cluster->id));
|
|
$this->assertSame('approved', $approved['workflow']['state']);
|
|
$this->action($this->reviewer, 'published');
|
|
$public = $service->publicData($this->cluster->id);
|
|
$this->assertSame($this->draft, $public['draft']);
|
|
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $p) => $p->where('competitiveness.draft.region', 'Local sintético')->missing('competitiveness.workflow')->missing('competitiveness.actor'));
|
|
$changed = [...$this->draft, 'region' => 'Edição privada deliberada'];
|
|
$draft = $this->action($this->author, 'saved', ['draft' => $changed]);
|
|
$this->assertNull($draft['workflow']['approval']);
|
|
$this->assertSame($public, $service->publicData($this->cluster->id));
|
|
$this->action($this->author, 'published', [], 409);
|
|
$this->action($this->author, 'restored', ['revision' => 1]);
|
|
$this->action($this->author, 'archived');
|
|
$this->assertSame($public, $service->publicData($this->cluster->id));
|
|
$this->getJson($this->url())->assertJsonPath('workflow.state', 'archived')->assertJsonPath('draft', $this->draft);
|
|
$this->action($this->reviewer, 'withdrawn');
|
|
$this->assertNull($service->publicData($this->cluster->id));
|
|
$this->assertSame($this->version, DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $this->cluster->id)->count());
|
|
}
|
|
|
|
public function test_independence_contributors_return_reject_and_conflicts(): void
|
|
{
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft]);
|
|
$this->action($this->author, 'submitted');
|
|
$this->action($this->reviewer, 'review_started');
|
|
$this->action($this->author, 'approved', [], 403);
|
|
$this->action($this->reviewer, 'returned');
|
|
$this->action($this->reviewer, 'saved', ['draft' => [...$this->draft, 'region' => 'Alteração do revisor']]);
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft, 'expected_version' => 0], 409);
|
|
$this->action($this->author, 'submitted');
|
|
$this->action($this->reviewer, 'review_started');
|
|
$this->action($this->reviewer, 'approved', [], 403);
|
|
$this->action($this->reviewer, 'rejected');
|
|
$this->action($this->author, 'submitted', [], 409);
|
|
$this->action($this->author, 'restored', ['revision' => 1]);
|
|
$this->getJson($this->url())->assertJsonPath('workflow.state', 'draft')->assertJsonPath('workflow.approval', null);
|
|
}
|
|
|
|
public function test_changed_cluster_context_or_revoked_approval_requires_new_decision(): void
|
|
{
|
|
$this->approved();
|
|
$this->cluster->update(['sector' => 'Outro sector sintético']);
|
|
$this->getJson($this->url())->assertJsonPath('workflow.fresh', false);
|
|
$this->action($this->reviewer, 'published', [], 409);
|
|
$this->approved();
|
|
config(['portal.managers' => [$this->author->email]]);
|
|
$this->action($this->author, 'published', [], 403);
|
|
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($this->cluster->id));
|
|
}
|
|
|
|
public function test_legacy_publication_survives_activation_and_disabling_cannot_bypass_workflow(): void
|
|
{
|
|
config(['editorial_workflow.cluster_competitiveness' => false]);
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft]);
|
|
$this->action($this->author, 'approved');
|
|
$service = app(ClusterCompetitiveness::class);
|
|
$public = $service->publicData($this->cluster->id);
|
|
config(['editorial_workflow.cluster_competitiveness' => true]);
|
|
$this->action($this->author, 'submitted', [], 409);
|
|
$this->action($this->author, 'saved', ['draft' => [...$this->draft, 'region' => 'Nova edição privada']]);
|
|
$this->assertSame($public, $service->publicData($this->cluster->id));
|
|
config(['editorial_workflow.cluster_competitiveness' => false]);
|
|
$this->action($this->author, 'approved', [], 503);
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft], 503);
|
|
$this->assertSame($public, $service->publicData($this->cluster->id));
|
|
}
|
|
|
|
public function test_permissions_mfa_and_incomplete_evidence_do_not_write(): void
|
|
{
|
|
$this->getJson($this->url())->assertUnauthorized();
|
|
$this->actingAs(User::factory()->create());
|
|
$this->postJson($this->url().'/published', [])->assertForbidden();
|
|
$this->actingAsWithMfa($this->author);
|
|
session()->forget('portal.mfa_at');
|
|
$this->postJson($this->url().'/saved', ['expected_version' => 0, 'draft' => $this->draft])->assertForbidden();
|
|
$draft = $this->draft;
|
|
$draft['forces']['buyers']['source'] = '';
|
|
$this->action($this->author, 'saved', ['draft' => $draft]);
|
|
$this->action($this->author, 'submitted', [], 422);
|
|
$this->action($this->author, 'restored', ['revision' => 999], 404);
|
|
$this->getJson($this->url())->assertJsonPath('version', 1)->assertJsonCount(1, 'history');
|
|
}
|
|
|
|
public function test_existing_management_entry_and_cluster_list_show_editorial_state_without_new_grants(): void
|
|
{
|
|
$this->action($this->author, 'saved', ['draft' => $this->draft]);
|
|
$this->get('/gestao-conteudos')->assertOk()->assertInertia(fn (Assert $p) => $p->where('access.clusters.index', '/management/clusters'));
|
|
$this->get('/management/clusters?q='.$this->cluster->code)->assertOk()->assertInertia(fn (Assert $p) => $p->where('records.data.0.editorial_state', 'draft')->where('records.data.0.status', 'planned'));
|
|
$other = User::factory()->create();
|
|
$this->assertArrayNotHasKey('clusters', app(ContentManagement::class)->access($other));
|
|
config(['editorial_workflow.cluster_competitiveness' => false]);
|
|
$this->assertArrayNotHasKey('clusters', app(ContentManagement::class)->access($this->author));
|
|
}
|
|
|
|
public function test_withdrawal_and_context_changes_do_not_cancel_or_trap_a_private_review(): void
|
|
{
|
|
$this->approved();
|
|
$this->action($this->reviewer, 'published');
|
|
$this->action($this->author, 'saved', ['draft' => [...$this->draft, 'region' => 'Revisão privada']]);
|
|
$this->action($this->author, 'submitted');
|
|
$this->action($this->reviewer, 'review_started');
|
|
$withdrawn = $this->action($this->reviewer, 'withdrawn');
|
|
$this->assertSame('in_review', $withdrawn['workflow']['state']);
|
|
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($this->cluster->id));
|
|
$this->cluster->update(['name' => 'Contexto alterado durante revisão']);
|
|
$this->action($this->reviewer, 'approved', [], 409);
|
|
$returned = $this->action($this->reviewer, 'returned');
|
|
$this->assertSame('returned', $returned['workflow']['state']);
|
|
$this->assertSame('Revisão privada', $returned['draft']['region']);
|
|
}
|
|
}
|