mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
* fix(wifi): make Connect work from the setup AP Joining a network from LEDMatrix-Setup has to take the AP down first, which drops the phone that sent the request. The connect endpoint answered only after the attempt finished, so the browser never got a reply and the WiFi tab's Connect button appeared to do nothing. - /wifi/connect answers 202 immediately while the AP is active and connects in a background thread; the result (never the password) is reported via /wifi/status as last_connect_attempt. A second connect while one is pending gets 409. - connect_to_network holds a /tmp flag for the attempt; the monitor daemon skips AP management while it is fresh. Previously the daemon's disconnected counter, accumulated over the whole AP session, re-enabled the AP on its next tick in the middle of the connect. - The WiFi tab and captive setup page explain the handoff up front, and on reopening show why the last attempt failed. The wrong-password message now works: the route sets the error_type the captive page checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(wifi): serialize connect attempts on both paths Addresses CodeRabbit review on #571: - Check for a pending attempt before branching on AP state. A background attempt takes the AP down long before it finishes, so a second click used to bypass the 409 and start a competing synchronous connect. - Record pending for the synchronous (non-AP) path too, so two requests can't overlap and have the first clear the daemon's in-progress flag while the second is still connecting. - Clear the pending state if the background thread fails to start, rather than refusing every later request until restart. - Say the setup network returns "within a few minutes": a stale flag plus the daemon's grace period can take longer than one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
144 lines
6.2 KiB
Python
144 lines
6.2 KiB
Python
"""Tests for check_and_manage_ap_mode_with_state (src/wifi_manager.py).
|
|
|
|
The wifi monitor daemon used to fetch WiFi status before AND after each
|
|
check on top of the check's own internal fetch — every fetch is several
|
|
nmcli subprocess forks, every 30s, forever. The new API returns the state
|
|
the check observed, so the daemon runs exactly one fetch battery per tick.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from src.wifi_manager import WiFiManager, WiFiStatus # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def wm(tmp_path):
|
|
with patch.object(WiFiManager, "_load_config", return_value={}, create=True):
|
|
manager = WiFiManager.__new__(WiFiManager)
|
|
# minimal attribute setup without running the real __init__
|
|
manager.config = {"auto_enable_ap_mode": True}
|
|
manager._disconnected_checks = 0
|
|
manager._disconnected_checks_required = 3
|
|
manager._ap_enabled_at = None
|
|
return manager
|
|
|
|
|
|
def _wire(wm, connected, ethernet, ap_active):
|
|
wm._get_wifi_status_with_retry = MagicMock(
|
|
return_value=WiFiStatus(connected=connected, ssid="net" if connected else None))
|
|
wm._is_ethernet_connected = MagicMock(return_value=ethernet)
|
|
wm._is_ap_mode_active = MagicMock(return_value=ap_active)
|
|
wm.enable_ap_mode = MagicMock(return_value=(True, "ok"))
|
|
wm.disable_ap_mode = MagicMock(return_value=(True, "ok"))
|
|
wm.scan_networks = MagicMock(return_value=([], False))
|
|
wm._save_cached_scan = MagicMock()
|
|
wm._FORCE_AP_FLAG_PATH = MagicMock()
|
|
wm._FORCE_AP_FLAG_PATH.exists.return_value = False
|
|
wm._connect_in_progress = MagicMock(return_value=False)
|
|
|
|
|
|
class TestConnectInProgressHoldsOffTheDaemon:
|
|
"""Connecting from the setup AP takes the AP down before joining. The
|
|
daemon had already counted the whole AP session as "disconnected", so its
|
|
next tick re-enabled the AP in the middle of the connect."""
|
|
|
|
def test_no_ap_enable_while_a_connect_is_running(self, wm):
|
|
_wire(wm, connected=False, ethernet=False, ap_active=False)
|
|
wm._connect_in_progress.return_value = True
|
|
wm._disconnected_checks = 10 # accumulated while the AP was up
|
|
changed, *_ = wm.check_and_manage_ap_mode_with_state()
|
|
assert changed is False
|
|
wm.enable_ap_mode.assert_not_called()
|
|
assert wm._disconnected_checks == 0
|
|
|
|
def test_grace_period_restarts_after_the_connect(self, wm):
|
|
_wire(wm, connected=False, ethernet=False, ap_active=False)
|
|
wm._connect_in_progress.return_value = True
|
|
wm._disconnected_checks = 10
|
|
wm.check_and_manage_ap_mode_with_state()
|
|
wm._connect_in_progress.return_value = False
|
|
wm.check_and_manage_ap_mode_with_state()
|
|
wm.enable_ap_mode.assert_not_called()
|
|
|
|
def test_flag_is_held_for_the_connect_and_removed_after(self, wm, tmp_path):
|
|
flag = tmp_path / "connecting"
|
|
wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag
|
|
seen = []
|
|
wm._connect_validated = lambda ssid, pw: seen.append(flag.exists()) or (True, "ok")
|
|
assert wm.connect_to_network("HomeNet", "hunter22") == (True, "ok")
|
|
assert seen == [True]
|
|
assert not flag.exists()
|
|
|
|
def test_flag_is_removed_when_the_connect_raises(self, wm, tmp_path):
|
|
flag = tmp_path / "connecting"
|
|
wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag
|
|
wm._connect_validated = MagicMock(side_effect=RuntimeError("nmcli gone"))
|
|
with pytest.raises(RuntimeError):
|
|
wm.connect_to_network("HomeNet", "hunter22")
|
|
assert not flag.exists()
|
|
|
|
def test_a_stale_flag_from_a_dead_process_is_ignored(self, wm, tmp_path):
|
|
flag = tmp_path / "connecting"
|
|
flag.touch()
|
|
wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag
|
|
assert WiFiManager._connect_in_progress(wm) is True
|
|
old = flag.stat().st_mtime - WiFiManager._CONNECT_FLAG_MAX_AGE_SECONDS - 1
|
|
os.utime(flag, (old, old))
|
|
assert WiFiManager._connect_in_progress(wm) is False
|
|
|
|
|
|
class TestWithState:
|
|
def test_single_fetch_per_call(self, wm):
|
|
_wire(wm, connected=True, ethernet=False, ap_active=False)
|
|
wm.check_and_manage_ap_mode_with_state()
|
|
assert wm._get_wifi_status_with_retry.call_count == 1
|
|
assert wm._is_ethernet_connected.call_count == 1
|
|
assert wm._is_ap_mode_active.call_count == 1
|
|
|
|
def test_returns_observed_state(self, wm):
|
|
_wire(wm, connected=True, ethernet=True, ap_active=False)
|
|
changed, status, ethernet, ap_after = wm.check_and_manage_ap_mode_with_state()
|
|
assert changed is False
|
|
assert status.connected is True
|
|
assert ethernet is True
|
|
assert ap_after is False
|
|
|
|
def test_ap_after_inverts_on_disable(self, wm):
|
|
"""WiFi reconnects while AP is up -> auto-disable -> ap_after False."""
|
|
_wire(wm, connected=True, ethernet=False, ap_active=True)
|
|
changed, _status, _ethernet, ap_after = wm.check_and_manage_ap_mode_with_state()
|
|
assert changed is True
|
|
assert ap_after is False
|
|
wm.disable_ap_mode.assert_called_once()
|
|
|
|
def test_ap_after_inverts_on_enable(self, wm):
|
|
"""Grace period exhausted with nothing connected -> enable -> True."""
|
|
_wire(wm, connected=False, ethernet=False, ap_active=False)
|
|
wm._disconnected_checks = 2 # this call is the 3rd
|
|
changed, _status, _ethernet, ap_after = wm.check_and_manage_ap_mode_with_state()
|
|
assert changed is True
|
|
assert ap_after is True
|
|
wm.enable_ap_mode.assert_called_once()
|
|
|
|
def test_bool_wrapper_is_back_compatible(self, wm):
|
|
_wire(wm, connected=True, ethernet=False, ap_active=False)
|
|
assert wm.check_and_manage_ap_mode() is False
|
|
_wire(wm, connected=True, ethernet=False, ap_active=True)
|
|
assert wm.check_and_manage_ap_mode() is True
|
|
|
|
def test_exception_path_never_raises(self, wm):
|
|
wm._get_wifi_status_with_retry = MagicMock(side_effect=RuntimeError("nmcli gone"))
|
|
changed, status, _ethernet, _ap_after = wm.check_and_manage_ap_mode_with_state()
|
|
assert changed is False
|
|
assert status.connected is False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main([__file__, "-v"]))
|