feat(clusters): persist versioned competitiveness and approved public analysis
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\EconomicCluster;
|
||||
use App\Models\InvestmentDossier;
|
||||
use App\Models\User;
|
||||
use App\Services\ClusterCompetitiveness;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClusterCompetitivenessTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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_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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user