mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-13 16:35:40 +00:00
Keep stealth separate from custom, saved-random and rotating MAC policies without migrating the development MAC mode. Persist authenticated peer state, guard advertising transitions and recovery, and expose the setting through the CLI and WebConfig. Add regression and CI checks and record exact XIAO BLE pairing, reconnect, reboot and address-rotation hardware results.
262 lines
13 KiB
Python
262 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import gzip
|
|
from pathlib import Path
|
|
import re
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE = ROOT / "webui" / "index.html"
|
|
HEADER = ROOT / "src" / "helpers" / "esp32" / "WebConfigHtml.h"
|
|
|
|
|
|
class WebConfigUiTest(unittest.TestCase):
|
|
def test_wifi_password_fields_have_local_show_controls(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertEqual(page.count('onclick="toggleWiFiPassword(this)"'), 2)
|
|
self.assertEqual(page.count('class="pwrow"'), 2)
|
|
self.assertIn('type="password" data-k="wifi.pwd" id="wz-wifi-pwd"', page)
|
|
self.assertIn('type="password" data-k="wifi.pwd" id="app-wifi-pwd"', page)
|
|
self.assertIn('input.type=show?"text":"password"', page)
|
|
self.assertIn('btn.textContent=show?"Hide":"Show"', page)
|
|
self.assertIn('btn.setAttribute("aria-pressed",show?"true":"false")', page)
|
|
|
|
def test_setup_wizard_can_skip_mqtt_and_usa_radio(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn('id="wz-radio-skip" onclick="wzSkipRadio()"', page)
|
|
self.assertIn('onclick="wzSkipMqtt()">Skip MQTT</button>', page)
|
|
self.assertIn("st.radioOptional=!!s.radio_optional", page)
|
|
|
|
radio_skip = page[page.index("function wzSkipRadio()"):
|
|
page.index("function wzUseMqtt()")]
|
|
self.assertIn("if(!st.radioOptional)return", radio_skip)
|
|
self.assertIn("delete st.dirty.radio", radio_skip)
|
|
self.assertIn("delete st.dirty.tx", radio_skip)
|
|
|
|
mqtt_skip = page[page.index("function wzSkipMqtt()"):
|
|
page.index("function popOut()")]
|
|
self.assertIn("st.skipMqtt=true", mqtt_skip)
|
|
self.assertIn("/^mqtt(?:\\.|[1-9]\\.)/", mqtt_skip)
|
|
self.assertIn("syncPacketFilters()", mqtt_skip)
|
|
|
|
save = page[page.index("function wizardSave()"):
|
|
page.index("/* ---------- reboot ---------- */")]
|
|
self.assertIn('if(!st.skipRadio&&$("#wz-rp").value==="")', save)
|
|
self.assertIn("if(st.hasMqtt&&!st.skipMqtt&&", save)
|
|
|
|
def test_setup_wizard_opens_existing_wifi_scan_once(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn("wizardScanOpened:false", page)
|
|
self.assertIn('onclick="openScan(\'wz-ssid\')">Scan</button>', page)
|
|
self.assertIn('onclick="startScan(true)">Rescan</button>', page)
|
|
|
|
open_scan = page[page.index("function openScan(target)"):
|
|
page.index("function closeScan()")]
|
|
self.assertIn('if(target==="wz-ssid")st.wizardScanOpened=true', open_scan)
|
|
|
|
auto_open = page[page.index("function openWizardScanOnce()"):
|
|
page.index("function wzGo(n)")]
|
|
self.assertIn("if(st.wizardScanOpened)return", auto_open)
|
|
self.assertIn('openScan("wz-ssid")', auto_open)
|
|
self.assertNotIn("startScan(true)", auto_open)
|
|
|
|
show_wizard = page[page.index("function showWizard()"):
|
|
page.index("function wzGo(n)")]
|
|
self.assertLess(show_wizard.index("loadConfig(capture)"),
|
|
show_wizard.index("openWizardScanOnce()"))
|
|
self.assertIn("wzRadioFill();openWizardScanOnce()", show_wizard)
|
|
|
|
# Scan completion may replace only the result list. Credentials change
|
|
# only in the existing explicit network-click handler below it.
|
|
poll_scan = page[page.index("function pollScan(url,epoch)"):
|
|
page.index("function clearInheritedWiFiPassword")]
|
|
self.assertNotIn(".value=", poll_scan)
|
|
self.assertNotIn("dispatchEvent", poll_scan)
|
|
|
|
def test_scan_epoch_invalidates_old_timers_and_responses(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn("scanEpoch:0", page)
|
|
|
|
close_scan = page[page.index("function closeScan()"):
|
|
page.index("function startScan(force)")]
|
|
self.assertIn("clearTimeout(st.scanTimer);st.scanTimer=0", close_scan)
|
|
self.assertIn("st.scanEpoch++", close_scan)
|
|
|
|
start_scan = page[page.index("function startScan(force)"):
|
|
page.index("function bars(rssi)")]
|
|
self.assertIn("var epoch=++st.scanEpoch", start_scan)
|
|
self.assertIn('pollScan(force?"/api/scan?rescan=1":"/api/scan",epoch)',
|
|
start_scan)
|
|
|
|
poll_scan = page[page.index("function pollScan(url,epoch)"):
|
|
page.index("function clearInheritedWiFiPassword")]
|
|
# Check before issuing a request, after a response, and in the error
|
|
# path so neither a stale timer nor a stale reply can alter the panel.
|
|
self.assertEqual(poll_scan.count("if(epoch!==st.scanEpoch)return"), 3)
|
|
self.assertIn('pollScan("/api/scan",epoch)', poll_scan)
|
|
|
|
def test_late_config_load_replays_visible_form_edits(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn("configLoadCapture:null", page)
|
|
self.assertIn("configEpoch:0", page)
|
|
|
|
listener = page[page.index('document.addEventListener("input"'):
|
|
page.index("function updateSaveBar()")]
|
|
self.assertIn("el.closest(capture.root)", listener)
|
|
self.assertIn("capture.values[el.dataset.k]=elVal(el)", listener)
|
|
self.assertIn("capture.confirms[el.dataset.cfm]=el.value", listener)
|
|
self.assertIn("capture.wifiPasswordAutoCleared=!!ev.meshAutoWiFiPassword", listener)
|
|
self.assertIn("capture.radio[el.dataset.rg]=elVal(el)", listener)
|
|
self.assertIn("capture.rxps[el.dataset.rxps]=elVal(el)", listener)
|
|
|
|
load_config = page[page.index("function loadConfig(loadCapture)"):
|
|
page.index("function markDirty")]
|
|
self.assertIn("var epoch=++st.configEpoch", load_config)
|
|
self.assertIn("loadCapture.loadEpoch=epoch", load_config)
|
|
self.assertIn("if(epoch!==st.configEpoch)return false", load_config)
|
|
self.assertIn("restoreConfigLoadEdits(loadCapture)", load_config)
|
|
self.assertIn("return true", load_config)
|
|
|
|
restore = page[page.index("function restoreConfigLoadEdits"):
|
|
page.index("function rxpsCfgValue")]
|
|
self.assertIn('owns.call(capture.values,"wifi.ssid")', restore)
|
|
self.assertIn('capture.values["wifi.ssid"]!==st.orig["wifi.ssid"]',
|
|
restore)
|
|
self.assertIn('!owns.call(capture.values,"wifi.pwd")', restore)
|
|
self.assertIn('st.orig["wifi.pwd"]===SENTINEL', restore)
|
|
self.assertIn('capture.values["wifi.pwd"]=""', restore)
|
|
self.assertIn("capture.wifiPasswordAutoCleared=true", restore)
|
|
self.assertIn("st.wifiPasswordAutoCleared=true", restore)
|
|
self.assertIn("v!==st.orig[k]", restore)
|
|
self.assertIn("if(dirty)st.dirty[k]=v;else delete st.dirty[k]", restore)
|
|
self.assertIn("capture.confirms[k]", restore)
|
|
self.assertIn("Object.keys(capture.radio)", restore)
|
|
self.assertIn("var restoredRadio=radioCombo()", restore)
|
|
self.assertIn("if(capture.rxps)", restore)
|
|
|
|
show_wizard = page[page.index("function showWizard()"):
|
|
page.index("function wzGo(n)")]
|
|
self.assertLess(show_wizard.index("st.configLoadCapture=capture"),
|
|
show_wizard.index("loadConfig(capture)"))
|
|
self.assertIn("if(st.configLoadCapture===capture)", show_wizard)
|
|
self.assertIn("if(!applied)return", show_wizard)
|
|
self.assertIn("st.configEpoch===capture.loadEpoch", show_wizard)
|
|
|
|
enter_app = page[page.index("function enterApp()"):
|
|
page.index("function enterConsole()")]
|
|
self.assertIn('root:"#v-app"', enter_app)
|
|
self.assertIn("st.configLoadCapture=capture", enter_app)
|
|
self.assertIn("loadConfig(capture)", enter_app)
|
|
|
|
def test_new_ssid_clears_only_inherited_masked_password(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
clear_pwd = page[page.index("function clearInheritedWiFiPassword"):
|
|
page.index('$("#scan-list").addEventListener')]
|
|
self.assertIn('target==="wz-ssid"?"wz-wifi-pwd"', clear_pwd)
|
|
self.assertIn('target==="app-ssid"?"app-wifi-pwd"', clear_pwd)
|
|
self.assertIn('nextSsid===st.orig["wifi.ssid"]', clear_pwd)
|
|
self.assertIn("pwd.value!==SENTINEL", clear_pwd)
|
|
self.assertIn('pwd.value=""', clear_pwd)
|
|
self.assertIn("st.wifiPasswordAutoCleared=true", clear_pwd)
|
|
self.assertIn("st.wifiPasswordAutoCleared=false", clear_pwd)
|
|
self.assertIn("pwd.value=SENTINEL", clear_pwd)
|
|
self.assertIn("meshAutoWiFiPassword=true", clear_pwd)
|
|
|
|
choose = page[page.index('$("#scan-list").addEventListener'):
|
|
page.index("/* ---------- wizard ---------- */")]
|
|
self.assertLess(
|
|
choose.index("clearInheritedWiFiPassword(st.scanTarget,selected)"),
|
|
choose.index("el.value=selected"),
|
|
)
|
|
|
|
listener = page[page.index('document.addEventListener("input"'):
|
|
page.index("function updateSaveBar()")]
|
|
self.assertIn(
|
|
'if(el.dataset.k==="wifi.ssid")clearInheritedWiFiPassword(el.id,elVal(el))',
|
|
listener,
|
|
)
|
|
|
|
def test_password_review_distinguishes_explicit_clear(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
review = page[page.index("function buildReview()"):
|
|
page.index("function wizardSave()")]
|
|
self.assertIn(
|
|
'Object.prototype.hasOwnProperty.call(d,"wifi.pwd")', review
|
|
)
|
|
self.assertIn(
|
|
'pwdChanged?(d["wifi.pwd"]?"******":"(open network)")', review
|
|
)
|
|
|
|
def test_bluetooth_address_field_is_capability_gated_and_bound(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn('data-cap="65536"><label>Bluetooth address</label>', page)
|
|
self.assertIn('data-k="bluetooth.mac"', page)
|
|
self.assertIn('case"bluetooth.mac":return r.bluetooth_mac||"";', page)
|
|
self.assertIn("random-every-boot", page)
|
|
self.assertIn("random-after-connect", page)
|
|
self.assertNotIn("random-after-connect, or stealth", page)
|
|
self.assertIn('("bluetooth.mac" in setmap)', page)
|
|
self.assertIn("Bluetooth address saved [OK] - rebooting", page)
|
|
|
|
def test_stealth_flag_has_independent_control_and_reboot_flow(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn('data-cap="131072"><label>Bluetooth stealth</label>', page)
|
|
self.assertIn('data-k="bluetooth.stealth"', page)
|
|
self.assertIn('case"bluetooth.stealth":return r.bluetooth_stealth?"on":"off";', page)
|
|
self.assertIn('("bluetooth.stealth" in setmap)', page)
|
|
self.assertIn('("bluetooth.stealth" in st.dirty)', page)
|
|
self.assertIn('("bluetooth.stealth" in submitted)', page)
|
|
self.assertIn("Bluetooth stealth saved [OK] - rebooting", page)
|
|
self.assertNotIn("|stealth|", page)
|
|
|
|
def test_scan_panel_exposes_automatic_updates_accessibly(self):
|
|
page = SOURCE.read_text(encoding="utf-8")
|
|
self.assertIn('id="wz-scan-btn"', page)
|
|
self.assertIn('id="app-scan-btn"', page)
|
|
self.assertEqual(page.count('aria-controls="scan-panel"'), 2)
|
|
self.assertIn('id="scan-panel" role="region"', page)
|
|
self.assertIn('id="scan-list" aria-live="polite" aria-busy="true"', page)
|
|
open_scan = page[page.index("function openScan(target)"):
|
|
page.index("function closeScan()")]
|
|
self.assertIn('setAttribute("aria-expanded"', open_scan)
|
|
close_scan = page[page.index("function closeScan()"):
|
|
page.index("function startScan(force)")]
|
|
self.assertIn('setAttribute("aria-hidden","true")', close_scan)
|
|
|
|
def test_generated_asset_contains_show_controls(self):
|
|
header = HEADER.read_text(encoding="utf-8")
|
|
length = int(re.search(r"WEBCONFIG_HTML_GZ_LEN = (\d+);", header).group(1))
|
|
array = header.split(
|
|
"const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {", 1
|
|
)[1]
|
|
blob = bytes(
|
|
int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", array)
|
|
)[:length]
|
|
page = gzip.decompress(blob).decode("utf-8")
|
|
self.assertEqual(page.count('onclick="toggleWiFiPassword(this)"'), 2)
|
|
self.assertIn("function toggleWiFiPassword(btn)", page)
|
|
self.assertIn("function wzSkipRadio()", page)
|
|
self.assertIn("function wzSkipMqtt()", page)
|
|
self.assertIn("function openWizardScanOnce()", page)
|
|
self.assertIn('openScan("wz-ssid")', page)
|
|
self.assertIn("function restoreConfigLoadEdits(capture)", page)
|
|
self.assertIn("function pollScan(url,epoch)", page)
|
|
self.assertIn("function clearInheritedWiFiPassword(target,nextSsid)", page)
|
|
self.assertIn('data-cap="65536"><label>Bluetooth address</label>', page)
|
|
self.assertIn('data-k="bluetooth.mac"', page)
|
|
self.assertIn('case"bluetooth.mac":return r.bluetooth_mac||"";', page)
|
|
self.assertIn("random-every-boot", page)
|
|
|
|
def test_skipped_mqtt_does_not_pin_display_in_setup_mode(self):
|
|
main = (ROOT / "examples" / "companion_radio" / "main.cpp").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
display_route = main[main.index("void loop()") : main.index("rtc_clock.tick();")]
|
|
self.assertIn("the_mesh.isWebConfigSetupActive()", display_route)
|
|
self.assertNotIn("isMQTTConfigured()", display_route)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|