/* A14 / C1:网络配置、MQTT 链路的平台无关部分、三条网络命令。 计划里 A14 的验收原话是「协议层用 sim 传输测通;真连网留到 B 段」。 这里测的是会写错而且上板查不出来的那几件事: · 密码**永远**不出现在任何回应里; · 配置命令**只收 USB**,经 WiFi 来的一律拒并记事件; · 没存进 flash 的配置不生效(重启前后一致); · MQTT 入站消息与 USB 同一套坏字节规矩;topic / 退避 / 遥测节拍的数。 */ #include #include #include "cmd.h" #include "evlog.h" #include "mlink.h" #include "netcfg.h" #include "sim_hal.h" #include "unity.h" static char s_out[CMD_OUT_MAX]; /* 假存储后端:记住存了什么、可以注入失败。 */ static netcfg_t s_saved; static int s_saves; static hal_err_t s_save_err; static int s_changed; static hal_err_t fake_save(void *ctx, const netcfg_t *cfg) { (void)ctx; if (s_save_err != HAL_OK) { return s_save_err; } s_saved = *cfg; s_saves++; return HAL_OK; } static void fake_changed(void *ctx) { (void)ctx; s_changed++; } void setUp(void) { sim_hal_reset(); memset(&s_saved, 0, sizeof(s_saved)); s_saves = 0; s_save_err = HAL_OK; s_changed = 0; netcfg_init(NULL, fake_save, fake_changed, NULL); TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); cmd_init(NULL); } void tearDown(void) { TEST_ASSERT_EQUAL_INT(0, sim_crit_depth()); } static const char *send_on(cmd_link_t link, const char *line) { cmd_handle_line(link, line, s_out, sizeof(s_out)); return s_out; } static void assert_has_at(const char *resp, const char *needle, int line) { UNITY_TEST_ASSERT_NOT_NULL(strstr(resp, needle), line, resp); } static void assert_lacks_at(const char *resp, const char *needle, int line) { UNITY_TEST_ASSERT_NULL(strstr(resp, needle), line, resp); } #define ASSERT_HAS(r, n) assert_has_at((r), (n), __LINE__) #define ASSERT_LACKS(r, n) assert_lacks_at((r), (n), __LINE__) static int count_rejects(void) { evlog_rec_t rec[64]; (void)evlog_flush(0); const uint32_t n = evlog_read(0, rec, 64); int k = 0; for (uint32_t i = 0; i < n; i++) { k += rec[i].type == LOG_EV_CMD_REJECT; } return k; } /* ---- 配置命令 ------------------------------------------------------------ */ /* ⭐ USB 来的 wifi / mqtt:存进后端、内存生效、通知网络那一侧重连。 */ static void test_config_over_usb_is_saved_and_applied(void) { ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm-2g\",\"pass\":\"s3cret-pass\"}"), "\"ok\":true"); ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://10.5.5.13:1883\"," "\"user\":\"rack\",\"pass\":\"m-pass-1\",\"tele_ms\":2000}"), "\"ok\":true"); TEST_ASSERT_EQUAL_INT(2, s_saves); TEST_ASSERT_EQUAL_INT(2, s_changed); TEST_ASSERT_EQUAL_STRING("farm-2g", s_saved.ssid); TEST_ASSERT_EQUAL_STRING("s3cret-pass", s_saved.pass); /* 第二次存的是整份:WiFi 那部分还在 */ TEST_ASSERT_EQUAL_STRING("mqtt://10.5.5.13:1883", s_saved.uri); TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms); netcfg_t c; netcfg_get(&c); TEST_ASSERT_EQUAL_MEMORY(&s_saved, &c, sizeof(c)); } /* ⭐ 经 WiFi 改 WiFi / MQTT 配置:一律 `link`、记事件、后端一次都不碰。 */ static void test_config_over_wifi_is_refused_and_logged(void) { ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"evil\",\"pass\":\"12345678\"}"), "\"err\":\"link\""); ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://1.2.3.4\"}"), "\"err\":\"link\""); TEST_ASSERT_EQUAL_INT(0, s_saves); TEST_ASSERT_EQUAL_INT(0, s_changed); TEST_ASSERT_EQUAL_INT(2, count_rejects()); /* 只读的 net 经 WiFi 照常可用。 */ ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}"), "\"ok\":true"); } /* ⭐ 密码永不外泄:写进去之后 net 回应里只有 *_set,没有明文;换哪条链路问都一样。 */ static void test_net_never_echoes_a_password(void) { send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"TopSecret99\"}"); send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h:1883\",\"user\":\"u1\"," "\"pass\":\"BrokerPw77\"}"); for (int link = CMD_LINK_TEST; link <= CMD_LINK_WIFI; link++) { const char *r = send_on((cmd_link_t)link, "{\"cmd\":\"net\"}"); ASSERT_LACKS(r, "TopSecret99"); ASSERT_LACKS(r, "BrokerPw77"); ASSERT_HAS(r, "\"ssid\":\"farm\",\"pass_set\":true"); ASSERT_HAS(r, "\"user\":\"u1\",\"mpass_set\":true"); ASSERT_HAS(r, "\"uri\":\"mqtt://h:1883\""); ASSERT_HAS(r, "\"tele_ms\":1000"); /* 没给 → 默认 */ } /* status 里也不许有。 */ ASSERT_LACKS(send_on(CMD_LINK_USB, "{\"cmd\":\"status\"}"), "TopSecret99"); } static void test_config_validation(void) { static const char *const bad[] = { "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"\",\"pass\":\"12345678\"}", /* 空 SSID */ "{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"1234567\"}", /* 7 位口令 */ "{\"seq\":3,\"cmd\":\"mqtt\",\"uri\":\"http://10.5.5.13\"}", /* 不是 mqtt:// */ "{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"ws://10.5.5.13\"}", /* 不认的 scheme */ "{\"seq\":5,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":100}", /* 太密 */ "{\"seq\":6,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":60001}", "{\"seq\":7,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":-1}", }; for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { ASSERT_HAS(send_on(CMD_LINK_USB, bad[i]), "\"err\":\"range\""); } ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":8,\"cmd\":\"wifi\",\"pass\":\"12345678\"}"), "\"err\":\"arg\""); ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":9,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":1}"), "\"err\":\"arg\""); ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":12,\"cmd\":\"mqtt\",\"uri\":\"mqtts://\"}"), "\"err\":\"range\""); /* 只有 scheme 没有主机 */ ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":13,\"cmd\":\"mqtt\",\"uri\":\"mqtts://home.example:8883\"}"), "\"ok\":true"); /* 开放网络(空口令)是合法的;63 位口令顶到上限也合法。 */ ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":10,\"cmd\":\"wifi\",\"ssid\":\"open\"}"), "\"ok\":true"); char line[200]; char pass[64]; memset(pass, 'p', 63); pass[63] = '\0'; snprintf(line, sizeof(line), "{\"seq\":11,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"%s\"}", pass); ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true"); TEST_ASSERT_EQUAL_STRING(pass, s_saved.pass); TEST_ASSERT_EQUAL_INT(3, s_saves); } /* 存不下:回 io,内存里的配置**不变**,也不通知重连(重启前后一致)。 */ static void test_unsaved_config_does_not_take_effect(void) { send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"old\",\"pass\":\"12345678\"}"); s_save_err = HAL_ERR_BUS; ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"new\",\"pass\":\"12345678\"}"), "\"err\":\"io\""); netcfg_t c; netcfg_get(&c); TEST_ASSERT_EQUAL_STRING("old", c.ssid); TEST_ASSERT_EQUAL_INT(1, s_changed); } /* NVS 里读回来的东西不可信:没带结尾 0 的字段被强制收尾;遥测周期越界回默认; 引号 / 反斜杠 / 控制字符在 net 回应里换成 `?`,回应仍是一行合法 JSON。 */ static void test_loaded_config_is_sanitised(void) { netcfg_t junk; memset(&junk, 'A', sizeof(junk)); junk.tele_ms = 5; memcpy(junk.ssid, "a\"b\\c\n", 6); netcfg_init(&junk, NULL, NULL, NULL); netcfg_t c; netcfg_get(&c); TEST_ASSERT_EQUAL_size_t(NETCFG_SSID_MAX, strlen(c.ssid)); TEST_ASSERT_EQUAL_size_t(NETCFG_URI_MAX, strlen(c.uri)); TEST_ASSERT_EQUAL_UINT32(NETCFG_TELE_DEFAULT_MS, c.tele_ms); const char *r = send_on(CMD_LINK_USB, "{\"cmd\":\"net\"}"); ASSERT_HAS(r, "\"ssid\":\"a?b?c?AAAA"); TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]); } /* ---- MQTT 链路的平台无关部分 --------------------------------------------- */ static void test_topics_follow_the_mac(void) { const uint8_t mac[6] = { 0xB8, 0x1F, 0x3F, 0xC9, 0xC8, 0xBC }; mlink_init(mac); TEST_ASSERT_EQUAL_STRING("b81f3fc9c8bc", mlink_id()); TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/cmd", mlink_topic(MLINK_T_CMD)); TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/rsp", mlink_topic(MLINK_T_RSP)); TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/tele", mlink_topic(MLINK_T_TELE)); TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/state", mlink_topic(MLINK_T_STATE)); TEST_ASSERT_TRUE(strlen(mlink_topic(MLINK_T_STATE)) < MLINK_TOPIC_MAX); char buf[96]; TEST_ASSERT_TRUE(mlink_state_online("0.1.0-a14", "10.5.5.77", buf, sizeof(buf)) > 0); TEST_ASSERT_EQUAL_STRING("{\"online\":true,\"fw\":\"0.1.0-a14\",\"ip\":\"10.5.5.77\"}", buf); } /* 入站消息:与 USB 同一套坏字节规矩;切片的 / 超长的直接拒。 */ static void test_inbound_messages(void) { char line[CMD_LINE_MAX]; const char *ok = "{\"cmd\":\"status\"}"; TEST_ASSERT_NULL(mlink_inbound(ok, (int)strlen(ok), (int)strlen(ok), line, sizeof(line))); TEST_ASSERT_EQUAL_STRING(ok, line); /* 不带结尾 0 的负载也被正确收尾 */ const char nul[] = "{\"cmd\":\"status\"}\0junk"; TEST_ASSERT_EQUAL_STRING("bad_frame", mlink_inbound(nul, sizeof(nul) - 1, sizeof(nul) - 1, line, sizeof(line))); TEST_ASSERT_EQUAL_STRING("bad_frame", mlink_inbound("{\n}", 3, 3, line, sizeof(line))); TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(ok, 5, 100, line, sizeof(line))); /* 切片 */ TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(ok, 0, 0, line, sizeof(line))); static char big[CMD_LINE_MAX + 8]; memset(big, 'x', sizeof(big)); TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(big, CMD_LINE_MAX, CMD_LINE_MAX, line, sizeof(line))); TEST_ASSERT_NULL(mlink_inbound(big, CMD_LINE_MAX - 1, CMD_LINE_MAX - 1, line, sizeof(line))); } static void test_backoff_doubles_and_caps(void) { static const uint32_t want[] = { 1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000 }; for (uint32_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) { TEST_ASSERT_EQUAL_UINT32(want[i], mlink_backoff_ms(i)); } TEST_ASSERT_EQUAL_UINT32(MLINK_BACKOFF_MAX_MS, mlink_backoff_ms(0xFFFFFFFFu)); } /* 遥测节拍:一连上先报一条;之后按整周期推进(被挤晚了不漂);落后一整周期以上 就对齐到现在、不补发;32 位毫秒回绕照样对。 */ static void test_telemetry_cadence(void) { mlink_tele_t t = { 0 }; TEST_ASSERT_TRUE(mlink_tele_due(&t, 5000, 1000)); TEST_ASSERT_FALSE(mlink_tele_due(&t, 5999, 1000)); TEST_ASSERT_TRUE(mlink_tele_due(&t, 6030, 1000)); /* 晚 30 ms */ TEST_ASSERT_EQUAL_UINT32(6000, t.last_ms); /* 相位没漂 */ TEST_ASSERT_FALSE(mlink_tele_due(&t, 6999, 1000)); TEST_ASSERT_TRUE(mlink_tele_due(&t, 12000, 1000)); /* 卡了 5 s */ TEST_ASSERT_EQUAL_UINT32(12000, t.last_ms); /* 对齐到现在,不补发 */ TEST_ASSERT_FALSE(mlink_tele_due(&t, 12500, 1000)); mlink_tele_t w = { 0 }; TEST_ASSERT_TRUE(mlink_tele_due(&w, 0xFFFFFF00u, 1000)); TEST_ASSERT_FALSE(mlink_tele_due(&w, 0x00000100u, 1000)); /* 回绕后才过了 512 ms */ TEST_ASSERT_TRUE(mlink_tele_due(&w, 0x00000300u, 1000)); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_config_over_usb_is_saved_and_applied); RUN_TEST(test_config_over_wifi_is_refused_and_logged); RUN_TEST(test_net_never_echoes_a_password); RUN_TEST(test_config_validation); RUN_TEST(test_unsaved_config_does_not_take_effect); RUN_TEST(test_loaded_config_is_sanitised); RUN_TEST(test_topics_follow_the_mac); RUN_TEST(test_inbound_messages); RUN_TEST(test_backoff_doubles_and_caps); RUN_TEST(test_telemetry_cadence); return UNITY_END(); }