directory = storage_path('app/private/testing/geometry-import-'.Str::uuid()); mkdir($this->directory, 0700, true); $originals = storage_path('app/private/investmoz-v2/portal-audit'); foreach (['moz-adm1-ine-20190607.geojson', 'moz-adm1-ine-metadata.html'] as $file) { copy($originals.'/'.$file, $this->directory.'/'.$file); } $file = $this->directory.'/moz-adm1-ine-20190607.geojson'; config(['services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]); $scanner = app(MalwareScanner::class); if (! $scanner->health()['available']) { $this->markTestSkipped('Real local ClamD required; no fake clean evidence.'); } $scan = $scanner->scan($file, filesize($file), storage_path('app/private')); $this->assertSame('clean', $scan['result']); $scan['signatures_at'] = $scan['signatures_at']?->format(DATE_ATOM); file_put_contents($this->directory.'/geometry-source-scans.json', json_encode(['captured_at' => gmdate('c'), 'files' => [['file' => basename($file), 'sha256' => hash_file('sha256', $file), 'scan' => $scan]]], JSON_THROW_ON_ERROR)); $features = json_decode(file_get_contents($file), true)['features']; $country = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $country, 'code' => 'MZ', 'name' => 'Moçambique', 'location_type' => 'country', 'data_classification' => 'internal']); foreach (array_slice($features, 0, 2) as $f) { DB::table('geo_locations')->insert(['id' => (string) Str::uuid(), 'code' => (string) Str::uuid(), 'name' => $f['properties']['ADM1_PT'], 'location_type' => 'province', 'parent_id' => $country, 'data_classification' => 'internal']); } $foreignCountry = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $foreignCountry, 'code' => 'QA-FOREIGN', 'name' => 'Synthetic foreign QA only', 'location_type' => 'country', 'data_classification' => 'internal']); DB::table('geo_locations')->insert(['id' => (string) Str::uuid(), 'code' => (string) Str::uuid(), 'name' => $features[0]['properties']['ADM1_PT'], 'location_type' => 'province', 'parent_id' => $foreignCountry, 'data_classification' => 'internal']); $actor = User::factory()->create(); $actor->forceFill(['two_factor_secret' => Crypt::encryptString('QA-TEST-ONLY'), 'two_factor_confirmed_at' => now()])->save(); config(['portal.managers' => [$actor->email], 'geometry-import.enabled' => true, 'geometry-import.directory' => $this->directory]); $this->actingAs($actor); return $actor; } protected function tearDown(): void { if (isset($this->directory) && is_dir($this->directory)) { foreach (glob($this->directory.'/*') as $file) { unlink($file); } rmdir($this->directory); } parent::tearDown(); } private function input(array $plan): array { return ['level' => 1, 'hash' => $plan['hash'], 'idempotency_key' => (string) Str::uuid(), 'reason' => 'Ensaio autenticado de cartografia apenas em QA', 'confirm' => true]; } public function test_preview_apply_replay_reuse_and_reverse_preserve_history(): void { $actor = $this->setupImport(); $plan = $this->getJson('/management/territory-geometry-import?level=1')->assertOk()->assertJsonPath('expected', 11)->assertJsonPath('resolved', 2)->json(); $input = $this->input($plan); $result = $this->postJson('/management/territory-geometry-import', $input)->assertOk()->assertJsonPath('inserted', 2)->assertJsonPath('published', false)->json(); $this->postJson('/management/territory-geometry-import', $input)->assertOk()->assertExactJson($result); $this->postJson('/management/territory-geometry-import', [...$input, 'reason' => 'Chave com outro conteúdo: recusar sem escrever'])->assertConflict(); $this->assertSame(2, DB::table('geo_geometries')->count()); $reusePlan = $this->getJson('/management/territory-geometry-import?level=1')->json(); $reuseResponse = $this->postJson('/management/territory-geometry-import', $this->input($reusePlan)); $this->assertSame(200, $reuseResponse->status(), $reuseResponse->getContent()); $reuse = $reuseResponse->assertJsonPath('inserted', 0)->assertJsonPath('reused', 2)->json(); $reverse = ['idempotency_key' => (string) Str::uuid(), 'reason' => 'Reversão do lote QA mantendo todo o histórico', 'confirm' => true]; $this->postJson('/management/territory-geometry-import/'.$reuse['batch_id'].'/reverse', $reverse)->assertOk()->assertJsonPath('reversed', 0)->assertJsonPath('reused_preserved', 2); $this->assertSame(2, DB::table('geo_geometries')->whereNull('deleted_at')->count()); $url = '/management/territory-geometry-import/'.$result['batch_id'].'/reverse'; $reverse['idempotency_key'] = (string) Str::uuid(); $this->postJson($url, $reverse)->assertOk()->assertJsonPath('reversed', 2); $this->postJson($url, $reverse)->assertOk()->assertJsonPath('reversed', 2); $this->assertSame(0, DB::table('geo_geometries')->whereNull('deleted_at')->count()); $this->assertSame(2, DB::table('geo_geometries')->count()); $this->assertSame(4, DB::table('audit_events')->where('source_system', 'geometry-import')->where('actor_user_id', $actor->id)->count()); // Reversed identities stay reserved; do not silently resurrect or duplicate. $this->postJson('/management/territory-geometry-import', $this->input($this->getJson('/management/territory-geometry-import?level=1')->json()))->assertConflict(); } public function test_stale_preview_changed_geometry_mfa_and_foreign_actor_are_rejected(): void { $actor = $this->setupImport(); $plan = $this->getJson('/management/territory-geometry-import?level=1')->json(); DB::table('geo_locations')->where('name', $plan['rows'][0]['name'])->update(['name' => 'Changed QA only']); $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertConflict(); $plan = $this->getJson('/management/territory-geometry-import?level=1')->json(); $result = $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertOk()->assertJsonPath('inserted', 1)->json(); DB::table('geo_geometries')->where('id', $result['created'][0]['id'])->update(['data_classification' => 'public']); $this->postJson('/management/territory-geometry-import/'.$result['batch_id'].'/reverse', ['idempotency_key' => (string) Str::uuid(), 'reason' => 'Recusar reversão de geometria posteriormente publicada', 'confirm' => true])->assertConflict(); $actor->forceFill(['two_factor_confirmed_at' => null])->save(); $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertForbidden(); $this->actingAs(User::factory()->create())->getJson('/management/territory-geometry-import?level=1')->assertForbidden(); $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertForbidden(); } public function test_expired_scan_and_non_qa_target_cannot_write(): void { $this->setupImport(); $path = $this->directory.'/geometry-source-scans.json'; $proof = json_decode(file_get_contents($path), true); $proof['captured_at'] = '2000-01-01T00:00:00Z'; file_put_contents($path, json_encode($proof)); $plan = $this->getJson('/management/territory-geometry-import?level=1')->assertOk()->assertJsonPath('scan_ready', false)->json(); $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertStatus(423); $this->app->detectEnvironment(fn () => 'production'); try { app(TerritoryGeometryImport::class)->apply(auth()->user(), $this->input($plan)); $this->fail('An active target cannot bypass the backup and evidence guard.'); } catch (HttpException $e) { $this->assertSame(422, $e->getStatusCode()); } finally { $this->app->detectEnvironment(fn () => 'testing'); } $this->assertSame(0, DB::table('geo_geometries')->count()); } public function test_reversal_refuses_a_real_foreign_key_consumer_and_is_atomic(): void { $this->setupImport(); $plan = $this->getJson('/management/territory-geometry-import?level=1')->json(); $result = $this->postJson('/management/territory-geometry-import', $this->input($plan))->assertOk()->json(); // Transactional QA-only fixture, rolled back by RefreshDatabase; never a product migration. DB::statement('CREATE TABLE geometry_import_qa_consumer (geometry_id uuid REFERENCES geo_geometries(id))'); DB::table('geometry_import_qa_consumer')->insert(['geometry_id' => $result['created'][1]['id']]); $this->postJson('/management/territory-geometry-import/'.$result['batch_id'].'/reverse', ['idempotency_key' => (string) Str::uuid(), 'reason' => 'Reversão deve recusar qualquer consumidor ligado ao lote', 'confirm' => true])->assertConflict(); $this->assertSame(2, DB::table('geo_geometries')->whereNull('deleted_at')->count()); $this->assertSame(0, DB::table('audit_events')->where('action', 'geometry_import_reversed')->count()); } }