"""Unit tests for migrate_profiles.py migration helper.""" import os import sys import json import unittest import tempfile import shutil from unittest.mock import patch from io import StringIO # Add project root to sys.path PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) import migrate_profiles class TestMigrateProfiles(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.mkdtemp() self.v1_content = { "version": 1, "profiles": { "prgs": { "base_url": "https://gitea.prgs.cc", "username": "jcwalker3", "auth": { "type": "keychain", "id": "prgs-gitea-token" }, "default_owner": "Scaled-Tech-Consulting", "execution_profile": "personal-prgs" }, "mdcps": { "base_url": "https://gitea.dadeschools.net", "username": "913443", "auth": { "type": "keychain", "id": "mdcps-gitea-token" }, "default_owner": "Contractor", "execution_profile": "mdcps" }, "prgs-reviewer": { "base_url": "https://gitea.prgs.cc", "username": "sysadmin", "auth": { "type": "keychain", "id": "prgs-gitea-reviewer-token" }, "default_owner": "Scaled-Tech-Consulting", "execution_profile": "prgs-reviewer" } } } self.input_file = os.path.join(self.temp_dir, "profiles.json") with open(self.input_file, "w") as f: json.dump(self.v1_content, f) def tearDown(self): shutil.rmtree(self.temp_dir) def test_migration_logic(self): """Test the structural transformation and capability mapping.""" v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content) self.assertEqual(v2_data["version"], 2) # Check environment structure envs = v2_data["environments"] self.assertIn("prgs", envs) self.assertIn("mdcps", envs) # Check service and identity structure prgs_gitea = envs["prgs"]["services"]["gitea"] self.assertEqual(prgs_gitea["base_url"], "https://gitea.prgs.cc") self.assertEqual(prgs_gitea["default_owner"], "Scaled-Tech-Consulting") author = prgs_gitea["identities"]["author"] self.assertEqual(author["role"], "author") self.assertEqual(author["username"], "jcwalker3") self.assertEqual(author["auth"]["id"], "prgs-gitea-token") self.assertIn("push", author["allowed_operations"]) reviewer = prgs_gitea["identities"]["reviewer"] self.assertEqual(reviewer["role"], "reviewer") self.assertEqual(reviewer["username"], "sysadmin") self.assertEqual(reviewer["auth"]["id"], "prgs-gitea-reviewer-token") self.assertIn("merge", reviewer["allowed_operations"]) def test_alias_generation(self): """Test that aliases are correctly generated to support old profile names.""" v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content) aliases = v2_data["aliases"] self.assertEqual(aliases["prgs"], "prgs.gitea.author") self.assertEqual(aliases["prgs-author"], "prgs.gitea.author") self.assertEqual(aliases["prgs-reviewer"], "prgs.gitea.reviewer") self.assertEqual(aliases["mdcps"], "mdcps.gitea.author") def test_no_secret_behavior(self): """Ensure secrets are never extracted, printed, or processed.""" v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content) # Check that auth structures only contain keychain references, not credentials for env in v2_data["environments"].values(): for svc in env["services"].values(): for ident in svc["identities"].values(): auth = ident["auth"] self.assertEqual(auth["type"], "keychain") self.assertIn("id", auth) self.assertNotIn("token", auth) self.assertNotIn("password", auth) def test_validation(self): """Test that the generated v2 configuration validates against Gitea-Tools v2 loader.""" v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content) self.assertTrue(migrate_profiles.validate_v2_data(v2_data)) @patch("sys.stdout", new_callable=StringIO) def test_dry_run_default(self, mock_stdout): """Verify that running without -w prints generated config without modifying files.""" output_file = os.path.join(self.temp_dir, "migrated_dry.json") test_args = [ "migrate_profiles.py", "-i", self.input_file, "-o", output_file ] with patch.object(sys, "argv", test_args): with self.assertRaises(SystemExit) as cm: migrate_profiles.main() self.assertEqual(cm.exception.code, 0) self.assertFalse(os.path.exists(output_file)) self.assertFalse(os.path.exists(f"{self.input_file}.bak")) stdout_output = mock_stdout.getvalue() self.assertIn("DRY-RUN MODE", stdout_output) self.assertIn("version", stdout_output) self.assertIn("environments", stdout_output) def test_write_mode_and_backup(self): """Verify that write mode creates a backup and correctly saves the validated config.""" output_file = os.path.join(self.temp_dir, "migrated.json") backup_file = os.path.join(self.temp_dir, "profiles_backup.json.bak") test_args = [ "migrate_profiles.py", "-i", self.input_file, "-o", output_file, "--backup", backup_file, "-w" ] with patch.object(sys, "argv", test_args): migrate_profiles.main() # Verify backup exists and matches original v1 config self.assertTrue(os.path.exists(backup_file)) with open(backup_file, "r") as f: backup_data = json.load(f) self.assertEqual(backup_data["version"], 1) self.assertIn("prgs", backup_data["profiles"]) # Verify migrated v2 config exists and validates self.assertTrue(os.path.exists(output_file)) with open(output_file, "r") as f: v2_data = json.load(f) self.assertEqual(v2_data["version"], 2) self.assertIn("environments", v2_data) self.assertEqual(v2_data["aliases"]["prgs"], "prgs.gitea.author") def test_malformed_input_fails_safely(self): """Test that malformed JSON or invalid version numbers cause a clean exit with code 1.""" bad_json_file = os.path.join(self.temp_dir, "bad.json") with open(bad_json_file, "w") as f: f.write("{invalid-json}") test_args = ["migrate_profiles.py", "-i", bad_json_file] with patch.object(sys, "argv", test_args): with self.assertRaises(SystemExit) as cm: migrate_profiles.main() self.assertEqual(cm.exception.code, 1) bad_version_file = os.path.join(self.temp_dir, "bad_version.json") with open(bad_version_file, "w") as f: json.dump({"version": 3, "profiles": {}}, f) test_args = ["migrate_profiles.py", "-i", bad_version_file] with patch.object(sys, "argv", test_args): with self.assertRaises(SystemExit) as cm: migrate_profiles.main() self.assertEqual(cm.exception.code, 1) if __name__ == "__main__": unittest.main()