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