# ABOUTME: Tests for fprintd D-Bus integration. # ABOUTME: Verifies fingerprint listener lifecycle and signal handling with mocked D-Bus. from unittest.mock import patch, MagicMock, call from moonlock.fingerprint import FingerprintListener class TestFingerprintListenerAvailability: """Tests for checking fprintd availability.""" @patch("moonlock.fingerprint.Gio.DBusProxy.new_for_bus_sync") def test_is_available_when_fprintd_running_and_enrolled(self, mock_proxy_cls): manager = MagicMock() mock_proxy_cls.return_value = manager manager.GetDefaultDevice.return_value = ("(o)", "/dev/0") device = MagicMock() mock_proxy_cls.return_value = device device.ListEnrolledFingers.return_value = ("(as)", ["right-index-finger"]) listener = FingerprintListener.__new__(FingerprintListener) listener._manager_proxy = manager listener._device_proxy = device listener._device_path = "/dev/0" assert listener.is_available("testuser") is True def test_is_available_returns_false_when_no_device(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = None listener._device_path = None assert listener.is_available("testuser") is False class TestFingerprintListenerLifecycle: """Tests for start/stop lifecycle.""" def test_start_calls_verify_start(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._device_path = "/dev/0" listener._signal_id = None listener._running = False on_success = MagicMock() on_failure = MagicMock() listener.start("testuser", on_success=on_success, on_failure=on_failure) listener._device_proxy.Claim.assert_called_once_with("(s)", "testuser") listener._device_proxy.VerifyStart.assert_called_once_with("(s)", "any") def test_stop_calls_verify_stop_and_release(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True listener._signal_id = 42 listener.stop() listener._device_proxy.VerifyStop.assert_called_once() listener._device_proxy.Release.assert_called_once() assert listener._running is False def test_stop_is_noop_when_not_running(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = False listener.stop() listener._device_proxy.VerifyStop.assert_not_called() class TestFingerprintSignalHandling: """Tests for VerifyStatus signal processing.""" def test_verify_match_calls_on_success(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True on_success = MagicMock() on_failure = MagicMock() listener._on_success = on_success listener._on_failure = on_failure listener._on_verify_status("verify-match", False) on_success.assert_called_once() def test_verify_no_match_calls_on_failure_and_retries_when_done(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True on_success = MagicMock() on_failure = MagicMock() listener._on_success = on_success listener._on_failure = on_failure listener._on_verify_status("verify-no-match", True) on_failure.assert_called_once() # Should restart verification when done=True listener._device_proxy.VerifyStart.assert_called_once_with("(s)", "any") def test_verify_no_match_calls_on_failure_without_restart_when_not_done(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True on_success = MagicMock() on_failure = MagicMock() listener._on_success = on_success listener._on_failure = on_failure listener._on_verify_status("verify-no-match", False) on_failure.assert_called_once() # Should NOT restart verification when done=False (still in progress) listener._device_proxy.VerifyStart.assert_not_called() def test_verify_swipe_too_short_retries_when_done(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True on_success = MagicMock() on_failure = MagicMock() listener._on_success = on_success listener._on_failure = on_failure listener._on_verify_status("verify-swipe-too-short", True) on_success.assert_not_called() on_failure.assert_not_called() listener._device_proxy.VerifyStart.assert_called_once_with("(s)", "any") def test_retry_status_does_not_restart_when_not_done(self): listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._running = True on_success = MagicMock() on_failure = MagicMock() listener._on_success = on_success listener._on_failure = on_failure listener._on_verify_status("verify-swipe-too-short", False) on_success.assert_not_called() on_failure.assert_not_called() # Should NOT restart — verification still in progress listener._device_proxy.VerifyStart.assert_not_called() class TestFingerprintStartErrorHandling: """Tests for GLib.Error handling in start().""" def test_claim_glib_error_logs_and_returns_without_starting(self): """When Claim() raises GLib.Error, start() should not proceed.""" from gi.repository import GLib listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._device_path = "/dev/0" listener._signal_id = None listener._running = False listener._on_success = None listener._on_failure = None listener._device_proxy.Claim.side_effect = GLib.Error( "net.reactivated.Fprint.Error.AlreadyClaimed" ) on_success = MagicMock() on_failure = MagicMock() listener.start("testuser", on_success=on_success, on_failure=on_failure) # Should NOT have connected signals or started verification listener._device_proxy.connect.assert_not_called() listener._device_proxy.VerifyStart.assert_not_called() assert listener._running is False def test_verify_start_glib_error_disconnects_and_releases(self): """When VerifyStart() raises GLib.Error, start() should clean up.""" from gi.repository import GLib listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._device_path = "/dev/0" listener._signal_id = None listener._running = False listener._on_success = None listener._on_failure = None # Claim succeeds, signal connect returns an ID, VerifyStart fails listener._device_proxy.connect.return_value = 99 listener._device_proxy.VerifyStart.side_effect = GLib.Error( "net.reactivated.Fprint.Error.Internal" ) on_success = MagicMock() on_failure = MagicMock() listener.start("testuser", on_success=on_success, on_failure=on_failure) # Should have disconnected the signal listener._device_proxy.disconnect.assert_called_once_with(99) # Should have released the device listener._device_proxy.Release.assert_called_once() assert listener._running is False assert listener._signal_id is None def test_start_sets_running_true_only_on_success(self): """_running should only be True after both Claim and VerifyStart succeed.""" listener = FingerprintListener.__new__(FingerprintListener) listener._device_proxy = MagicMock() listener._device_path = "/dev/0" listener._signal_id = None listener._running = False listener._on_success = None listener._on_failure = None listener._device_proxy.connect.return_value = 42 on_success = MagicMock() on_failure = MagicMock() listener.start("testuser", on_success=on_success, on_failure=on_failure) assert listener._running is True