Files
HaloKeymind/test
agessaman 8abe26ba7b fix(webconfig): stop the CLI reading secrets, and enforce the setup password
Two findings from review, both real, both mine.

The CLI could read secrets the portal has never exposed. CommonCLI splits its
surface by CALLER, not by command: a serial caller (sender_timestamp 0, physical
access) reads secrets in plaintext, a remote one gets "******** (serial only)".
Its own comments say so — "Serial only (WiFi creds grant LAN access); remote
sees set/unset". execCommand passes 0, which is what makes `erase`, `stats-*`
and `set freq` reachable at all, and with it the terminal inherited the serial
console's plaintext answers for an HTTP request: `get prv.key` returned this
node's identity, `get wifi.pwd` the operator's network.

Worse in setup mode, which authenticates by proximity to an open AP — and `start
webconfig ap` can be run on an already-configured node, so the secrets are real
by then, not blank.

I had reasoned that the AP was the trust boundary either way because the wizard
can already rewrite these. That conflated two capabilities: replacing a WiFi
password does not reveal the current one, and replacing an identity does not
reveal the existing private key. /api/config has always masked these on read
(wcIsSecretKey); the CLI simply broke that rule. Now only the READ is masked —
the command surface stays whole — in CommonCLI's own words, keeping the
set/unset signal that is the useful part.

Onboarding could also skip the mandatory password. handleConfigPost refuses to
arm a reboot during initial setup without one; the CLI only warned in the
browser, which a pasted script or a direct POST ignores, so a node could reboot
onto the LAN still holding the factory credential. Same rule now applies at
POST. It is satisfied by a `password` command anywhere in the session rather
than only in the same request, so the natural two-step console flow still works
— the form batch always sends both together and never needed that memory.

wcIsSecretReadCommand lives in WebConfigKeys.h beside the rest of the secret
classification, pinned by three host tests: what must be masked, what must not,
and that only reads are touched. 17 keys + 24 batch tests pass; the audit checks
a masked read round-trips as masked.
2026-08-08 14:05:02 -07:00
..

Host unit tests

Fast, hardware-free unit tests for the fork's pure logic, run on the host with GoogleTest via PlatformIO's native environment. They cover the extractable observer/WebConfig logic (validation, preset table, topic templates, key parsing) — the parts that don't depend on the ESP32, radio, or network stack. Integration behavior (AsyncTCP transport, WiFi/MQTT, SoftAP) is exercised separately; see "Local testing without hardware" in MQTT_IMPLEMENTATION.md.

Running

pio test -e native                      # all suites
pio test -e native -f test_webconfig_keys   # a single suite

A green [PASSED] per suite means GoogleTest returned 0 (all assertions passed). PlatformIO's "0 test cases" line is just its Unity-style counter and does not reflect the GoogleTest count — run the built binary directly (.pio/build/native/program) to see the per-assertion breakdown.

Suites

Suite Source under test Covers
test_mqtt_presets src/helpers/MQTTPresets.h preset lookup; table integrity (unique names, non-empty URLs, JWT-audience invariant, names fit the slot buffer); mqttPresetNeedsSlotCredentials; slot-count constants
test_observer_validation src/helpers/MQTTObserverValidation.h IATA (exactly 3 alphanumerics), owner key (64 hex), NTP hostname, and the buffer-fit check behind the #17 length validation — including boundaries and nulls
test_webconfig_keys src/helpers/WebConfigKeys.h POST-key allowlist, secret detection, admin-password classification/validation, slot-index bounds, and the short-key out-of-bounds guard (attacker-supplied keys)
test_topic_template src/helpers/MQTTTopicTemplate.h {iata}/{device}/{token}/{type} expansion, overflow/NUL-termination, and a buffer-size fuzz
test_mqtt_topic_router src/helpers/MQTTTopicRouter.h complete preset/custom topic-routing contract; MeshRank all types except raw; required identifiers; invalid inputs/slots; exact buffer boundaries
test_mqtt_connection_policy src/helpers/MQTTConnectionPolicy.h reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit millis() rollover
test_mqtt_packet_queue_policy src/helpers/MQTTPacketQueuePolicy.h queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit millis() rollover
test_mqtt_packet_filter src/helpers/MQTTPacketFilter.h per-slot 0-15 allowlist parsing/formatting, numeric and named spellings; exact bounds; membership; candidate/eligible split and retry-completion policy; pre-queue union gate; default-mask detection
test_mqtt_runtime_buffer_lifecycle src/helpers/MQTTRuntimeBufferLifecycle.h idempotent allocation/release; partial-allocation degradation; retry of only missing buffers
test_mqtt_prefs_codec src/helpers/MQTTPrefsStorage.h, src/helpers/MQTTPrefsCodec.h binary pre-slot/3-slot/6-slot migration fixtures; v1 header integrity; downgrade preservation; shortest-payload write policy (default filters stay downgrade-readable)
test_mqtt_prefs_atomic_store src/helpers/MQTTPrefsAtomicStore.h transactional MQTT writes and legacy /node_prefs handoff; exact short-write detection; begin/finish/rename failure cleanup; original-file preservation
test_mqtt_payload_builder src/helpers/MQTTPayloadBuilder.cpp status/packet/raw JSON contracts; optional fields; escaping; RX metrics and path; score handling; exact buffer bounds; maximum representative payloads
test_utils src/Utils.cpp Utils::toHex (upstream)

Conventions (and how to add a suite)

  • Each test/test_<name>/ directory builds into its own GoogleTest program and must define its own main() (::testing::InitGoogleTest + RUN_ALL_TESTS).
  • Tests are host-only: include only pure headers. Arduino/crypto stubs live in test/mocks/ (on the include path via -I test/mocks).
  • Firmware headers are included from src (via -I src, e.g. #include "helpers/MQTTPresets.h"). Some are guarded or ESP-flavored, so a suite may need shims before the include — e.g. test_mqtt_presets does #define WITH_MQTT_BRIDGE 1 (the preset table is behind that flag) and #define PROGMEM (the embedded CA-cert strings are PROGMEM-qualified).
  • To add a suite: create test/test_<name>/test_<name>.cpp with a main(), and add any host-only source it links to the native env's build_src_filter in platformio.ini (header-only code needs no source entry). No other wiring.
  • Keep logic testable by extracting pure functions into headers (as MQTTObserverValidation.h / WebConfigKeys.h / MQTTTopicTemplate.h do) and having the firmware call the same functions.