A Python port of the Invisible Internet Project (I2P)
1"""Tests for desktop tray runner — pystray integration."""
2
3import sys
4from unittest.mock import MagicMock, patch
5
6import pytest
7
8from i2p_apps.desktop.tray import TrayConfig, TrayMenu
9
10
11class TestTrayRunnerImport:
12 """Test that runner handles missing pystray gracefully."""
13
14 def test_runner_raises_without_pystray(self):
15 """TrayRunner should raise RuntimeError if pystray is not installed."""
16 with patch.dict(sys.modules, {"pystray": None, "PIL": None, "PIL.Image": None}):
17 # Force re-import with pystray unavailable
18 import importlib
19 import i2p_apps.desktop.runner as runner_mod
20
21 # Patch the module-level flag
22 orig = runner_mod._HAS_PYSTRAY
23 runner_mod._HAS_PYSTRAY = False
24 try:
25 with pytest.raises(RuntimeError, match="pystray is required"):
26 runner_mod.TrayRunner()
27 finally:
28 runner_mod._HAS_PYSTRAY = orig
29
30
31class TestTrayRunnerActions:
32 """Test action dispatch (mocked pystray)."""
33
34 def _make_runner(self):
35 """Create a TrayRunner with pystray mocked."""
36 from i2p_apps.desktop.runner import TrayRunner
37
38 # If pystray is not installed, mock the flag
39 import i2p_apps.desktop.runner as mod
40
41 orig = mod._HAS_PYSTRAY
42 mod._HAS_PYSTRAY = True
43 try:
44 runner = object.__new__(TrayRunner)
45 runner._config = TrayConfig()
46 runner._menu = TrayMenu()
47 runner._icon = MagicMock()
48 return runner
49 finally:
50 mod._HAS_PYSTRAY = orig
51
52 def test_open_console_action(self):
53 """open_console should call webbrowser.open."""
54 runner = self._make_runner()
55 with patch("i2p_apps.desktop.runner.webbrowser") as mock_wb:
56 runner._dispatch_action("open_console")
57 mock_wb.open.assert_called_once_with("http://127.0.0.1:7657")
58
59 def test_quit_action_stops_icon(self):
60 """quit action should stop the tray icon."""
61 runner = self._make_runner()
62 runner._dispatch_action("quit")
63 runner._icon.stop.assert_called_once()
64
65 def test_shutdown_signals_router(self):
66 """shutdown_graceful should POST to router API."""
67 runner = self._make_runner()
68 with patch("i2p_apps.desktop.runner.TrayRunner._signal_router") as mock_sig:
69 runner._dispatch_action("shutdown_graceful")
70 mock_sig.assert_called_once_with("shutdown")
71
72 def test_restart_signals_router(self):
73 """restart should POST to router API."""
74 runner = self._make_runner()
75 with patch("i2p_apps.desktop.runner.TrayRunner._signal_router") as mock_sig:
76 runner._dispatch_action("restart")
77 mock_sig.assert_called_once_with("restart")
78
79
80class TestWindowsService:
81 """Test Windows service wrapper can be imported on all platforms."""
82
83 def test_import_service_module(self):
84 """The service module should be importable even without pywin32."""
85 from pathlib import Path
86 import importlib.util
87
88 # Find service.py relative to this test file's repo root
89 repo_root = Path(__file__).resolve().parents[2]
90 service_path = repo_root / "packaging" / "windows" / "service.py"
91 if not service_path.exists():
92 pytest.skip("packaging/windows/service.py not found")
93
94 sys.path.insert(0, str(service_path.parent))
95 try:
96 spec = importlib.util.spec_from_file_location("service", str(service_path))
97 mod = importlib.util.module_from_spec(spec)
98 spec.loader.exec_module(mod)
99
100 # On non-Windows, _HAS_PYWIN32 should be False
101 assert hasattr(mod, "_HAS_PYWIN32")
102 assert hasattr(mod, "_run_router")
103 assert hasattr(mod, "cli")
104 finally:
105 sys.path.pop(0)
106
107
108class TestIconImage:
109 """Test icon image generation."""
110
111 def test_create_icon_without_pystray(self):
112 """Should return None if pystray is not available."""
113 from i2p_apps.desktop.runner import _create_icon_image
114
115 import i2p_apps.desktop.runner as mod
116
117 orig = mod._HAS_PYSTRAY
118 mod._HAS_PYSTRAY = False
119 try:
120 assert _create_icon_image() is None
121 finally:
122 mod._HAS_PYSTRAY = orig
123
124
125class TestMainEntryPoint:
126 """Test the main() entry point."""
127
128 def test_main_exits_without_pystray(self, capsys):
129 """main() should print error and exit if pystray missing."""
130 import i2p_apps.desktop.runner as mod
131
132 orig = mod._HAS_PYSTRAY
133 mod._HAS_PYSTRAY = False
134 try:
135 with pytest.raises(SystemExit) as exc_info:
136 mod.main()
137 assert exc_info.value.code == 1
138 finally:
139 mod._HAS_PYSTRAY = orig