import os, sys, unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui')) import server import mk8_guard HOP_KEY = 'pineapd.wlan1mon.hop' class GuardTest(unittest.TestCase): def setUp(self): self.calls = [] self.uci = {} self.pool = 0 self.mon_up = {'wlan0mon': True, 'wlan1mon': True} self.old_iface_up = server._iface_up self.old_server_device_run = server.device_run self.old_guard_device_run = mk8_guard.device_run server._iface_up = lambda name: self.mon_up.get(name, True) def fake_run(args, timeout=20, input_data=None): a = list(args) self.calls.append(a) if a[:2] == ['uci', 'get']: key = a[2] if key == 'pineapd.@ssidpool[0].ssid': return (0, ''.join('s%d\n' % i for i in range(self.pool)), '') return (0, self.uci.get(key, '') + '\n', '') if a[:2] == ['uci', 'set']: k, _, v = a[2].partition('=') self.uci[k] = v if a[:2] == ['uci', 'delete']: self.pool = 0 return (0, '', '') mk8_guard.device_run = fake_run server.device_run = fake_run mk8_guard._GR_CACHE['data'] = None def tearDown(self): server._iface_up = self.old_iface_up server.device_run = self.old_server_device_run mk8_guard.device_run = self.old_guard_device_run mk8_guard._GR_CACHE['data'] = None def wanted_count(self): # Safe set minus wlan1mon.hop (rfplan owns hop), plus WANTED_EXTRA. return (len(server.PINEAPD_SAFE_UCI) - 1 + len(mk8_guard.WANTED_EXTRA)) def test_applies_all_wanted_when_missing(self): self.pool = 0 result = mk8_guard.reconcile(clear_pool=False) sets = [c[2] for c in self.calls if c[:2] == ['uci', 'set']] self.assertEqual(len(sets), self.wanted_count()) self.assertTrue(result['changed']) self.assertNotIn(HOP_KEY, {c[2].partition('=')[0] for c in self.calls if c[:2] == ['uci', 'set']}) def test_wanted_excludes_hop(self): wanted = mk8_guard._wanted() self.assertNotIn(HOP_KEY, wanted) self.assertEqual(len(wanted), self.wanted_count()) def test_clears_large_pool_only(self): self.pool = 25 result = mk8_guard.reconcile(clear_pool=True) self.assertTrue(result['pool_cleared']) self.assertIn(['uci', 'delete', 'pineapd.@ssidpool[0].ssid'], self.calls) def test_small_pool_untouched(self): self.pool = 5 result = mk8_guard.reconcile(clear_pool=True) self.assertFalse(result['pool_cleared']) def test_report_ignores_hop_and_caches(self): for key, value in mk8_guard._wanted().items(): self.uci[key] = value self.uci[HOP_KEY] = '1' # attack-role baseline; must stay ignored report = mk8_guard.guard_report() self.assertTrue(report['in_sync'], report) gets = [c[2] for c in self.calls if c[:2] == ['uci', 'get']] self.assertNotIn(HOP_KEY, gets) n_after_first = len(self.calls) self.assertIs(mk8_guard.guard_report(), report) self.assertEqual(len(self.calls), n_after_first, 'guard_report must serve from cache within TTL') mk8_guard._GR_CACHE['t'] -= mk8_guard.GR_TTL_SECONDS * 2 mk8_guard.guard_report() self.assertGreater(len(self.calls), n_after_first) # reconcile mutates live state; it must invalidate the cached report. mk8_guard.guard_report() n_cached = len(self.calls) mk8_guard.reconcile(clear_pool=False) self.assertIsNone(mk8_guard._GR_CACHE['data']) mk8_guard.guard_report() self.assertGreater(len(self.calls), n_cached) if __name__ == '__main__': unittest.main()