|
3 | 3 | Tests the timer and mouse jiggle functions in isolation |
4 | 4 | """ |
5 | 5 |
|
| 6 | +import subprocess |
6 | 7 | import sys |
7 | 8 | import time |
8 | 9 | import unittest |
9 | 10 | from unittest.mock import MagicMock, Mock, patch |
10 | 11 |
|
| 12 | +# Mock pyautogui to avoid X11/display requirements in CI |
| 13 | +# Create a proper mock module with the attributes we need |
| 14 | +mock_pyautogui = MagicMock() |
| 15 | +mock_pyautogui.moveRel = MagicMock() |
| 16 | +sys.modules["pyautogui"] = mock_pyautogui |
| 17 | + |
11 | 18 |
|
12 | 19 | class TestTimerFunctions(unittest.TestCase): |
13 | 20 | """Test the core timer logic""" |
@@ -182,41 +189,101 @@ def test_preset_timers(self): |
182 | 189 | class TestPlatformLocking(unittest.TestCase): |
183 | 190 | """Test platform-specific lock commands""" |
184 | 191 |
|
185 | | - @patch("ctypes.windll.user32.LockWorkStation") |
186 | 192 | @patch("sys.platform", "win32") |
187 | | - def test_windows_lock(self, mock_lock): |
| 193 | + @patch("src.loctight.platform", "win32") |
| 194 | + def test_windows_lock(self): |
188 | 195 | """Test Windows lock workstation call""" |
189 | | - import ctypes |
| 196 | + # Mock ctypes module |
| 197 | + mock_ctypes = MagicMock() |
| 198 | + with patch.dict("sys.modules", {"ctypes": mock_ctypes}): |
| 199 | + # Import after mocking to ensure ctypes is available |
| 200 | + import importlib |
| 201 | + |
| 202 | + import src.loctight |
| 203 | + |
| 204 | + # Inject the mock into the module |
| 205 | + src.loctight.ctypes = mock_ctypes |
190 | 206 |
|
191 | | - # Simulate Windows lock |
192 | | - if sys.platform == "win32": |
193 | | - mock_lock() |
| 207 | + from src.loctight import lock_workstation |
194 | 208 |
|
195 | | - mock_lock.assert_called_once() |
| 209 | + lock_workstation() |
| 210 | + mock_ctypes.windll.user32.LockWorkStation.assert_called_once() |
196 | 211 |
|
197 | | - @patch("subprocess.call") |
198 | | - @patch("sys.platform", "darwin") |
| 212 | + @patch("src.loctight.subprocess.run") |
| 213 | + @patch("src.loctight.platform", "darwin") |
199 | 214 | def test_macos_lock(self, mock_subprocess): |
200 | 215 | """Test macOS lock command""" |
201 | | - if sys.platform == "darwin": |
202 | | - mock_subprocess( |
203 | | - r"/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend", |
204 | | - shell=True, |
205 | | - ) |
206 | | - |
207 | | - self.assertTrue(mock_subprocess.called) |
208 | | - |
209 | | - @patch("subprocess.call") |
210 | | - @patch("sys.platform", "linux") |
211 | | - def test_linux_lock(self, mock_subprocess): |
212 | | - """Test Linux lock command""" |
213 | | - if sys.platform.startswith("linux"): |
214 | | - mock_subprocess( |
215 | | - r"/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend", |
216 | | - shell=True, |
217 | | - ) |
218 | | - |
219 | | - self.assertTrue(mock_subprocess.called) |
| 216 | + from src.loctight import lock_workstation |
| 217 | + |
| 218 | + lock_workstation() |
| 219 | + |
| 220 | + mock_subprocess.assert_called_once_with( |
| 221 | + [ |
| 222 | + "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession", |
| 223 | + "-suspend", |
| 224 | + ], |
| 225 | + check=True, |
| 226 | + ) |
| 227 | + |
| 228 | + @patch("src.loctight.subprocess.run") |
| 229 | + @patch("src.loctight.platform", "linux") |
| 230 | + def test_linux_lock_first_locker_succeeds(self, mock_subprocess): |
| 231 | + """Test Linux lock succeeds on first locker""" |
| 232 | + from src.loctight import lock_workstation |
| 233 | + |
| 234 | + # Mock successful lock on first try |
| 235 | + mock_subprocess.return_value = MagicMock() |
| 236 | + |
| 237 | + lock_workstation() |
| 238 | + |
| 239 | + # Should only call the first locker |
| 240 | + mock_subprocess.assert_called_once_with( |
| 241 | + ["loginctl", "lock-session"], check=True, capture_output=True |
| 242 | + ) |
| 243 | + |
| 244 | + @patch("src.loctight.subprocess.run") |
| 245 | + @patch("src.loctight.platform", "linux") |
| 246 | + def test_linux_lock_fallback_mechanism(self, mock_subprocess): |
| 247 | + """Test Linux lock tries multiple lockers on failure""" |
| 248 | + from src.loctight import lock_workstation |
| 249 | + |
| 250 | + # First two lockers fail, third succeeds |
| 251 | + mock_subprocess.side_effect = [ |
| 252 | + FileNotFoundError(), # loginctl not found |
| 253 | + subprocess.CalledProcessError(1, "xdg-screensaver"), # xdg fails |
| 254 | + MagicMock(), # gnome-screensaver succeeds |
| 255 | + ] |
| 256 | + |
| 257 | + lock_workstation() |
| 258 | + |
| 259 | + # Should have tried three lockers |
| 260 | + self.assertEqual(mock_subprocess.call_count, 3) |
| 261 | + calls = mock_subprocess.call_args_list |
| 262 | + self.assertEqual(calls[0][0][0], ["loginctl", "lock-session"]) # First attempt |
| 263 | + self.assertEqual(calls[1][0][0], ["xdg-screensaver", "lock"]) # Second attempt |
| 264 | + self.assertEqual( |
| 265 | + calls[2][0][0], ["gnome-screensaver-command", "--lock"] |
| 266 | + ) # Third attempt |
| 267 | + |
| 268 | + @patch("src.loctight.subprocess.run") |
| 269 | + @patch("src.loctight.platform", "linux") |
| 270 | + @patch("src.loctight.messagebox.showwarning") |
| 271 | + def test_linux_lock_all_fail(self, mock_messagebox, mock_subprocess): |
| 272 | + """Test Linux lock handles all lockers failing gracefully""" |
| 273 | + from src.loctight import lock_workstation |
| 274 | + |
| 275 | + # All lockers fail |
| 276 | + mock_subprocess.side_effect = FileNotFoundError() |
| 277 | + |
| 278 | + lock_workstation() |
| 279 | + |
| 280 | + # Should have tried all 5 lockers |
| 281 | + self.assertEqual(mock_subprocess.call_count, 5) |
| 282 | + # Should show warning messagebox |
| 283 | + mock_messagebox.assert_called_once_with( |
| 284 | + "Screen Lock Failed", |
| 285 | + "Could not lock screen. No supported screen locker found.", |
| 286 | + ) |
220 | 287 |
|
221 | 288 |
|
222 | 289 | class TestButtonStateManagement(unittest.TestCase): |
|
0 commit comments