Files
Investidor-V2/tests/Feature/InvestmentTerritorialTraceabilityTest.php

109 lines
7.6 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\EconomicCluster;
use App\Models\InvestmentDossier;
use App\Models\Project;
use App\Models\User;
use App\Services\InvestmentTerritorialTraceability;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class InvestmentTerritorialTraceabilityTest extends TestCase
{
use RefreshDatabase;
private string $directory;
protected function setUp(): void
{
parent::setUp();
$this->directory = storage_path('framework/testing/territorial-'.Str::uuid());
mkdir($this->directory, 0700, true);
config(['investment.traceability_directory' => $this->directory]);
}
protected function tearDown(): void
{
foreach (glob($this->directory.'/*') ?: [] as $file) {
unlink($file);
}
rmdir($this->directory);
parent::tearDown();
}
private function fixture(): array
{
$project = Project::factory()->create();
$cluster = EconomicCluster::create(['code' => 'QA-TRACE', 'name' => 'Synthetic QA cluster', 'sector' => 'QA', 'status' => 'planned']);
$dossier = InvestmentDossier::create(['cluster_id' => $cluster->id, 'title' => 'QA synthetic source', 'source' => 'Synthetic fixture only', 'content' => [], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'approval_basis' => 'document_owner', 'approval_evidence' => 'QA synthetic disclosure']);
$id = (string) Str::uuid();
DB::table('cluster_projects')->insert(['id' => $id, 'cluster_id' => $cluster->id, 'project_id' => $project->id, 'role' => 'anchor', 'source_system' => 'QA']);
$row = DB::table('cluster_projects')->where('id', $id)->first();
$plan = ['schema' => 1, 'target' => ['database' => config('database.connections.pgsql.database'), 'port' => 55441], 'code_hash' => app(InvestmentTerritorialTraceability::class)->fingerprint(),
'rows' => [['kind' => 'economic_clusters', 'record_id' => $cluster->id, 'association' => $project->id, 'role' => 'context', 'public' => true, 'dossier_id' => $dossier->id, 'source_reference' => 'QA synthetic source', 'source_quote' => 'QA anchor', 'supports' => 'Existing membership only', 'traceability' => 'quote recovered', 'official_source_accepted' => false, 'existing_validation' => $row->validation_status]],
'guards' => [['table' => 'cluster_projects', 'id' => $id, 'expected' => ['version_no' => $row->version_no, 'validation_status' => $row->validation_status, 'source_reference' => null]]],
'updates' => [['table' => 'cluster_projects', 'id' => $id, 'values' => ['source_reference' => 'QA synthetic source', 'evidence_reference' => $dossier->id]]]];
$bytes = json_encode($plan, JSON_THROW_ON_ERROR);
file_put_contents($this->directory.'/territory-activation-plan.json', $bytes);
file_put_contents($this->directory.'/qa-backup-fixture', 'QA fixture, not active backup evidence');
$hash = hash('sha256', $bytes);
$proof = ['restore_verified' => true, 'reversal_verified' => true, 'target' => $plan['target'], 'plan_hash' => $hash, 'code_hash' => $plan['code_hash'], 'verified_at' => gmdate('c'), 'dump_path' => $this->directory.'/qa-backup-fixture', 'sha256' => hash_file('sha256', $this->directory.'/qa-backup-fixture')];
file_put_contents($this->directory.'/territory-backup-proof.json', json_encode($proof, JSON_THROW_ON_ERROR));
$manager = User::factory()->create(['email' => 'territory-manager@example.invalid']);
config(['portal.managers' => [$manager->email]]);
return compact('hash', 'manager', 'id', 'cluster', 'dossier', 'row');
}
public function test_authenticated_application_is_idempotent_and_reversible_without_revalidating_sources(): void
{
$f = $this->fixture();
$payload = ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false];
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', $payload)->assertOk()->assertJsonPath('changed', 1);
$this->postJson('/management/relationship-repair', $payload)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('changed', 0);
$service = app(InvestmentTerritorialTraceability::class);
$this->assertCount(1, $service->sources('economic_clusters', $f['cluster']->id));
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'validation_status' => $f['row']->validation_status, 'source_reference' => 'QA synthetic source']);
$f['dossier']->update(['publication_status' => 'draft']);
$this->assertSame([], $service->sources('economic_clusters', $f['cluster']->id));
$this->postJson('/management/relationship-repair', [...$payload, 'reverse' => true])->assertUnprocessable()->assertJsonValidationErrors(['reason', 'confirm_reverse']);
$this->postJson('/management/relationship-repair', [...$payload, 'reverse' => true, 'reason' => 'QA reversal preservation test', 'confirm_reverse' => true])->assertOk()->assertJsonPath('changed', 1);
$this->assertSame('QA reversal preservation test', app(InvestmentTerritorialTraceability::class)->preview()['history'][0]['reason']);
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'version_no' => $f['row']->version_no + 2, 'source_reference' => null, 'evidence_reference' => null, 'validation_status' => $f['row']->validation_status]);
$this->assertSame([], $service->sources('economic_clusters', $f['cluster']->id));
$this->assertSame(2, DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->count());
$this->postJson('/management/relationship-repair', $payload)->assertConflict();
}
public function test_permissions_and_conflicting_snapshot_prevent_writes_and_show_field_error(): void
{
$f = $this->fixture();
$payload = ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false];
$this->actingAs(User::factory()->create())->postJson('/management/relationship-repair', $payload)->assertForbidden();
DB::table('cluster_projects')->where('id', $f['id'])->increment('version_no');
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', $payload)->assertConflict();
$this->from('/management/relationship-repair')->post('/management/relationship-repair', $payload)->assertRedirect('/management/relationship-repair')->assertSessionHasErrors('operation');
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'source_reference' => null]);
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->count());
}
public function test_expired_backup_and_modified_code_proof_require_updated_plan(): void
{
$f = $this->fixture();
$proofFile = $this->directory.'/territory-backup-proof.json';
$proof = json_decode(file_get_contents($proofFile), true);
$proof['verified_at'] = gmdate('c', time() - 86401);
file_put_contents($proofFile, json_encode($proof));
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false])->assertUnprocessable();
$proof['verified_at'] = gmdate('c');
$proof['code_hash'] = 'wrong';
file_put_contents($proofFile, json_encode($proof));
$this->assertFalse(app(InvestmentTerritorialTraceability::class)->preview()['backup_ready']);
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'source_reference' => null]);
}
}