tests/test_scanner.py (4242B)
1 """Tests for ap_scanner.scanner — ChannelHopper schedule + ScannerEngine logic."""
2
3 from unittest.mock import patch
4
5 from ap_scanner.scanner import ChannelHopper, ScannerEngine
6
7
8 # --- ChannelHopper._build_schedule ---
9
10 class TestBuildSchedule:
11 """Tests for ChannelHopper._build_schedule weighted scheduling."""
12
13 def _make_hopper(self, channels):
14 """Create a ChannelHopper without starting any threads."""
15 with patch.object(ChannelHopper, "_get_mac", return_value="00:00:00:00:00:00"):
16 return ChannelHopper("wlan0", channels)
17
18 def test_primary_channels_appear_first(self):
19 """Channels 1, 6, 11 should be the first entries for quick discovery."""
20 hopper = self._make_hopper([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
21 sched = hopper._schedule
22 # First three entries should be the primary channels
23 assert sched[0] in {1, 6, 11}
24 assert sched[1] in {1, 6, 11}
25 assert sched[2] in {1, 6, 11}
26
27 def test_primary_channels_visited_more(self):
28 """Primary channels (1, 6, 11) should appear more than once."""
29 hopper = self._make_hopper(list(range(1, 14)))
30 sched = hopper._schedule
31 for ch in (1, 6, 11):
32 assert sched.count(ch) >= 2, f"Channel {ch} should appear >= 2 times"
33
34 def test_only_primary_channels(self):
35 """With only primary channels, schedule still works."""
36 hopper = self._make_hopper([1, 6, 11])
37 sched = hopper._schedule
38 assert len(sched) > 0
39 assert set(sched) == {1, 6, 11}
40
41 def test_no_primary_channels(self):
42 """With no primary channels (5GHz only), schedule is a plain list."""
43 hopper = self._make_hopper([36, 40, 44, 48])
44 sched = hopper._schedule
45 assert sched == [36, 40, 44, 48]
46
47 def test_empty_channels(self):
48 """Empty channel list produces empty schedule."""
49 hopper = self._make_hopper([])
50 assert hopper._schedule == []
51
52 def test_single_channel(self):
53 """Single non-primary channel produces single-entry schedule."""
54 hopper = self._make_hopper([36])
55 assert hopper._schedule == [36]
56
57 def test_single_primary_channel(self):
58 """Single primary channel appears twice (prepended + tail)."""
59 hopper = self._make_hopper([6])
60 sched = hopper._schedule
61 assert 6 in sched
62 assert len(sched) >= 1
63
64 def test_5ghz_schedule_unchanged(self):
65 """5GHz-only channels should maintain order (no reordering)."""
66 channels = [36, 40, 44, 48, 52, 56, 60, 64]
67 hopper = self._make_hopper(channels)
68 assert hopper._schedule == channels
69
70 def test_mixed_channels_all_present(self):
71 """All channels from input are present in the schedule."""
72 channels = [1, 6, 11, 36, 40, 44, 48]
73 hopper = self._make_hopper(channels)
74 sched = hopper._schedule
75 for ch in channels:
76 assert ch in sched, f"Channel {ch} missing from schedule"
77
78
79 # --- ScannerEngine AP tracking ---
80
81 class TestScannerEngineAPUpdate:
82 """Tests for AP update logic in _on_packet."""
83
84 def test_access_points_returns_copy(self):
85 """access_points property should return a copy, not the internal dict."""
86 with patch.object(ChannelHopper, "_get_mac", return_value="00:00:00:00:00:00"):
87 engine = ScannerEngine("wlan0", [1, 6, 11])
88 aps1 = engine.access_points
89 aps2 = engine.access_points
90 assert aps1 is not aps2
91
92 def test_clear_removes_all_aps(self):
93 """clear() should remove all discovered APs."""
94 from datetime import datetime
95 from ap_scanner.models import AccessPoint
96
97 with patch.object(ChannelHopper, "_get_mac", return_value="00:00:00:00:00:00"):
98 engine = ScannerEngine("wlan0", [1, 6, 11])
99 # Manually inject an AP
100 ap = AccessPoint(
101 bssid="AA:BB:CC:DD:EE:FF", ssid="Test", channel=6,
102 rssi=-50, encryption="WPA2", cipher="CCMP", auth="PSK",
103 band="2.4GHz", wifi_standard="n",
104 )
105 engine._aps["AA:BB:CC:DD:EE:FF"] = ap
106 assert len(engine.access_points) == 1
107 engine.clear()
108 assert len(engine.access_points) == 0