withoutVite(); Http::fake(); Mail::fake(); $this->admin = $this->actor(); $this->second = $this->actor(); $this->staff = $this->actor(); DB::table('portal_superuser_pair')->insert(['id' => 1, 'first_user_id' => $this->admin->id, 'second_user_id' => $this->second->id, 'approved_by' => $this->second->id, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); $this->directory = storage_path('framework/testing/support-profile-install-'.Str::uuid()); mkdir($this->directory, 0700, true); config(['portal_profiles.proof_directory' => $this->directory, 'portal.profile_access_enabled' => true, 'portal.managers' => [$this->admin->email, $this->staff->email], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $this->login($this->admin); } protected function tearDown(): void { foreach (glob($this->directory.'/*') ?: [] as $file) { unlink($file); }rmdir($this->directory); parent::tearDown(); } private function actor(): User { return User::factory()->create(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()]); } private function login(User $actor, bool $mfa = true): static { return $this->actingAs($actor)->withSession(['portal.mfa_actor' => $mfa ? $actor->id : null, 'portal.mfa_at' => $mfa ? time() : 0, 'portal.access_revision' => (int) $actor->fresh()->portal_access_revision]); } private function beforeSupport(bool $keepLedger = false): void { (require database_path('migrations/'.TechnicalSupportProfileSchema::MIGRATION.'.php'))->down(); if (! $keepLedger) { DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->delete(); } } /** Synthetic files validate the existing readiness contract; these are not real deployment backups. @return array */ private function proof(): array { file_put_contents($this->directory.'/dump', 'SYNTHETIC contract fixture, not a database dump.'); file_put_contents($this->directory.'/files', 'SYNTHETIC contract fixture, not a private-file archive.'); $proof = ['verified_at' => gmdate('c'), 'target' => DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port'), 'code_hash' => app(PortalProfileInstallation::class)->fingerprint(), 'restore_verified' => true, 'files_verified' => true, 'compatibility_verified' => true, 'qa_passed' => true, 'dump_path' => $this->directory.'/dump', 'dump_sha256' => hash_file('sha256', $this->directory.'/dump'), 'files_path' => $this->directory.'/files', 'files_sha256' => hash_file('sha256', $this->directory.'/files')]; $this->writeProof($proof); return $proof; } /** @param array $proof */ private function writeProof(array $proof): void { file_put_contents($this->directory.'/readiness.json', json_encode($proof, JSON_THROW_ON_ERROR)); } /** @return array */ private function input(): array { return ['hash' => $this->getJson(self::URL)->assertOk()->json('hash'), 'confirm' => true, 'reason' => 'SYNTHETIC scoped support profile upgrade in isolated QA.', 'idempotency_key' => (string) Str::uuid()]; } public function test_authenticated_upgrade_preserves_accounts_existing_authority_and_business_data_without_granting_support(): void { $this->beforeSupport(); $organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $organization, 'name' => 'SYNTHETIC preserved organization', 'organization_type' => 'private']); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $this->admin, ['user_id' => $this->staff->id, 'role' => 'promoter', 'organization_id' => $organization, 'opportunity_types' => ['investment_project']]); $id = (string) Str::uuid(); $opportunity = new Opportunity(['reference' => $id, 'slug' => $id, 'title' => 'PRIVATE SYNTHETIC PRESERVED INVESTMENT', 'status' => 'draft']); $opportunity->forceFill(['owner_user_id' => $this->staff->id, 'organization_id' => $organization, 'opportunity_category' => 'investment_project', 'management_version' => 3, 'workflow_context' => ['working_state' => 'draft', 'history' => ['SYNTHETIC previous version']]])->save(); $before = []; foreach (['users', 'organizations', 'portal_role_assignments', 'portal_superuser_pair', 'opportunities', 'tasks'] as $table) { $before[$table] = DB::table($table)->orderBy('id')->get()->toJson(); } $this->proof(); $this->getJson(self::URL)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertJsonPath('ready', true)->assertJsonPath('action', 'install')->assertJsonPath('schema.state', 'base'); $input = $this->input(); $result = $this->postJson(self::URL, $input)->assertOk()->assertJsonPath('installed', true)->assertJsonPath('changed', true)->assertJsonPath('privileges_granted', false)->assertJsonPath('mode', 'install')->json(); $this->postJson(self::URL, $input)->assertOk()->assertExactJson($result); $this->postJson(self::URL, [...$input, 'reason' => 'SYNTHETIC changed upgrade purpose'])->assertConflict(); foreach ($before as $table => $snapshot) { $this->assertSame($snapshot, DB::table($table)->orderBy('id')->get()->toJson()); } $this->assertTrue(app(OpportunityTechnicalSupport::class)->installed()); $this->assertFalse(app(OpportunityTechnicalSupport::class)->allows($this->staff)); $this->assertFalse(config('portal_profiles.authenticated_installation')); $this->assertSame(1, DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->count()); $this->assertSame(1, DB::table('audit_events')->where('action', 'technical-support-install')->count()); } public function test_already_installed_update_is_noop_and_receipts_are_visible_only_to_their_actor(): void { $input = $this->input(); $this->postJson(self::URL, $input)->assertOk()->assertJsonPath('changed', false)->assertJsonPath('mode', 'already_installed'); $this->getJson(self::URL)->assertOk()->assertJsonCount(1, 'commands')->assertJsonPath('commands.0.key', $input['idempotency_key'])->assertJsonMissingPath('commands.0.request_hash'); $this->login($this->second)->getJson(self::URL)->assertOk()->assertJsonCount(0, 'commands'); $this->login($this->admin, false)->postJson(self::URL, $input)->assertForbidden(); DB::table('portal_superuser_pair')->where('id', 1)->update(['first_user_id' => $this->staff->id]); $this->login($this->admin)->getJson(self::URL)->assertForbidden(); $this->postJson(self::URL, $input)->assertForbidden(); } public function test_final_audit_failure_rolls_back_role_constraint_and_migration_ledger_and_allows_explicit_retry(): void { $this->beforeSupport(); $this->proof(); $input = $this->input(); DB::statement("ALTER TABLE audit_events ADD CONSTRAINT qa_reject_support_install CHECK (source_system <> 'portal-profile-installation')"); $this->postJson(self::URL, $input)->assertStatus(503); $state = app(TechnicalSupportProfileSchema::class)->inspect(); $this->assertSame('base', $state['state']); $this->assertFalse($state['ledger']); $this->assertFalse(config('portal_profiles.authenticated_installation')); DB::statement('ALTER TABLE audit_events DROP CONSTRAINT qa_reject_support_install'); $this->postJson(self::URL, $input)->assertOk()->assertJsonPath('changed', true); } public function test_readiness_rejects_false_flags_malformed_values_expiry_future_dates_wrong_target_and_tampered_files(): void { $this->beforeSupport(); $valid = $this->proof(); $target = get_object_vars($valid['target']); $variants = [['restore_verified' => 'false'], ['qa_passed' => 1], ['verified_at' => []], ['verified_at' => gmdate('c', time() + 3600)], ['verified_at' => gmdate('c', time() - 172800)], ['dump_path' => []], ['target' => 'invalid'], ['target' => [...$target, 'port' => 5432]], ['code_hash' => str_repeat('0', 64)]]; foreach ($variants as $variant) { $this->writeProof([...$valid, ...$variant]); $this->getJson(self::URL)->assertOk()->assertJsonPath('ready', false); $this->postJson(self::URL, $this->input())->assertUnprocessable(); } $this->writeProof($valid); file_put_contents($this->directory.'/files', 'SYNTHETIC tampering'); $this->getJson(self::URL)->assertOk()->assertJsonPath('ready', false); $this->postJson(self::URL, $this->input())->assertUnprocessable(); $this->assertFalse(app(OpportunityTechnicalSupport::class)->installed()); } public function test_a_changed_valid_proof_invalidates_the_old_reviewed_plan_and_normalized_retry_keeps_the_same_receipt(): void { $this->beforeSupport(); $proof = $this->proof(); $stale = $this->input(); $this->writeProof([...$proof, 'snapshot_reference' => 'SYNTHETIC newer verified snapshot']); $this->getJson(self::URL)->assertJsonPath('ready', true); $this->postJson(self::URL, $stale)->assertConflict(); $input = $this->input(); $this->postJson(self::URL, [...$input, 'idempotency_key' => strtoupper($input['idempotency_key']), 'confirm' => 'yes', 'reason' => ' '.$input['reason'].' '])->assertOk(); $this->postJson(self::URL, $input)->assertOk(); $this->assertSame(1, DB::table('audit_events')->where('action', 'technical-support-install')->count()); } public function test_known_base_constraint_with_registered_migration_has_a_scoped_authenticated_repair(): void { $ledger = DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->first(); $this->beforeSupport(true); $this->assertFalse(app(OpportunityTechnicalSupport::class)->installed()); $this->getJson(self::URL)->assertOk()->assertJsonPath('consistent', false)->assertJsonPath('repairable', true)->assertJsonPath('ready', false); $this->postJson(self::URL, $this->input())->assertUnprocessable(); $this->proof(); $this->postJson(self::URL, $this->input())->assertOk()->assertJsonPath('mode', 'repair_known_base')->assertJsonPath('changed', true); $this->assertEquals($ledger, DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->first()); $this->assertTrue(app(OpportunityTechnicalSupport::class)->installed()); $this->assertSame(0, DB::table('portal_role_assignments')->count()); } public function test_unknown_or_weakened_constraints_are_not_overwritten_or_reported_as_installed(): void { $this->proof(); DB::statement("ALTER TABLE portal_role_assignments DROP CONSTRAINT portal_role_name, ADD CONSTRAINT portal_role_name CHECK (role IN ('promoter','validator','publisher','auditor','technical_support') OR TRUE)"); $fingerprint = app(TechnicalSupportProfileSchema::class)->inspect()['fingerprint']; $this->getJson(self::URL)->assertOk()->assertJsonPath('schema.state', 'unrecognised')->assertJsonPath('installed', false)->assertJsonPath('repairable', false); $this->postJson(self::URL, $this->input())->assertConflict(); $this->assertSame($fingerprint, app(TechnicalSupportProfileSchema::class)->inspect()['fingerprint']); DB::statement('ALTER TABLE portal_role_assignments DROP CONSTRAINT portal_role_name'); $this->postJson(self::URL, $this->input())->assertConflict(); } public function test_missing_ledger_for_existing_support_schema_does_not_implicitly_adopt_authority(): void { DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->delete(); $this->proof(); $this->getJson(self::URL)->assertOk()->assertJsonPath('schema.state', 'support')->assertJsonPath('installed', false)->assertJsonPath('repairable', false); $this->postJson(self::URL, $this->input())->assertConflict(); $this->assertFalse(DB::table('migrations')->where('migration', TechnicalSupportProfileSchema::MIGRATION)->exists()); } public function test_support_role_and_legacy_allowlist_do_not_grant_installation_authority_and_auditor_denial_prevails(): void { app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $this->admin, ['user_id' => $this->staff->id, 'role' => 'technical_support', 'opportunity_types' => ['investment_project']]); $this->login($this->staff)->getJson(self::URL)->assertForbidden(); $this->postJson(self::URL, ['hash' => str_repeat('a', 64), 'confirm' => true, 'reason' => 'SYNTHETIC forbidden installation', 'idempotency_key' => (string) Str::uuid()])->assertForbidden(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $this->second, ['user_id' => $this->admin->id, 'role' => 'auditor', 'opportunity_types' => []]); $this->login($this->admin)->getJson(self::URL)->assertForbidden(); $this->postJson(self::URL, [])->assertForbidden(); } public function test_upgrade_keeps_existing_dependency_and_historical_support_downgrade_controls(): void { $this->beforeSupport(); $this->proof(); DB::table('migrations')->where('migration', '2026_10_09_030000_extend_opportunity_resources')->delete(); $this->getJson(self::URL)->assertJsonPath('ready', false)->assertJsonPath('dependencies.resources_030000', false); $this->postJson(self::URL, $this->input())->assertUnprocessable(); DB::table('migrations')->insert(['migration' => '2026_10_09_030000_extend_opportunity_resources', 'batch' => 1]); $this->postJson(self::URL, $this->input())->assertOk(); $assignment = app(PortalProfileAdministration::class)->grant($this->admin, ['user_id' => $this->staff->id, 'role' => 'technical_support', 'opportunity_types' => ['investment_project'], 'reason' => 'SYNTHETIC explicit independent support assignment']); app(PortalProfileAdministration::class)->revoke($this->admin, $assignment, 'SYNTHETIC explicit removal; preserve the historical assignment'); try { (require database_path('migrations/'.TechnicalSupportProfileSchema::MIGRATION.'.php'))->down(); $this->fail('Historical support assignment must survive downgrade.'); } catch (\LogicException $error) { $this->assertStringContainsString('Preserve existing and historical', $error->getMessage()); } $this->assertTrue(app(OpportunityTechnicalSupport::class)->installed()); $this->assertSame(1, DB::table('portal_role_assignments')->count()); } }