# SPDX-License-Identifier: GPL-3.0-or-later """Functional tests for the Qt GUI frontend using the offscreen platform.""" import os os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") import unittest from unittest.mock import patch from enodia_sentinel.config import Config from enodia_sentinel.gui import qt_app from enodia_sentinel.gui.model import GuiModel from enodia_sentinel.tray.state import parse_status try: from PySide6 import QtCore, QtGui, QtWidgets _HAS_QT = True except ImportError: # pragma: no cover - optional extra _HAS_QT = False @unittest.skipUnless(_HAS_QT, "PySide6 not installed") class TestQtGui(unittest.TestCase): @classmethod def setUpClass(cls): cls.app = QtWidgets.QApplication.instance() if cls.app is None: cls.app = QtWidgets.QApplication([]) def _window(self): cfg = Config.load(None) return qt_app._MainWindow(cfg) def _destroy(self, win): win.close() win.deleteLater() self.app.processEvents() def _model(self): return GuiModel( header=parse_status({ "running": True, "total_alerts": 2, "counts": {"HIGH": 1, "MEDIUM": 1}, "host": "testhost", "version": "0.7.0", }), status_lines=("Host: testhost", "Daemon: running"), alerts=( ("2026-07-10T12:00:00", "HIGH", 100001, "reverse_shell", "bash"), ("2026-07-10T12:01:00", "MEDIUM", 100002, "ld_preload", "libevil"), ), incidents=( ("INC-1", "HIGH", "2026-07-10T12:00:00", "2026-07-10T12:05:00", 2, "reverse_shell"), ), posture=(("HIGH", "ssh_root_login", "PermitRootLogin yes"),), integrity_lines=("watchdog.ok: True",), plans=(("INC-1", 2, "freeze and contain"),), plan_details={"INC-1": "Incident: INC-1\nSummary: freeze"}, events=("event one", "event two"), ) def test_window_title(self): win = self._window() self.assertEqual(win.windowTitle(), "Enodia Sentinel") self._destroy(win) def test_refresh_renders_model(self): win = self._window() model = self._model() win._on_refreshed(model) self.assertIn("alerts", win.summary_label.text()) self.assertIn("testhost", win.status_edit.toPlainText()) self.assertIn("Daemon: running", win.status_edit.toPlainText()) self.assertEqual(win.alerts_table.rowCount(), 2) self.assertEqual(win.incidents_table.rowCount(), 1) self.assertEqual(win.posture_table.rowCount(), 1) self.assertEqual(win.plans_table.rowCount(), 1) self.assertIn("event two", win.events_edit.toPlainText()) self.assertTrue(win.last_refreshed_label.text().startswith("refreshed")) self._destroy(win) def test_severity_coloring(self): table = QtWidgets.QTableWidget() table.setColumnCount(2) table.setHorizontalHeaderLabels(["Severity", "Signature"]) qt_app._MainWindow._populate_table(None, table, [ ("CRITICAL", "rootkit"), ("HIGH", "reverse_shell"), ("MEDIUM", "ld_preload"), ]) critical = table.item(0, 0) self.assertEqual( critical.background().color().name(), "#ff4444" ) high = table.item(1, 0) self.assertEqual( high.background().color().name(), "#ffaa00" ) def test_plan_selection_shows_detail(self): win = self._window() win._on_refreshed(self._model()) win.plans_table.selectRow(0) self.app.processEvents() text = win.plan_edit.toPlainText() self.assertIn("INC-1", text) self.assertIn("freeze", text) self._destroy(win) def test_shortcuts_exist(self): win = self._window() shortcuts = [s.key().toString() for s in win.findChildren(QtGui.QShortcut)] self.assertIn("Ctrl+R", shortcuts) self.assertIn("Ctrl+Q", shortcuts) self._destroy(win) class TestQtMainWithoutQt(unittest.TestCase): def test_main_exits_gracefully_without_qt(self): with patch.object(qt_app, "_HAS_QT", False): with self.assertRaises(SystemExit) as cm: qt_app.main([]) self.assertIn("PySide6", str(cm.exception))