#include "ManagementReporter.h" #include "CommonCLI.h" #include "PersistentStoreFormat.h" #include #include #include #include using namespace mesh; using namespace mesh::management; namespace mesh { UsbLoggingStatus test_usb_status; unsigned usb_status_calls = 0; UsbLoggingStatus usbLoggingStatus() { ++usb_status_calls; return test_usb_status; } } struct Fixture { MemoryFS fs; Mesh mesh; MainBoard board; SensorManager sensors; ClientACL acl; NodePrefs prefs; CommonCLI cli; CommonCLICallbacks callbacks; std::unique_ptr reporter; Fixture() { test_millis = 0; test_usb_status = UsbLoggingStatus(); usb_status_calls = 0; reboot(); } void reboot() { reporter.reset(); test_millis = 0; reporter.reset(new ManagementReporter(mesh, board, sensors, acl, prefs, callbacks, cli, &fs)); } std::string cmd(const char* text, bool ok = true) { char b[200], reply[160] = {}; strcpy(b, text); assert(reporter->command(b, reply, sizeof(reply))); if (!strncmp(text, "set ", 4) && ((!strncmp(reply, "OK", 2)) != ok)) { fprintf(stderr, "%s: %s\n", text, reply); assert(false); } if (!strncmp(text, "set mgmt.password ", 18)) assert(b[18] == 0); return reply; } void advance(uint32_t seconds) { while (seconds) { const uint32_t step = seconds < 60 ? seconds : 60; test_millis += step * 1000; reporter->loop(); seconds -= step; } } void configure(bool direct = true) { cmd("set mgmt.enabled on", false); cmd("set mgmt.password short", false); cmd("set mgmt.password management test password"); if (direct) cmd("set mgmt.interval 5"); cmd("set mgmt.enabled on"); // Simulate a route becoming unavailable after enable so fallback-only // tests exercise the scoped 21-day safety report. if (!direct) cli.path_len = 0xff; const auto& file = fs.files["/management"]; assert(std::string(file.begin(), file.end()).find("management test password") == std::string::npos); } std::vector installLegacyRoute() { auto legacy = fs.files["/management"]; memcpy(legacy.data(), "MGC1", 4); legacy[7] = 1; legacy[8] = 0x12; write32(legacy.data() + 116, storage::updateCRC32(0xffffffff, legacy.data(), 116)); fs.files["/management"] = legacy; return legacy; } }; int main() { { Fixture f; assert(f.cmd("get mgmt").find("direct=5d") != std::string::npos); assert(f.cmd("get mgmt").find("flood=21d") != std::string::npos); f.advance(100 * DAY); assert(f.mesh.packets.empty()); f.configure(); f.cmd("set mgmt.interval 91", false); f.advance(5 * DAY - 60); assert(f.mesh.packets.empty()); // Changes to RTC in either direction have no effect on flood eligibility. f.mesh.rtc.time = 0; f.advance(60); assert(f.mesh.packets.size() == 1 && !f.mesh.packets[0].flood); f.mesh.rtc.time = 0xffffffff; f.reboot(); f.advance(5 * DAY + 3600); assert(f.mesh.packets.size() == 2); for (const auto& p : f.mesh.packets) assert(!p.flood); } { Fixture f; f.configure(); f.advance(21 * DAY + 601); assert(f.mesh.packets.size() == 5 && !f.mesh.packets[0].flood && f.mesh.packets.back().flood); const uint32_t seq = read32(f.mesh.packets.back().payload + 20); f.reboot(); f.advance(600); assert(f.mesh.packets.size() == 5); f.cmd("set mgmt.enabled off"); f.cmd("set mgmt.enabled on"); f.advance(600); assert(f.mesh.packets.size() == 5 && read32(f.mesh.packets.back().payload + 20) == seq); } { Fixture f; f.configure(); f.cmd("set mgmt.flood off"); f.advance(21 * DAY + 3600); assert(f.mesh.packets.size() == 4); for (const auto& p : f.mesh.packets) assert(!p.flood); } { Fixture f; f.configure(); f.cmd("set mgmt.direct off"); f.advance(21 * DAY + 60); assert(f.mesh.packets.size() == 1); assert(f.mesh.packets[0].flood); } { Fixture f; f.configure(); f.cmd("set mgmt.direct 10"); f.cmd("set mgmt.flood 30"); f.advance(30 * DAY + 60); assert(f.mesh.packets.size() == 3 && f.mesh.packets.back().flood); assert(!f.mesh.packets[0].flood && !f.mesh.packets[1].flood); } { Fixture f; f.configure(false); f.advance(21 * DAY - 60); f.fs.fail_write = true; f.advance(120); assert(f.mesh.packets.empty()); assert(f.cmd("get mgmt").find("FAULT") != std::string::npos); // Last hourly checkpoint was one hour before the failed reservation. f.fs.fail_write = false; f.reboot(); f.advance(3600); assert(f.mesh.packets.size() == 1); } { Fixture f; f.configure(false); f.fs.files["/management"][72] ^= 1; f.reboot(); f.advance(100 * DAY); assert(f.mesh.packets.empty()); assert(f.cmd("get mgmt").find("FAULT") != std::string::npos); } { Fixture f; f.configure(false); f.fs.fail_read_open = true; f.reboot(); f.advance(100 * DAY); assert(f.mesh.packets.empty()); } { Fixture f; for (unsigned i = 0; i < 36; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); } test_usb_status.supported = true; test_usb_status.watchdog_enabled = true; test_usb_status.host_connected = test_usb_status.reader_connected = true; test_usb_status.stalled = true; test_usb_status.retry_seconds = 7200; test_usb_status.inactive_seconds = 3600; test_usb_status.backoff_step = 1; test_usb_status.last_event.reasons = UsbLoggingWatchdogEvent::TX_STALLED; test_usb_status.last_event.action = UsbLoggingWatchdogEvent::REENUMERATE; test_usb_status.last_event.sequence = 17; test_usb_status.last_event.uptime_seconds = 300; test_usb_status.last_event.persisted = true; f.configure(false); f.advance(21 * DAY + 600); assert(f.mesh.packets.size() == 9); assert(usb_status_calls == 1); for (unsigned i = 0; i < 9; ++i) { const auto& p = f.mesh.packets[i]; assert(validPage(p.payload, p.payload_len)); assert(p.payload[78] == i && p.payload[80] == 36 && p.flood); assert(!memcmp(p.payload, "MGR2", 4) && read16(p.payload + HEADER) == 61); assert(!(read16(p.payload + HEADER) & 1024)); // an open reader is not an active logger assert(read32(p.payload + HEADER + 3) == 7200); assert(read32(p.payload + HEADER + 7) == 3600); UsbLoggingWatchdogEvent event; assert(decodeUsbWatchdogEvent(p.payload + WATCHDOG_EVENT_OFFSET, event)); assert(event.reasons == UsbLoggingWatchdogEvent::TX_STALLED && event.action == UsbLoggingWatchdogEvent::REENUMERATE); assert(event.sequence == 17 && event.uptime_seconds == 300 && event.persisted && event.epoch == 0); assert(floodSize(p.payload_len) <= 179); } } { Fixture f; for (unsigned i = 0; i < 7; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); } f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1); assert(!memcmp(f.mesh.packets[0].payload, "MGR2", 4)); assert(read16(f.mesh.packets[0].payload + HEADER) == 0); // unsupported is explicit test_usb_status.supported = true; test_usb_status.retry_seconds = 999; test_usb_status.watchdog_auto = true; test_usb_status.auto_connected_seconds = 100; test_usb_status.host_connected = test_usb_status.reader_connected = test_usb_status.logger_active = true; test_usb_status.last_event.reasons = UsbLoggingWatchdogEvent::CLIENT_INACTIVE; test_usb_status.last_event.action = UsbLoggingWatchdogEvent::SOFT_RECOVERY; test_usb_status.last_event.sequence = 1; test_usb_status.last_event.epoch = 1700000001; f.board.voltage = 2900; f.board.temperature = 60; f.advance(60); assert(f.mesh.packets.size() == 2); assert(read16(f.mesh.packets[1].payload + HEADER) == 0); // frozen report snapshot assert(read32(f.mesh.packets[1].payload + HEADER + 11) == 0); assert(read32(f.mesh.packets[1].payload + WATCHDOG_EVENT_OFFSET + 9) == 0); assert(usb_status_calls == 1); // The low reading was not in the first snapshot and must survive into the next one. f.board.voltage = 3740; f.board.temperature = 24; f.advance(21 * DAY + 3600); assert(f.mesh.packets.size() == 4); assert(read16(f.mesh.packets.back().payload + 66) == 2900); assert(f.mesh.packets.back().payload[69] == temperature(60)); assert(read16(f.mesh.packets.back().payload + HEADER) == 1561); assert(read32(f.mesh.packets.back().payload + HEADER + 11) == 100); assert(read32(f.mesh.packets.back().payload + WATCHDOG_EVENT_OFFSET + 9) == 1); assert(usb_status_calls == 2); } { Fixture f; f.configure(false); f.mesh.temp = true; f.advance(22 * DAY); assert(f.mesh.packets.empty()); f.mesh.temp = false; f.mesh.budget = 0; f.advance(60); assert(f.mesh.packets.empty()); f.mesh.budget = 1000; f.advance(60); assert(f.mesh.packets.size() == 1); } { Fixture f; f.configure(false); f.cmd("set mgmt.flood 90"); f.advance(89 * DAY); assert(f.mesh.packets.empty()); f.advance(DAY); assert(f.mesh.packets.size() == 1); } { Fixture f; f.configure(); const auto saved = f.fs.files; const std::string state = f.cmd("get mgmt"); for (const char* field : {"direct", "flood", "interval"}) { for (const char* value : {"", "-", "-5", "-4294967291", "-4294967275", "+21", " 21", "21 ", "21x", "4294967301", "4294967317", "18446744073709551637", "999999999999999999999999"}) { const std::string command = std::string("set mgmt.") + field + " " + value; f.cmd(command.c_str(), false); assert(f.cmd("get mgmt") == state && f.fs.files == saved); } } f.cmd("set mgmt.direct 0005"); f.cmd("set mgmt.flood 0021"); f.cmd("set mgmt.interval 0005"); assert(f.cmd("get mgmt").find("direct=5d") != std::string::npos); assert(f.cmd("get mgmt").find("flood=21d") != std::string::npos); f.cmd("set mgmt.direct 90"); f.cmd("set mgmt.flood 90"); f.cmd("set mgmt.direct 4", false); f.cmd("set mgmt.flood 20", false); f.cmd("set mgmt.interval 91", false); } { Fixture f; for (unsigned i = 0; i < 36; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); } f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1); const auto saved = f.fs.files; const std::string state = f.cmd("get mgmt"); for (const char* command : {"set mgmt.unknown value", "set mgmt.enabled maybe", "set mgmt.direct 4", "set mgmt.flood 20", "set mgmt.interval 91", "set mgmt.password short", "set mgmt.direct 5", "set mgmt.enabled on"}) { f.cmd(command, false); assert(f.cmd("get mgmt") == state && f.fs.files == saved); } f.cli.scope_available = false; const std::string unscoped = f.cmd("get mgmt"); f.cmd("set mgmt.flood 21", false); assert(f.cmd("get mgmt") == unscoped && f.fs.files == saved); f.cli.scope_available = true; f.advance(60); assert(f.mesh.packets.size() == 2); f.advance(7 * 60); assert(f.mesh.packets.size() == 9 && usb_status_calls == 1); for (unsigned page = 0; page < 9; ++page) assert(f.mesh.packets[page].payload[78] == page); } for (bool fail_save : {false, true}) { Fixture f; for (unsigned i = 0; i < 8; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); } f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1); const auto saved = f.fs.files; f.fs.fail_write = fail_save; f.cmd("set mgmt.flood 30", !fail_save); f.advance(3600); assert(f.mesh.packets.size() == 1); assert((f.cmd("get mgmt").find("FAULT") != std::string::npos) == fail_save); if (fail_save) assert(f.fs.files == saved); else assert(f.cmd("get mgmt").find("flood=30d") != std::string::npos); } { Fixture f; f.configure(false); f.fs.files["/management"][72] ^= 1; f.reboot(); assert(f.cmd("get mgmt").find("FAULT") != std::string::npos); const auto saved = f.fs.files; f.cmd("set mgmt.password another valid secret", false); f.cmd("set mgmt.password short", false); assert(f.fs.files == saved); } for (bool migration_succeeds : {false, true}) { Fixture f; f.configure(); const auto legacy = f.installLegacyRoute(); f.cli.adoption_succeeds = migration_succeeds; f.reboot(); f.advance(3600); assert(f.cli.adoption_calls == 1 && f.mesh.packets.empty()); if (!migration_succeeds) { // The route write alone failed, while management writes still work. // Do not let an hourly MGC2 rewrite destroy the only saved legacy path. assert(f.fs.files["/management"] == legacy); const std::string fault = f.cmd("get mgmt"); assert(fault.find("FAULT") != std::string::npos); assert(fault.find("> off key=unset") != std::string::npos); f.cli.adoption_succeeds = true; f.reboot(); f.advance(3600); assert(f.cli.adoption_calls == 2); } const uint8_t* path = nullptr; uint8_t path_len = OUT_PATH_UNKNOWN; assert(f.cli.getDataTxPath(path, path_len) && path_len == 1 && path[0] == 0x12); assert(!memcmp(f.fs.files["/management"].data(), "MGC2", 4)); assert(f.cmd("get mgmt").find("FAULT") == std::string::npos); const auto calls = f.cli.adoption_calls; f.reboot(); assert(f.cli.adoption_calls == calls); // no repeated migration } { Fixture f; f.configure(); f.installLegacyRoute(); const uint8_t explicit_path[] = {0x34}; assert(f.cli.adoptLegacyDataTxPath(explicit_path, 1)); // already saved /data_tx f.cli.adoption_succeeds = false; // no additional route write should be needed f.reboot(); f.advance(3600); const uint8_t* path = nullptr; uint8_t path_len = OUT_PATH_UNKNOWN; assert(f.cli.getDataTxPath(path, path_len) && path_len == 1 && path[0] == 0x34); assert(f.cmd("get mgmt").find("FAULT") == std::string::npos); assert(!memcmp(f.fs.files["/management"].data(), "MGC2", 4)); } }