loctight-public/tests/test_core_logic.py
2026-01-09 12:15:52 -06:00

345 lines
11 KiB
Python

"""
Unit tests for LOCTight core functionality
Tests the timer and mouse jiggle functions in isolation
"""
import subprocess
import sys
import time
import unittest
from unittest.mock import MagicMock, Mock, patch
# Mock pyautogui to avoid X11/display requirements in CI
# Create a proper mock module with the attributes we need
mock_pyautogui = MagicMock()
mock_pyautogui.moveRel = MagicMock()
sys.modules["pyautogui"] = mock_pyautogui
class TestTimerFunctions(unittest.TestCase):
"""Test the core timer logic"""
@patch("time.sleep")
@patch("pyautogui.moveRel")
def test_jiggle_performs_mouse_movements(self, mock_move, mock_sleep):
"""Test that jiggle moves mouse correct number of times"""
# Create a simple version of jiggle for testing
timer_running = [True]
def jiggle(x, checks):
a = 0
while a < x and timer_running[0]:
b = 0
while b < 6 and timer_running[0]:
mock_move(0, 1, duration=0.001)
mock_sleep(10)
b += 1
a += 1
jiggle(1, 1)
self.assertEqual(mock_move.call_count, 6)
self.assertEqual(mock_sleep.call_count, 6)
@patch("time.sleep")
def test_countdown_logic(self, mock_sleep):
"""Test countdown decrements properly"""
timer_running = [True]
update_count = [0]
def update_time_label(m, s):
update_count[0] += 1
def countdown(minutes):
m = minutes
s = 0
while m > 0 and timer_running[0]:
s = 59
update_time_label(m - 1, s)
while s >= 0 and timer_running[0]:
update_time_label(m - 1, s)
mock_sleep(1)
s -= 1
m -= 1
update_time_label(0, 0)
# Test 1 minute countdown
countdown(1)
# Should update label 61 times (60 seconds + initial + final)
self.assertEqual(update_count[0], 62)
def test_time_formatting(self):
"""Test time label format"""
def format_time(m, s):
return f"Time Left: {m:02}:{s:02}"
self.assertEqual(format_time(5, 30), "Time Left: 05:30")
self.assertEqual(format_time(0, 9), "Time Left: 00:09")
self.assertEqual(format_time(59, 59), "Time Left: 59:59")
self.assertEqual(format_time(0, 0), "Time Left: 00:00")
def test_pause_toggle_logic(self):
"""Test pause toggle behavior"""
timer_running = [True]
paused = [False]
def pause_timer():
if timer_running[0]:
paused[0] = not paused[0]
return paused[0]
return None
# First pause
result = pause_timer()
self.assertTrue(result)
self.assertTrue(paused[0])
# Resume
result = pause_timer()
self.assertFalse(result)
self.assertFalse(paused[0])
# When timer not running, should not toggle
timer_running[0] = False
result = pause_timer()
self.assertIsNone(result)
self.assertFalse(paused[0])
def test_cancel_timer_logic(self):
"""Test cancel timer behavior"""
timer_running = [True]
paused = [True]
def cancel_timer():
timer_running[0] = False
paused[0] = False
return timer_running[0], paused[0]
t, p = cancel_timer()
self.assertFalse(t)
self.assertFalse(p)
def test_start_timer_logic(self):
"""Test start timer can only start when not already running"""
timer_running = [False]
def start_timer():
if not timer_running[0]:
timer_running[0] = True
return True
return False
# First start should succeed
result = start_timer()
self.assertTrue(result)
self.assertTrue(timer_running[0])
# Second start should fail (already running)
result = start_timer()
self.assertFalse(result)
self.assertTrue(timer_running[0])
class TestInputValidation(unittest.TestCase):
"""Test input validation logic"""
def test_custom_timer_validation(self):
"""Test custom timer input validation"""
def validate_custom_time(value):
try:
time_val = int(value)
if time_val <= 0:
return False, "Must be positive"
return True, time_val
except ValueError:
return False, "Must be integer"
# Valid inputs
valid, val = validate_custom_time("45")
self.assertTrue(valid)
self.assertEqual(val, 45)
valid, val = validate_custom_time("1")
self.assertTrue(valid)
self.assertEqual(val, 1)
# Invalid inputs
valid, msg = validate_custom_time("abc")
self.assertFalse(valid)
self.assertEqual(msg, "Must be integer")
valid, msg = validate_custom_time("-10")
self.assertFalse(valid)
self.assertEqual(msg, "Must be positive")
valid, msg = validate_custom_time("0")
self.assertFalse(valid)
self.assertEqual(msg, "Must be positive")
def test_preset_timers(self):
"""Test preset timer values"""
SHORT_TIMER = 30
LONG_TIMER = 60
self.assertEqual(SHORT_TIMER, 30)
self.assertEqual(LONG_TIMER, 60)
class TestPlatformLocking(unittest.TestCase):
"""Test platform-specific lock commands"""
@patch("sys.platform", "win32")
@patch("src.loctight.platform", "win32")
def test_windows_lock(self):
"""Test Windows lock workstation call"""
# Mock ctypes module
mock_ctypes = MagicMock()
with patch.dict("sys.modules", {"ctypes": mock_ctypes}):
# Import after mocking to ensure ctypes is available
import importlib
import src.loctight
# Inject the mock into the module
src.loctight.ctypes = mock_ctypes
from src.loctight import lock_workstation
lock_workstation()
mock_ctypes.windll.user32.LockWorkStation.assert_called_once()
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "darwin")
def test_macos_lock(self, mock_subprocess):
"""Test macOS lock command"""
from src.loctight import lock_workstation
lock_workstation()
mock_subprocess.assert_called_once_with(
[
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
"-suspend",
],
check=True,
)
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
def test_linux_lock_first_locker_succeeds(self, mock_subprocess):
"""Test Linux lock succeeds on first locker"""
from src.loctight import lock_workstation
# Mock successful lock on first try
mock_subprocess.return_value = MagicMock()
lock_workstation()
# Should only call the first locker
mock_subprocess.assert_called_once_with(
["loginctl", "lock-session"], check=True, capture_output=True
)
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
def test_linux_lock_fallback_mechanism(self, mock_subprocess):
"""Test Linux lock tries multiple lockers on failure"""
from src.loctight import lock_workstation
# First two lockers fail, third succeeds
mock_subprocess.side_effect = [
FileNotFoundError(), # loginctl not found
subprocess.CalledProcessError(1, "xdg-screensaver"), # xdg fails
MagicMock(), # gnome-screensaver succeeds
]
lock_workstation()
# Should have tried three lockers
self.assertEqual(mock_subprocess.call_count, 3)
calls = mock_subprocess.call_args_list
self.assertEqual(calls[0][0][0], ["loginctl", "lock-session"]) # First attempt
self.assertEqual(calls[1][0][0], ["xdg-screensaver", "lock"]) # Second attempt
self.assertEqual(
calls[2][0][0], ["gnome-screensaver-command", "--lock"]
) # Third attempt
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
@patch("src.loctight.messagebox.showwarning")
def test_linux_lock_all_fail(self, mock_messagebox, mock_subprocess):
"""Test Linux lock handles all lockers failing gracefully"""
from src.loctight import lock_workstation
# All lockers fail
mock_subprocess.side_effect = FileNotFoundError()
lock_workstation()
# Should have tried all 5 lockers
self.assertEqual(mock_subprocess.call_count, 5)
# Should show warning messagebox
mock_messagebox.assert_called_once_with(
"Screen Lock Failed",
"Could not lock screen. No supported screen locker found.",
)
class TestButtonStateManagement(unittest.TestCase):
"""Test button enable/disable logic"""
def test_disable_buttons_on_timer_start(self):
"""Test that starting timer disables appropriate buttons"""
buttons_state = {
"button1": "NORMAL",
"button2": "NORMAL",
"button3": "NORMAL",
"pause": "DISABLED",
"cancel": "DISABLED",
}
def disable_buttons():
buttons_state["button1"] = "DISABLED"
buttons_state["button2"] = "DISABLED"
buttons_state["button3"] = "DISABLED"
buttons_state["pause"] = "NORMAL"
buttons_state["cancel"] = "NORMAL"
disable_buttons()
self.assertEqual(buttons_state["button1"], "DISABLED")
self.assertEqual(buttons_state["button2"], "DISABLED")
self.assertEqual(buttons_state["button3"], "DISABLED")
self.assertEqual(buttons_state["pause"], "NORMAL")
self.assertEqual(buttons_state["cancel"], "NORMAL")
def test_enable_buttons_on_timer_end(self):
"""Test that ending timer enables appropriate buttons"""
buttons_state = {
"button1": "DISABLED",
"button2": "DISABLED",
"button3": "DISABLED",
"pause": "NORMAL",
"cancel": "NORMAL",
}
def enable_buttons():
buttons_state["button1"] = "NORMAL"
buttons_state["button2"] = "NORMAL"
buttons_state["button3"] = "NORMAL"
buttons_state["pause"] = "DISABLED"
buttons_state["cancel"] = "DISABLED"
enable_buttons()
self.assertEqual(buttons_state["button1"], "NORMAL")
self.assertEqual(buttons_state["button2"], "NORMAL")
self.assertEqual(buttons_state["button3"], "NORMAL")
self.assertEqual(buttons_state["pause"], "DISABLED")
self.assertEqual(buttons_state["cancel"], "DISABLED")
if __name__ == "__main__":
unittest.main()