withoutVite(); } private function manager(): User { $user = User::factory()->create(); config(['portal.managers' => [$user->email], 'portal.profile_access_enabled' => false]); $this->actingAs($user); return $user; } private function cluster(): EconomicCluster { return EconomicCluster::create(['code' => 'COMP-'.bin2hex(random_bytes(6)), 'name' => 'Cluster de teste de persistência', 'sector' => 'Energia', 'status' => 'planned']); } private function draft(EconomicCluster $cluster): array { $draft = ['version' => 1, 'clusterId' => $cluster->id, 'sector' => 'Energia', 'region' => 'Distrito de demonstração', 'forces' => []]; foreach (ClusterCompetitiveness::FORCES as $force) { $draft['forces'][$force] = ['pressure' => 'medium', 'evidence' => 'Observação de teste, não publicável como facto real', 'source' => 'Fonte de demonstração p. 2', 'period' => '2025', 'rationale' => 'Apreciação de teste', 'limitations' => 'Dados sintéticos', 'opportunity' => 'Hipótese de demonstração']; } return $draft; } private function url(EconomicCluster $cluster): string { return '/management/clusters/'.$cluster->id.'/competitiveness'; } public function test_save_recovers_from_database_across_requests_and_records_encrypted_history(): void { $actor = $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft]) ->assertOk()->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonPath('approved', null); $this->getJson($this->url($cluster))->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertJsonPath('draft', $draft)->assertJsonPath('history.0.actor', $actor->id)->assertJsonPath('history.0.action', 'saved'); $this->get('/management/clusters/'.$cluster->id.'/edit')->assertInertia(fn (AssertableInertia $page) => $page ->component('structures/editor')->where('competitiveness.version', 1)->where('competitiveness.draft.region', $draft['region'])); $event = DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->first(); $this->assertStringNotContainsString($draft['forces']['rivalry']['evidence'], $event->after_data); $this->assertSame('restricted', $event->data_classification); $this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id)); } public function test_validation_conflict_and_server_error_do_not_overwrite_the_saved_version(): void { $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk(); $changed = $draft; $changed['region'] = 'Outra região'; $this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $changed])->assertConflict(); $changed['forces']['buyers']['pressure'] = 'zero'; $this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable(); $changed = $draft; $changed['clusterId'] = $this->cluster()->id; $this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable(); $this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonCount(1, 'history'); } public function test_approval_pins_content_and_later_drafts_and_restoration_do_not_publish(): void { $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $url = $this->url($cluster); $this->postJson($url.'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk(); $this->postJson($url.'/approved', ['expected_version' => 1, 'reason' => 'Aprovação sintética em QA'])->assertOk()->assertJsonPath('version', 2); $changed = $draft; $changed['region'] = 'Rascunho privado posterior'; $this->postJson($url.'/saved', ['expected_version' => 2, 'draft' => $changed])->assertOk(); $public = app(ClusterCompetitiveness::class)->publicData($cluster->id); $this->assertSame($draft, $public['draft']); $this->assertSame(['draft', 'version', 'at'], array_keys($public)); $this->postJson($url.'/restored', ['expected_version' => 3, 'revision' => 1])->assertOk() ->assertJsonPath('version', 4)->assertJsonPath('draft', $draft)->assertJsonPath('approved.version', 2); $this->assertSame($public, app(ClusterCompetitiveness::class)->publicData($cluster->id)); $this->postJson($url.'/withdrawn', ['expected_version' => 4, 'reason' => 'Retirada sintética em QA'])->assertOk()->assertJsonCount(5, 'history'); $this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id)); $this->getJson($url)->assertJsonPath('draft', $draft); } public function test_incomplete_drafts_are_saved_but_not_approved_and_unknown_revisions_do_not_write(): void { $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $draft['forces']['buyers']['source'] = ''; $draft['forces']['entrants']['pressure'] = ''; $this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk(); $this->postJson($this->url($cluster).'/approved', ['expected_version' => 1, 'reason' => 'Pedido de aprovação QA'])->assertUnprocessable(); $this->postJson($this->url($cluster).'/restored', ['expected_version' => 1, 'revision' => 99])->assertNotFound(); $this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft.forces.buyers.source', ''); } public function test_saved_data_keeps_the_portable_file_contract_and_rejects_missing_forces(): void { $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $draft['version'] = '1'; $this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft]) ->assertOk()->assertJsonPath('draft.version', 1); unset($draft['forces']['suppliers']); $this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $draft])->assertUnprocessable(); $this->getJson($this->url($cluster))->assertJsonPath('version', 1); } public function test_anonymous_and_unauthorised_users_cannot_read_history_or_mutate(): void { $cluster = $this->cluster(); $url = $this->url($cluster); $this->getJson($url)->assertUnauthorized(); $this->postJson($url.'/saved', [])->assertUnauthorized(); $this->actingAs(User::factory()->create()); $this->getJson($url)->assertForbidden(); foreach (['saved', 'approved', 'withdrawn', 'restored'] as $action) { $this->postJson($url.'/'.$action, [])->assertForbidden(); } $this->assertSame(0, DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->count()); } public function test_public_dossier_returns_only_the_approved_projection_and_unpublished_dossiers_remain_hidden(): void { $actor = $this->manager(); $cluster = $this->cluster(); $draft = $this->draft($cluster); $dossier = InvestmentDossier::create(['cluster_id' => $cluster->id, 'title' => 'Ficha sintética QA', 'source' => 'Fonte sintética QA', 'content' => ['sector' => 'Energia'], 'publication_status' => 'published', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Aprovação sintética só na base QA', 'published_at' => now()]); $service = app(ClusterCompetitiveness::class); $service->change($cluster, $actor->id, 0, 'saved', ['draft' => $draft]); $this->app['auth']->forgetGuards(); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null)); $service->change($cluster, $actor->id, 1, 'approved', ['reason' => 'Aprovação privada de teste']); $draft['region'] = 'CONTEÚDO PRIVADO NÃO APROVADO'; $service->change($cluster, $actor->id, 2, 'saved', ['draft' => $draft]); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page ->where('competitiveness.version', 2)->where('competitiveness.draft.region', 'Distrito de demonstração') ->missing('competitiveness.actor')->missing('competitiveness.evidence')->missing('competitiveness.history')); $dossier->update(['publication_status' => 'draft']); $this->get('/carteira/'.$dossier->id)->assertNotFound(); $dossier->update(['publication_status' => 'published']); $service->change($cluster, $actor->id, 3, 'withdrawn', ['reason' => 'Retirada privada de teste']); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null)); } }