diff --git a/muxplex/settings.py b/muxplex/settings.py index eceae8a..27ee91d 100644 --- a/muxplex/settings.py +++ b/muxplex/settings.py @@ -4,6 +4,7 @@ Server-side settings management for muxplex. Settings are stored at ~/.config/muxplex/settings.json. """ +import copy import json from pathlib import Path @@ -25,16 +26,14 @@ def load_settings() -> dict: Returns DEFAULT_SETTINGS if the file does not exist or contains corrupt JSON. Unknown keys in the file are ignored. """ - result = DEFAULT_SETTINGS.copy() + result = copy.deepcopy(DEFAULT_SETTINGS) try: text = SETTINGS_PATH.read_text() data = json.loads(text) for key in DEFAULT_SETTINGS: if key in data: result[key] = data[key] - except FileNotFoundError: - pass - except (json.JSONDecodeError, Exception): + except (FileNotFoundError, json.JSONDecodeError): pass return result @@ -45,7 +44,7 @@ def save_settings(data: dict) -> None: Creates parent directories as needed. Writes JSON with indent=2 and a trailing newline. """ - merged = DEFAULT_SETTINGS.copy() + merged = copy.deepcopy(DEFAULT_SETTINGS) for key in DEFAULT_SETTINGS: if key in data: merged[key] = data[key] diff --git a/muxplex/tests/test_settings.py b/muxplex/tests/test_settings.py index 28d1d2b..de7780b 100644 --- a/muxplex/tests/test_settings.py +++ b/muxplex/tests/test_settings.py @@ -116,3 +116,29 @@ def test_patch_settings_ignores_unknown_keys(tmp_path, monkeypatch): assert "unknown_key" not in result assert result["sort_order"] == "alpha" + + +def test_load_does_not_mutate_default_settings(): + """Mutating the list returned by load_settings() must not corrupt DEFAULT_SETTINGS.""" + result = load_settings() + # Mutate the returned hidden_sessions list + result["hidden_sessions"].append("leaked_session") + + # DEFAULT_SETTINGS must be unchanged + assert DEFAULT_SETTINGS["hidden_sessions"] == [] + + # A second load must still return the clean default + result2 = load_settings() + assert result2["hidden_sessions"] == [] + + +def test_load_propagates_non_json_errors(monkeypatch): + """load_settings() must not swallow unexpected errors (e.g. PermissionError).""" + from unittest.mock import MagicMock + + mock_path = MagicMock() + mock_path.read_text.side_effect = PermissionError("no read permission") + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", mock_path) + + with pytest.raises(PermissionError): + load_settings()