"""Round-trip / migrate / sanitize tests for config.Settings.""" from __future__ import annotations import json import unittest from dataclasses import asdict from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch from proxy_chain_manager import config class TestSanitize(unittest.TestCase): def test_clamps_chain_length(self) -> None: s = config.Settings() s.chain_length = 99 s, changed = config.sanitize_settings(s) self.assertTrue(changed) self.assertEqual(s.chain_length, 8) def test_chain_length_string_resets_to_default(self) -> None: s = config.Settings() s.chain_length = "not a number" # type: ignore[assignment] s, changed = config.sanitize_settings(s) self.assertTrue(changed) self.assertEqual(s.chain_length, 3) def test_local_port_clamped(self) -> None: s = config.Settings() s.local_port = 99999 s, changed = config.sanitize_settings(s) self.assertTrue(changed) self.assertLessEqual(s.local_port, 65535) def test_http_source_url_rejected(self) -> None: # _is_safe_https_url should strip plaintext HTTP sources. s = config.Settings() s.sources = ["http://insecure.example/list.json"] s, _ = config.sanitize_settings(s) self.assertTrue(all(u.startswith("https://") for u in s.sources)) def test_rfc1918_source_url_rejected(self) -> None: s = config.Settings() s.sources = [ "https://192.168.1.1/list.json", "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/http/data.json", ] s, _ = config.sanitize_settings(s) for u in s.sources: # rfc1918 host must be gone. self.assertNotIn("192.168.", u) class TestMigrate(unittest.TestCase): def test_unversioned_dict_gets_versioned(self) -> None: raw = {"chain_length": 4} out = config.migrate(raw) self.assertEqual(out["settings_version"], config.SETTINGS_SCHEMA_VERSION) self.assertEqual(out["chain_length"], 4) def test_already_current_passthrough(self) -> None: raw = {"settings_version": config.SETTINGS_SCHEMA_VERSION, "chain_length": 4} out = config.migrate(raw) self.assertEqual(out["settings_version"], config.SETTINGS_SCHEMA_VERSION) class TestLoadSaveRoundTrip(unittest.TestCase): def test_save_then_load_preserves_values(self) -> None: with TemporaryDirectory() as tmp: tmp_path = Path(tmp) with patch.object(config, "app_data_dir", return_value=tmp_path): s = config.Settings() s.chain_length = 5 s.local_port = 19191 s.kill_switch_enabled = False config.save_settings(s) loaded = config.load_settings() self.assertEqual(loaded.chain_length, 5) self.assertEqual(loaded.local_port, 19191) self.assertFalse(loaded.kill_switch_enabled) def test_corrupt_settings_backed_up_and_defaults_returned(self) -> None: with TemporaryDirectory() as tmp: tmp_path = Path(tmp) (tmp_path / "settings.json").write_text("{ not json", encoding="utf-8") with patch.object(config, "app_data_dir", return_value=tmp_path): loaded = config.load_settings() self.assertEqual(loaded.chain_length, config.Settings().chain_length) self.assertTrue((tmp_path / "settings.json.corrupt").is_file()) def test_save_writes_backup_before_overwriting(self) -> None: with TemporaryDirectory() as tmp: tmp_path = Path(tmp) with patch.object(config, "app_data_dir", return_value=tmp_path): config.save_settings(config.Settings()) # Mutate + save again — .bak should now exist. s = config.load_settings() s.chain_length = 7 config.save_settings(s) self.assertTrue((tmp_path / "settings.json.bak").is_file()) if __name__ == "__main__": unittest.main()