From ae200209e2a866b9de027839e171a8bc616d31ac Mon Sep 17 00:00:00 2001 From: aadrian Date: Mon, 5 Oct 2026 09:37:00 +0200 Subject: [PATCH 1/2] ui, tests: isolate HOME and XDG dirs from the real installation The preferences tests delete ~/.config/opensnitch/settings.conf and write it through QSettings, and pressing Apply calls Autostart.enable(), which can create, remove or hide ~/.config/autostart/opensnitch_ui.desktop. Running the suite on a machine with OpenSnitch installed therefore replaced the user's real UI settings. Redirect HOME and the XDG dirs to a temporary directory before opensnitch is imported (utils/xdg.py caches the paths at import time), and add a session fixture that fails if the paths resolve outside of it or if the real settings.conf or autostart file changed during the run. --- ui/tests/conftest.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ui/tests/conftest.py b/ui/tests/conftest.py index 5dd29006b3..9930e138c2 100644 --- a/ui/tests/conftest.py +++ b/ui/tests/conftest.py @@ -10,6 +10,51 @@ # Global flag to track initialization _initialized = False +# Isolation: the tests write QSettings (~/.config/opensnitch/settings.conf), +# delete settings.conf, and toggle the autostart .desktop file. Redirect HOME +# and the XDG dirs to a throwaway directory *before* opensnitch or Qt cache +# any path, so a test run never touches an installed OpenSnitch UI. +import hashlib +import os +import pwd +import shutil +import sys +import tempfile + +if any(m == "opensnitch" or m.startswith("opensnitch.") for m in sys.modules): + raise RuntimeError("opensnitch was imported before tests/conftest.py could isolate HOME; refusing to run") + +_REAL_HOME = pwd.getpwuid(os.getuid()).pw_dir +_SANDBOX = tempfile.mkdtemp(prefix="opensnitch-ui-tests-") +for _var, _sub in ( + ("HOME", ""), + ("XDG_CONFIG_HOME", ".config"), + ("XDG_DATA_HOME", ".local/share"), + ("XDG_CACHE_HOME", ".cache"), + ("XDG_STATE_HOME", ".local/state"), + ("XDG_RUNTIME_DIR", "run")): + _path = os.path.join(_SANDBOX, _sub) + os.makedirs(_path, mode=0o700, exist_ok=True) + os.environ[_var] = _path +# no desktop notifications or other D-Bus traffic to the real session +os.environ.pop("DBUS_SESSION_BUS_ADDRESS", None) + +# files of a real installation that a test run must never modify +_REAL_FILES = [ + os.path.join(_REAL_HOME, ".config/opensnitch/settings.conf"), + os.path.join(_REAL_HOME, ".config/autostart/opensnitch_ui.desktop"), +] + +def _snapshot_real_files(): + snap = {} + for path in _REAL_FILES: + try: + with open(path, "rb") as f: + snap[path] = hashlib.sha256(f.read()).hexdigest() + except FileNotFoundError: + snap[path] = None + return snap + def init_test_environment(): """Initialize database and config after QApplication exists.""" global _initialized @@ -35,6 +80,27 @@ def init_test_environment(): _initialized = True +@pytest.fixture(scope="session", autouse=True) +def isolated_home(): + """Fail loudly if anything resolves outside the sandbox, or if the + real installation's files changed during the run.""" + try: + from PyQt6 import QtCore + from opensnitch.utils import xdg + except ImportError: + # tests that run without Qt: only the check of the real files applies + pass + else: + settings_file = QtCore.QSettings("opensnitch", "settings").fileName() + for path in (settings_file, xdg.xdg_config_home, xdg.xdg_runtime_dir, xdg.Autostart().userAutostart): + assert path.startswith(_SANDBOX + os.sep), f"not isolated: {path} is outside {_SANDBOX}" + + before = _snapshot_real_files() + yield _SANDBOX + after = _snapshot_real_files() + shutil.rmtree(_SANDBOX, ignore_errors=True) + assert before == after, f"test run modified the real installation: {before} -> {after}" + @pytest.fixture(scope="session") def qapp(): """Create QApplication for the entire test session.""" From beb850527a43eccafc67037bac7792b7c67eecba Mon Sep 17 00:00:00 2001 From: aadrian Date: Mon, 5 Oct 2026 09:47:17 +0200 Subject: [PATCH 2/2] ui, tests: fixed preferences tests broken by renames - the columns of the events view are saved per table (STATS_SHOW_COLUMNS + "_connections"). - the preferences sections are in stackedWidget, and the update flag is node_needs_update. - the "deny" default action is now "drop" (ACTION_DROP_IDX). - test_cancel_discards_changes: select an action different from the saved one, otherwise the test does not check anything. --- ui/tests/dialogs/test_preferences.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ui/tests/dialogs/test_preferences.py b/ui/tests/dialogs/test_preferences.py index f44cace44c..cf6fb68898 100644 --- a/ui/tests/dialogs/test_preferences.py +++ b/ui/tests/dialogs/test_preferences.py @@ -65,7 +65,7 @@ def test_save_ui_settings(self, qtbot): self.run(qtbot) assert self.prefs.cfgMgr.getBool(self.prefs.cfgMgr.DEFAULT_IGNORE_RULES) == True and self.prefs.cfgMgr.getInt(self.prefs.cfgMgr.DEFAULT_IGNORE_TEMPORARY_RULES) == 1 - cols = self.prefs.cfgMgr.getSettings(Config.STATS_SHOW_COLUMNS) + cols = self.prefs.cfgMgr.getSettings(Config.STATS_SHOW_COLUMNS + "_connections") # Column indices changed since original test - just verify columns are saved assert cols is not None and len(cols) > 0 @@ -76,8 +76,8 @@ def test_save_node_settings(self, qtbot, capsys): self.prefs.checkNodeLogUTC.setChecked(False) self.prefs.checkNodeLogMicro.setChecked(True) self.prefs.checkInterceptUnknown.setChecked(True) - self.prefs.tabWidget.setCurrentIndex(self.prefs.TAB_NODES) - self.prefs._node_needs_update = True + self.prefs.stackedWidget.setCurrentIndex(self.prefs.TAB_NODES) + self.prefs.node_needs_update = True self.run(qtbot) @@ -161,8 +161,9 @@ def test_cancel_discards_changes(self, qtbot): original_action = self.prefs.cfgMgr.getInt(self.prefs.cfgMgr.DEFAULT_ACTION_KEY) original_timeout = self.prefs.cfgMgr.getInt(self.prefs.cfgMgr.DEFAULT_TIMEOUT_KEY, 15) - # Make changes - self.prefs.comboUIAction.setCurrentIndex(Config.ACTION_DENY_IDX) + # Make changes (to an action other than the saved one) + new_action = Config.ACTION_REJECT_IDX if original_action != Config.ACTION_REJECT_IDX else Config.ACTION_ALLOW_IDX + self.prefs.comboUIAction.setCurrentIndex(new_action) self.prefs.spinUITimeout.setValue(99) # This test needs exec() to verify cancel closes dialog without saving. @@ -348,16 +349,16 @@ def test_settings_persistence_reopen(self, qtbot): assert prefs2.comboUIAction.currentIndex() == Config.ACTION_ALLOW_IDX assert prefs2.spinUITimeout.value() == 45 - def test_action_deny_setting(self, qtbot): - """Test saving deny action.""" + def test_action_drop_setting(self, qtbot): + """Test saving drop action.""" qtbot.addWidget(self.prefs) - self.prefs.comboUIAction.setCurrentIndex(Config.ACTION_DENY_IDX) + self.prefs.comboUIAction.setCurrentIndex(Config.ACTION_DROP_IDX) self.run(qtbot) - assert self.prefs.cfgMgr.getInt(self.prefs.cfgMgr.DEFAULT_ACTION_KEY) == Config.ACTION_DENY_IDX - assert self.prefs.comboUIAction.currentText() == Config.ACTION_DENY + assert self.prefs.cfgMgr.getInt(self.prefs.cfgMgr.DEFAULT_ACTION_KEY) == Config.ACTION_DROP_IDX + assert self.prefs.comboUIAction.currentText() == Config.ACTION_DROP def test_action_reject_setting(self, qtbot): """Test saving reject action."""