/* 0.1.19:配网热点(prov.h)的平台无关部分,加上「配置命令从哪条链路收」的新口径。 测的是上板不好查、写错了又很疼的几件事: · 热点**什么时候开、什么时候关** —— 开早了白白露一个入口,关早了人正填着表单就断; · STA 连不上时热点**不许自己消失**(消失 = 又只剩拆机插 USB); · 网页表单拼出来的命令行**拼不出别的命令**,带引号的字段进不来; · 配置命令:热点链路收、MQTT 照旧拒;`keep_pass` 不丢旧口令;`net` 报热点状态。 */ #include #include #include "cmd.h" #include "evlog.h" #include "netcfg.h" #include "prov.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 fake_save(void *ctx, const netcfg_t *cfg) { (void)ctx; s_saved = *cfg; s_saves++; return HAL_OK; } void setUp(void) { sim_hal_reset(); memset(&s_saved, 0, sizeof(s_saved)); s_saves = 0; netcfg_init(NULL, fake_save, NULL, NULL); TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); cmd_init(NULL); prov_init(0); } void tearDown(void) {} 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; } #define ASSERT_HAS(r, n) UNITY_TEST_ASSERT_NOT_NULL(strstr((r), (n)), __LINE__, (r)) #define ASSERT_LACKS(r, n) UNITY_TEST_ASSERT_NULL(strstr((r), (n)), __LINE__, (r)) static prov_out_t step(int n_wifi, bool up, int sta, bool touched, uint32_t now) { const prov_in_t in = { .n_wifi = n_wifi, .wifi_up = up, .ap_sta = sta, .touched = touched }; return prov_step(&in, now); } /* ---- 判据 ------------------------------------------------------------------ */ /* ⭐ 一组都没配:第一拍就开,不等 60 s。 */ static void test_no_config_opens_at_once(void) { const prov_out_t o = step(0, false, 0, false, 0); TEST_ASSERT_TRUE(o.ap); TEST_ASSERT_EQUAL_INT(PROV_WHY_NOCFG, o.why); TEST_ASSERT_FALSE(o.hold); } /* ⭐ 配了但连不上:满 60 s 才开;之后一直开着,哪怕再过几天。 */ static void test_failure_opens_after_a_minute_and_stays(void) { TEST_ASSERT_FALSE(step(1, false, 0, false, 0).ap); TEST_ASSERT_FALSE(step(1, false, 0, false, PROV_FAIL_MS - 1).ap); prov_out_t o = step(1, false, 0, false, PROV_FAIL_MS); TEST_ASSERT_TRUE(o.ap); TEST_ASSERT_EQUAL_INT(PROV_WHY_FAIL, o.why); /* 时钟回绕(49.7 天)之后「断了多久」会变小 —— 不许因此关掉。 */ TEST_ASSERT_TRUE(step(1, false, 0, false, 0xFFFFFF00u).ap); TEST_ASSERT_TRUE(step(1, false, 0, false, 0x00000100u).ap); } /* 连上过又掉:从掉的那一刻重新数 60 s(路由器重启一下不该立刻冒热点)。 */ static void test_a_drop_restarts_the_minute(void) { TEST_ASSERT_FALSE(step(1, true, 0, false, 1000).ap); TEST_ASSERT_FALSE(step(1, true, 0, false, 500000).ap); TEST_ASSERT_FALSE(step(1, false, 0, false, 500100).ap); TEST_ASSERT_FALSE(step(1, false, 0, false, 500100 + PROV_FAIL_MS - 1).ap); TEST_ASSERT_TRUE(step(1, false, 0, false, 500100 + PROV_FAIL_MS).ap); /* 没满 60 s 就连回来了:计时作废。 */ prov_init(0); (void)step(1, false, 0, false, 0); (void)step(1, true, 0, false, 30000); (void)step(1, false, 0, false, 40000); TEST_ASSERT_FALSE(step(1, false, 0, false, 70000).ap); /* 离启动 70 s,但离这次掉线才 30 s */ } /* ⭐ STA 连上了:热点上没人立刻关;有人再留 60 s(让网页看到「连上了」),到点照关。 */ static void test_closes_when_sta_comes_up(void) { TEST_ASSERT_TRUE(step(1, false, 0, false, PROV_FAIL_MS).ap); TEST_ASSERT_FALSE(step(1, true, 0, false, PROV_FAIL_MS + 100).ap); prov_init(0); TEST_ASSERT_TRUE(step(1, false, 1, false, PROV_FAIL_MS).ap); prov_out_t o = step(1, true, 1, false, 100000); TEST_ASSERT_TRUE(o.ap); TEST_ASSERT_EQUAL_INT(PROV_WHY_LINGER, o.why); TEST_ASSERT_FALSE(o.hold); /* 已经连上了,没有重连可放慢 */ TEST_ASSERT_TRUE(step(1, true, 1, false, 100000 + PROV_LINGER_MS - 1).ap); TEST_ASSERT_FALSE(step(1, true, 1, false, 100000 + PROV_LINGER_MS).ap); /* 留的那一分钟里人走了:马上关,不等到点。 */ prov_init(0); (void)step(1, false, 1, false, PROV_FAIL_MS); TEST_ASSERT_TRUE(step(1, true, 1, false, 100000).ap); TEST_ASSERT_FALSE(step(1, true, 0, false, 101000).ap); /* 关了就是关了:连着的时候不会再自己开。 */ TEST_ASSERT_FALSE(step(1, true, 1, false, 102000).ap); } /* ⭐ 没配网时开的热点:人在网页上刚存了第一组 WiFi(n 0 → 1,离启动还不到 60 s)—— 不许在这一刻把热点关掉,要等 STA 真的连上。 */ static void test_first_config_does_not_close_the_ap(void) { TEST_ASSERT_TRUE(step(0, false, 1, true, 5000).ap); prov_out_t o = step(1, false, 1, true, 6000); TEST_ASSERT_TRUE(o.ap); TEST_ASSERT_EQUAL_INT(PROV_WHY_FAIL, o.why); TEST_ASSERT_TRUE(o.hold); TEST_ASSERT_TRUE(step(1, true, 1, false, 9000).ap); /* 连上了:linger */ } /* 放慢重连:热点上有人 + 最近 5 分钟动过网页。连上热点本身算一次动作;状态轮询不算。 */ static void test_hold_follows_activity(void) { TEST_ASSERT_FALSE(step(1, false, 0, false, PROV_FAIL_MS).hold); /* 没人 */ TEST_ASSERT_TRUE(step(1, false, 1, false, 70000).hold); /* 刚连上 */ TEST_ASSERT_TRUE(step(1, false, 1, false, 70000 + PROV_HOLD_MS - 1).hold); TEST_ASSERT_FALSE(step(1, false, 1, false, 70000 + PROV_HOLD_MS).hold); /* 连着但 5 分钟没动:恢复正常节奏 */ TEST_ASSERT_TRUE(step(1, false, 1, true, 900000).hold); /* 又点了一下 */ TEST_ASSERT_FALSE(step(1, false, 1, false, 900000 + PROV_HOLD_MS).hold); /* 过期的窗口不许在时钟回绕后复活(人一直连着没走)。 */ TEST_ASSERT_FALSE(step(1, false, 1, false, 900000u + 0xFFFFFF00u).hold); TEST_ASSERT_TRUE(step(1, false, 1, true, 5000).hold); TEST_ASSERT_FALSE(step(1, false, 0, false, 6000).hold); /* 人走了 */ TEST_ASSERT_TRUE(step(1, false, 0, false, 7000).ap); /* 热点还在 */ TEST_ASSERT_GREATER_OR_EQUAL_UINT32(10000, PROV_SLOW_MS); /* 一次连接尝试 ≈3 s,间隔得明显比它长 */ } /* ---- 名字与口令 ------------------------------------------------------------ */ static void test_ssid_and_password(void) { char ssid[PROV_SSID_MAX]; prov_ssid("b81f3fc9c8bc", ssid); TEST_ASSERT_EQUAL_STRING("PhoneRail-c9c8bc", ssid); prov_ssid("", ssid); TEST_ASSERT_EQUAL_STRING("PhoneRail-", ssid); /* 口令是固定值:WPA2 收 8~63 位;装得进 net_status_t,也不需要 JSON 转义。 */ TEST_ASSERT_EQUAL_STRING("88888888", PROV_PASS); TEST_ASSERT_TRUE(strlen(PROV_PASS) >= 8 && strlen(PROV_PASS) <= 63); TEST_ASSERT_NULL(strpbrk(PROV_PASS, "\"\\")); } /* ---- 表单 → 命令行 --------------------------------------------------------- */ static const char *build(const char *body, const char **err) { static char line[CMD_LINE_MAX]; line[0] = '\0'; const size_t n = prov_build_line(body, 77, line, sizeof(line), err); TEST_ASSERT_EQUAL_UINT(n, strlen(line)); return line; } /* ⭐ 拼出来的行能被命令处理器原样吃下去、存进后端。 */ static void test_form_becomes_the_usb_commands(void) { const char *err = "x"; const char *l = build("wifi\nfarm 2g\ns3cret pass", &err); TEST_ASSERT_NULL(err); TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"farm 2g\",\"pass\":\"s3cret pass\"}", l); ASSERT_HAS(send_on(CMD_LINK_AP, l), "\"ok\":true,\"n\":1"); TEST_ASSERT_EQUAL_STRING("s3cret pass", s_saved.wifi[0].pass); /* 开放网络:口令那一行可以是空的,也可以整行没有;行尾的 \r 不算内容。 */ TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"open\",\"pass\":\"\"}", build("wifi\nopen\n", &err)); TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"open\",\"pass\":\"\"}", build("wifi\r\nopen\r", &err)); /* 中文名字(UTF-8)原样过。 */ ASSERT_HAS(build("wifi\n\xE5\xAE\xB6\n12345678", &err), "\"ssid\":\"\xE5\xAE\xB6\""); TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"farm 2g\",\"del\":true}", build("del\nfarm 2g", &err)); cmd_init(NULL); /* 清判重窗口:这里每条都用 seq 77 */ ASSERT_HAS(send_on(CMD_LINK_AP, build("del\nfarm 2g", &err)), "\"ok\":true,\"n\":0"); l = build("mqtt\nmqtts://home.example:8883\nboard-b\nBrokerPw77\n2000", &err); TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"mqtt\",\"uri\":\"mqtts://home.example:8883\",\"user\":\"board-b\"," "\"pass\":\"BrokerPw77\",\"tele_ms\":2000}", l); cmd_init(NULL); /* 清判重窗口:这里每条都用 seq 77 */ ASSERT_HAS(send_on(CMD_LINK_AP, l), "\"ok\":true"); TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms); } /* ⭐ MQTT 口令留空 = 不改:只换地址时旧口令还在;给了 keep_pass 又给 pass 是自相矛盾。 */ static void test_keep_pass_keeps_the_stored_password(void) { send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"mqtt\",\"uri\":\"mqtt://old:1883\",\"user\":\"u\",\"pass\":\"BrokerPw77\"}"); const char *err = NULL; const char *l = build("mqtt\nmqtt://new:1883\nu2\n\n", &err); TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"mqtt\",\"uri\":\"mqtt://new:1883\",\"user\":\"u2\"," "\"keep_pass\":true,\"tele_ms\":0}", l); const char *r = send_on(CMD_LINK_AP, l); ASSERT_HAS(r, "\"ok\":true"); ASSERT_LACKS(r, "BrokerPw77"); TEST_ASSERT_EQUAL_STRING("mqtt://new:1883", s_saved.uri); TEST_ASSERT_EQUAL_STRING("u2", s_saved.user); TEST_ASSERT_EQUAL_STRING("BrokerPw77", s_saved.mpass); ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":3,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"pass\":\"other\"," "\"keep_pass\":true}"), "\"err\":\"arg\""); ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"keep_pass\":1}"), "\"err\":\"arg\""); TEST_ASSERT_EQUAL_STRING("BrokerPw77", s_saved.mpass); /* keep_pass:false 就是老行为:没给 pass = 清空。 */ ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":5,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"keep_pass\":false}"), "\"ok\":true"); TEST_ASSERT_EQUAL_STRING("", s_saved.mpass); } /* ⭐ 拼不出别的命令;能把 JSON 撑破的字符进不来。 */ static void test_form_rejects_what_it_should(void) { static const struct { const char *body; const char *err; } k[] = { { "", "bad_form" }, { "wifi", "bad_form" }, { "wifi\n\n12345678", "bad_form" }, /* 没名字 */ { "wifi\na\nb\nc", "bad_form" }, /* 多了一行 */ { "del\na\nb", "bad_form" }, { "mqtt\n", "bad_form" }, { "mqtt\nmqtt://h\nu\np\n12x", "bad_form" }, /* tele 不是数字 */ { "mqtt\nmqtt://h\nu\np\n99999999999999", "bad_form" }, { "power\n1", "bad_form" }, /* 别的命令 */ { "status", "bad_form" }, { "wifi\na\"b\n12345678", "bad_char" }, { "wifi\na\n1234\\5678", "bad_char" }, { "wifi\na\tb\n12345678", "bad_char" }, { "wifi\na\n12345678\",\"cmd\":\"power", "bad_char" }, /* 想往行里塞第二个键 */ { "wifi\na\nb\nc\nd\ne", "too_long" }, /* 行数超了 */ { "wifi\n0123456789012345678901234567890123456789012345678901234567890123456789\nx", "too_long" }, }; for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) { const char *err = NULL; char line[CMD_LINE_MAX] = "junk"; TEST_ASSERT_EQUAL_UINT_MESSAGE(0, prov_build_line(k[i].body, 1, line, sizeof(line), &err), k[i].body); TEST_ASSERT_EQUAL_STRING_MESSAGE(k[i].err, err, k[i].body); } /* seq 0 不是合法序号;缓冲不够不越界。 */ char small[16]; TEST_ASSERT_EQUAL_UINT(0, prov_build_line("wifi\na\n12345678", 0, small, sizeof(small), NULL)); TEST_ASSERT_EQUAL_UINT(0, prov_build_line("wifi\na\n12345678", 1, small, sizeof(small), NULL)); TEST_ASSERT_EQUAL_STRING("", small); /* 三个字段都顶到头(64 + 32 + 64)的 mqtt 行也装得进一行命令。 */ char body[256]; char f64[65]; memset(f64, 'p', 64); f64[64] = '\0'; snprintf(body, sizeof(body), "mqtt\nmqtts://%.*s\n%.32s\n%s\n60000", 56, f64, f64, f64); char line[CMD_LINE_MAX]; const char *err = "x"; TEST_ASSERT_GREATER_THAN_UINT(0, prov_build_line(body, 0xFFFFFFFFu, line, sizeof(line), &err)); ASSERT_HAS(send_on(CMD_LINK_AP, line), "\"ok\":true"); TEST_ASSERT_EQUAL_STRING(f64, s_saved.mpass); } static void test_json_escape(void) { char out[64]; TEST_ASSERT_EQUAL_UINT(4, prov_json_esc("farm", out, sizeof(out))); TEST_ASSERT_EQUAL_STRING("farm", out); prov_json_esc("a\"b\\c\n", out, sizeof(out)); TEST_ASSERT_EQUAL_STRING("a\\u0022b\\u005cc\\u000a", out); prov_json_esc("\xE5\xAE\xB6", out, sizeof(out)); TEST_ASSERT_EQUAL_STRING("\xE5\xAE\xB6", out); char tiny[8]; TEST_ASSERT_EQUAL_UINT(0, prov_json_esc("12345678", tiny, sizeof(tiny))); TEST_ASSERT_EQUAL_STRING("", tiny); TEST_ASSERT_EQUAL_UINT(0, prov_json_esc("ab\"", tiny, sizeof(tiny))); /* 转义写到一半不留半截 */ TEST_ASSERT_EQUAL_STRING("", tiny); } /* ---- 链路口径 -------------------------------------------------------------- */ /* ⭐ 配置命令:热点链路和 USB 一样收;MQTT 照旧拒(0.1.19 没有放开这一条)。 */ static void test_ap_link_may_configure_but_mqtt_still_may_not(void) { ASSERT_HAS(send_on(CMD_LINK_AP, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"12345678\"}"), "\"ok\":true"); ASSERT_HAS(send_on(CMD_LINK_AP, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h:1883\"}"), "\"ok\":true"); TEST_ASSERT_EQUAL_INT(2, s_saves); ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":3,\"cmd\":\"wifi\",\"ssid\":\"evil\",\"pass\":\"12345678\"}"), "\"err\":\"link\""); ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"mqtt://1.2.3.4\",\"keep_pass\":true}"), "\"err\":\"link\""); TEST_ASSERT_EQUAL_INT(2, s_saves); TEST_ASSERT_EQUAL_INT(3, CMD_LINK_AP); /* 进事件日志的编号(CMD_REBOOT_ARG),只许追加 */ } /* `net` 报热点状态;热点口令能读到,WiFi / MQTT 口令照旧读不到。 */ static void test_net_reports_the_ap(void) { send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"TopSecret99\"}"); ASSERT_HAS(send_on(CMD_LINK_AP, "{\"cmd\":\"net\"}"), "\"ap\":{\"on\":false,\"why\":\"off\",\"sta\":0,\"ssid\":\"\",\"pass\":\"\"}"); net_status_t st; net_status_get(&st); st.ap_on = true; st.ap_why = PROV_WHY_FAIL; st.ap_sta = 1; snprintf(st.ap_ssid, sizeof(st.ap_ssid), "PhoneRail-c9c8bc"); snprintf(st.ap_pass, sizeof(st.ap_pass), "%s", PROV_PASS); net_status_set(&st); const char *r = send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}"); ASSERT_HAS(r, "\"ap\":{\"on\":true,\"why\":\"fail\",\"sta\":1,\"ssid\":\"PhoneRail-c9c8bc\",\"pass\":\"88888888\"}"); ASSERT_LACKS(r, "TopSecret99"); TEST_ASSERT_TRUE(sizeof(st.ap_ssid) >= PROV_SSID_MAX && sizeof(st.ap_pass) > strlen(PROV_PASS)); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_no_config_opens_at_once); RUN_TEST(test_failure_opens_after_a_minute_and_stays); RUN_TEST(test_a_drop_restarts_the_minute); RUN_TEST(test_closes_when_sta_comes_up); RUN_TEST(test_first_config_does_not_close_the_ap); RUN_TEST(test_hold_follows_activity); RUN_TEST(test_ssid_and_password); RUN_TEST(test_form_becomes_the_usb_commands); RUN_TEST(test_keep_pass_keeps_the_stored_password); RUN_TEST(test_form_rejects_what_it_should); RUN_TEST(test_json_escape); RUN_TEST(test_ap_link_may_configure_but_mqtt_still_may_not); RUN_TEST(test_net_reports_the_ap); return UNITY_END(); }