Files
MeshCore/test/test_management_password_commands.py
T
mikecarper cd02e55824 Share the saved admin password across ESP32 management services
Use the committed node credential for setup WiFi, WebConfig and browser OTA. Add persistent ESP32 WiFi Companion management credentials without changing preferences layouts, revoke stale browser sessions on rotation, retain scoped save receipts, and coordinate credential changes with flash uploads. Keep external-service credentials and trusted TCP protocol compatibility independent. Update live join QR codes, failure handling and documentation.

Validated 51 native cases, 158 related host and browser methods, and serial Full V4 Companion, Station G3 Repeater, ESP32-C6 Room Server and V4 Sensor firmware builds.
2026-10-18 13:09:48 -07:00

107 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Exercise production role setters against failed saves and active flash uploads."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class ManagementPasswordCommandsTest(unittest.TestCase):
def test_infrastructure_and_companion_publish_only_confirmed_saves(self):
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
start = cli.index(' } else if (memcmp(command, "password ", 9) == 0) {')
end = cli.index(' } else if (memcmp(command, "clear stats",', start)
infrastructure = cli[start:end].replace('} else if (', 'if (', 1) + '\n }\n'
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
start = companion.index('bool MyMesh::handleLocalControlCommand(')
end = companion.index(' if (mesh::powerdiag::handleCommand(', start)
companion = companion[start:end] + ' return false;\n}\n'
source = r'''
#include <helpers/esp32/ManagementPassword.h>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <initializer_list>
using namespace mesh::esp32::managementPassword;
bool save_ok = true; int saves = 0;
bool durableSave() {
++saves; Snapshot current; assert(snapshot(current));
UploadGuard blocked(current.generation); assert(!blocked.acquired());
return save_ok;
}
namespace mesh { namespace esp32 { namespace companionAdminPassword {
bool save(const char*) { return durableSave(); }
} } }
struct StrHelper {
static void strncpy(char* out, const char* in, size_t size) { ::strncpy(out, in, size); out[size - 1] = 0; }
};
struct NodePrefs { char password[16] = "password"; };
class CommonCLI {
public:
NodePrefs prefs; NodePrefs* _prefs = &prefs;
bool trySavePrefs() { return durableSave(); }
void command(char* command, char* reply) {
@INFRASTRUCTURE@
}
};
class MyMesh {
public:
char _management_password[16] = "password";
bool _management_password_ready = true;
bool handleLocalControlCommand(const char*, char*, size_t);
void command(char* command, char* reply) { handleLocalControlCommand(command, reply, 160); }
};
@COMPANION@
void unchanged(uint32_t generation) {
Snapshot current; assert(snapshot(current));
assert(current.generation == generation && !strcmp(current.password, "password"));
}
template <typename Role> void test(Role& role) {
char command[80], reply[160]; Snapshot initial;
assert(publish("password") && snapshot(initial));
for (const char* value : {"", "1234567890123456", "bad\nvalue", "bad\rvalue"}) {
snprintf(command, sizeof(command), "password %s", value);
reply[0] = 0; const int before = saves; role.command(command, reply);
assert(!strncmp(reply, "Error:", 6) && saves == before); unchanged(initial.generation);
}
save_ok = false; strcpy(command, "password replacement"); role.command(command, reply);
assert(strstr(reply, "unconfirmed")); unchanged(initial.generation);
save_ok = true;
{
UploadGuard upload(initial.generation); assert(upload.acquired());
const int before = saves; role.command(command, reply);
assert(strstr(reply, "busy") && saves == before); unchanged(initial.generation);
}
role.command(command, reply); assert(!strcmp(reply, "password now: replacement"));
Snapshot changed; assert(snapshot(changed));
assert(!strcmp(changed.password, "replacement") && changed.generation == initial.generation + 1);
role.command(command, reply); assert(snapshot(changed) && changed.generation == initial.generation + 1);
strcpy(command, "password leading space"); role.command(command, reply);
assert(snapshot(changed) && !strcmp(changed.password, " leading space"));
}
int main() {
CommonCLI infrastructure; test(infrastructure);
assert(!strcmp(infrastructure.prefs.password, " leading space"));
MyMesh companion; test(companion);
assert(companion._management_password_ready && !strcmp(companion._management_password, " leading space"));
}
'''.replace("@INFRASTRUCTURE@", infrastructure).replace("@COMPANION@", companion)
with tempfile.TemporaryDirectory(prefix="management-command-") as temporary:
work = Path(temporary)
(work / "test.cpp").write_text(source)
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++11", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-DMESH_MANAGEMENT_PASSWORD_HOST_TEST=1", "-DESP_PLATFORM=1", "-DESP32_PLATFORM=1", '-DWIFI_SSID=""',
"-I", str(ROOT / "src"), str(work / "test.cpp"), "-o", str(work / "test")],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(work / "test")], capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()