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']); } }