// ============================================================================ // network_plugin_test.cpp — Network 插件单元测试 // W22.2 (qa-xu): 覆盖 parse_headers_json / SSE 行解析 / 异常保护 // 通过 #include plugin source 访问 file-scope static 函数 // ============================================================================ #define _CRT_SECURE_NO_WARNINGS #define BOOST_ASIO_DISABLE_STD_TO_ADDRESS #include "../plugins/network/src/network_plugin.cpp" #include #include #include #include #include #include static int g_failures = 0; #define CHECK(cond, msg) do { \ if (cond) { \ std::cout << "[OK] " << (msg) << "\n"; \ } else { \ std::cerr << "[FAIL] " << (msg) << "\n"; \ g_failures++; \ } \ } while (0) // ================================================================ // Mock host API — used by Block 8 (exception protection test) // ================================================================ #if 0 // Block 8 disabled static char g_mock_strdup_buf[65536]; static char* mock_strdup(const char* s) { if (!s) return nullptr; std::strncpy(g_mock_strdup_buf, s, sizeof(g_mock_strdup_buf) - 1); g_mock_strdup_buf[sizeof(g_mock_strdup_buf) - 1] = '\0'; return g_mock_strdup_buf; } static void mock_log(int, const char*, ...) { // discard all log output } // dstalk_host_api_t field order: // register_service, query_service, event_subscribe, event_emit, // event_unsubscribe, config_get, config_set, log, // alloc, free, strdup static dstalk_host_api_t g_mock_host = { nullptr, // register_service nullptr, // query_service nullptr, // event_subscribe nullptr, // event_emit nullptr, // event_unsubscribe nullptr, // config_get nullptr, // config_set mock_log, // log nullptr, // alloc nullptr, // free mock_strdup,// strdup }; #endif // ================================================================ // SSE 行分割 helper (复刻 do_post_stream 的 emit_lines 逻辑) // ================================================================ static std::vector split_sse_lines(std::string fragment) { std::vector lines; size_t pos = 0; while (pos < fragment.size()) { size_t nl = fragment.find('\n', pos); if (nl == std::string::npos) break; std::string line = fragment.substr(pos, nl - pos); if (!line.empty() && line.back() == '\r') line.pop_back(); lines.push_back(line); pos = nl + 1; } // 剩余 fragment (无尾随换行) — 对应 do_post_stream 最后的 on_line(fragment) if (pos > 0) { std::string remaining = fragment.substr(pos); if (!remaining.empty() && remaining.back() == '\r') remaining.pop_back(); if (!remaining.empty()) lines.push_back(remaining); } else if (pos == 0 && !fragment.empty()) { // 一个换行都没有 — 整段视为一行 std::string s = fragment; if (!s.empty() && s.back() == '\r') s.pop_back(); if (!s.empty()) lines.push_back(s); } return lines; } // ================================================================ int main() { // ================================================================ // Test Block 1: parse_headers_json — 正常 JSON // ================================================================ std::cout << "\n--- Block 1: parse_headers_json normal JSON ---\n"; { auto h = parse_headers_json("{\"Content-Type\":\"application/json\"}"); CHECK(h.size() == 1, "T1.1: single pair, size=1"); CHECK(h["Content-Type"] == "application/json", "T1.2: value correct"); } { auto h = parse_headers_json( "{\"Authorization\":\"Bearer xyz\",\"X-ID\":\"42\"}"); CHECK(h.size() == 2, "T1.3: two pairs, size=2"); CHECK(h["Authorization"] == "Bearer xyz", "T1.4: first value"); CHECK(h["X-ID"] == "42", "T1.5: second value"); } { auto h = parse_headers_json("{\"empty\":\"\"}"); CHECK(h.size() == 1, "T1.6: empty value parsed, size=1"); CHECK(h["empty"] == "", "T1.7: empty string value"); } { auto h = parse_headers_json( "{\"k1\":\"v1\",\"k2\":\"v2\",\"k3\":\"v3\"}"); CHECK(h.size() == 3, "T1.8: three pairs, size=3"); CHECK(h["k2"] == "v2", "T1.9: middle pair correct"); } { // escaped quote in value: {\"key\":\"val\\\"ue\"} auto h = parse_headers_json("{\"key\":\"val\\\"ue\"}"); CHECK(h.size() == 1, "T1.10: escaped quote in value, size=1"); CHECK(h["key"] == "val\"ue", "T1.11: value includes literal quote"); } { // escaped backslash: {\"path\":\"C:\\\\tmp\"} auto h = parse_headers_json("{\"path\":\"C:\\\\tmp\"}"); CHECK(h.size() == 1, "T1.12: escaped backslash in value"); CHECK(h["path"] == "C:\\tmp", "T1.13: single backslash in result"); } // ================================================================ // Test Block 2: parse_headers_json — 空 / null 输入 // ================================================================ std::cout << "\n--- Block 2: parse_headers_json empty/null input ---\n"; { auto h = parse_headers_json(nullptr); CHECK(h.empty(), "T2.1: nullptr returns empty map"); } { auto h = parse_headers_json(""); CHECK(h.empty(), "T2.2: empty string returns empty map"); } { auto h = parse_headers_json(" "); CHECK(h.empty(), "T2.3: whitespace-only returns empty map (no quotes)"); } { auto h = parse_headers_json("{}"); CHECK(h.empty(), "T2.4: empty object returns empty map"); } { auto h = parse_headers_json("\"not an object\""); CHECK(h.empty(), "T2.5: JSON string literal (not object) returns empty"); } // ================================================================ // Test Block 3: parse_headers_json — 畸形 JSON // ================================================================ std::cout << "\n--- Block 3: parse_headers_json malformed JSON ---" << std::endl; { // Unclosed brace — parser is lenient, ignores braces, finds the pair auto h = parse_headers_json("{\"key\":\"value\""); CHECK(h.size() == 1, "T3.1: unclosed brace, parser still finds pair"); CHECK(h["key"] == "value", "T3.1b: value correct despite missing }"); } { // Unclosed string value — inner while hits EOF, pair still added auto h = parse_headers_json("{\"key\":\"value"); CHECK(h.size() == 1, "T3.2: unclosed string, pair still added (lenient)"); CHECK(h["key"] == "value", "T3.2b: value read until EOF"); } { auto h = parse_headers_json("{\"key\" \"value\"}"); CHECK(h.empty(), "T3.3: missing colon, returns empty (no crash)"); } { auto h = parse_headers_json("not json at all"); CHECK(h.empty(), "T3.4: plain text, returns empty (no crash)"); } { auto h = parse_headers_json("{key:value}"); CHECK(h.empty(), "T3.5: unquoted keys, returns empty (no crash)"); } { auto h = parse_headers_json("{\"key\":value}"); CHECK(h.empty(), "T3.6: unquoted value, returns empty (no crash)"); } { auto h = parse_headers_json("\x00\x01\xFF\xFE"); CHECK(h.empty(), "T3.7: binary garbage, returns empty (no crash)"); } { auto h = parse_headers_json("{\"k\":\"v\",,\"k2\":\"v2\"}"); CHECK(h.size() == 2, "T3.8: double comma, parser skips past it"); } { // nested object as value — flat parser picks inner quoted string "nested" auto h = parse_headers_json("{\"k\":{\"nested\":1}}"); CHECK(h.size() == 1, "T3.9: nested object, flat parser extracts inner string as value"); CHECK(h["k"] == "nested", "T3.9b: value is 'nested' (inner quoted string)"); } // ================================================================ // Test Block 4: parse_headers_json — 超长 header 值 { std::string long_val(5000, 'A'); std::string json = "{\"long\":\"" + long_val + "\"}"; auto h = parse_headers_json(json.c_str()); CHECK(h.size() == 1, "T4.1: 5000-char value, size=1"); CHECK(h["long"] == long_val, "T4.2: full 5000-char value preserved"); } { std::string long_key(1000, 'K'); std::string json = "{\"" + long_key + "\":\"v\"}"; auto h = parse_headers_json(json.c_str()); CHECK(h.size() == 1, "T4.3: 1000-char key, size=1"); CHECK(h[long_key] == "v", "T4.4: long key lookup works"); } { // 10k value — stress test, no crash std::string huge(10000, 'Z'); std::string json = "{\"huge\":\"" + huge + "\"}"; auto h = parse_headers_json(json.c_str()); CHECK(h.size() == 1, "T4.5: 10000-char value parsed"); CHECK(h["huge"].size() == 10000, "T4.6: size preserved"); } { // empty key (two consecutive quotes) auto h = parse_headers_json("{\"\":\"value\"}"); CHECK(h.size() == 1, "T4.7: empty key accepted"); CHECK(h[""] == "value", "T4.8: empty key lookup works"); } // ================================================================ // Test Block 5: SSE 行解析边界 // ================================================================ std::cout << "\n--- Block 5: SSE line splitting boundaries ---\n"; { auto lines = split_sse_lines("data: hello\n"); CHECK(lines.size() == 1, "T5.1: single LF line"); CHECK(lines[0] == "data: hello", "T5.2: LF not included"); } { auto lines = split_sse_lines("data: hello\r\n"); CHECK(lines.size() == 1, "T5.3: single CRLF line"); CHECK(lines[0] == "data: hello", "T5.4: CR stripped"); } { auto lines = split_sse_lines("line1\nline2\nline3\n"); CHECK(lines.size() == 3, "T5.5: three lines with LF"); CHECK(lines[0] == "line1", "T5.6: first line"); CHECK(lines[2] == "line3", "T5.7: third line"); } { auto lines = split_sse_lines(""); CHECK(lines.empty(), "T5.8: empty string, no lines"); } { auto lines = split_sse_lines("\n"); CHECK(lines.size() == 1, "T5.9: single LF produces one empty line"); CHECK(lines[0].empty(), "T5.10: empty line content"); } { auto lines = split_sse_lines("\n\n"); CHECK(lines.size() == 2, "T5.11: two LFs produce two empty lines"); } { auto lines = split_sse_lines("data: [DONE]\n"); CHECK(lines.size() == 1, "T5.12: [DONE] marker parsed as line"); CHECK(lines[0] == "data: [DONE]", "T5.13: [DONE] content preserved"); } { auto lines = split_sse_lines("partial line without newline"); CHECK(lines.size() == 1, "T5.14: no-newline fragment = one line"); CHECK(lines[0] == "partial line without newline", "T5.15: content intact"); } { auto lines = split_sse_lines("line1\r\n\r\nline2\n"); CHECK(lines.size() == 3, "T5.16: CRLF + blank + LF"); CHECK(lines[1].empty(), "T5.17: blank line is empty string"); } { // binary content in line auto lines = split_sse_lines(std::string("data: \x00\x01\x02\n", 10)); CHECK(lines.size() == 1, "T5.18: null bytes in line, no crash"); } { // \r without \n auto lines = split_sse_lines("line\r"); CHECK(lines.size() == 1, "T5.19: trailing CR stripped"); CHECK(lines[0] == "line", "T5.20: content without CR"); } // ================================================================ // Test Block 6: http_post_json — 参数校验 // ================================================================ std::cout << "\n--- Block 6: http_post_json parameter validation ---\n"; { char* resp = nullptr; int code = 0; int ret = http_post_json(nullptr, "443", "/", "{}", "{}", &resp, &code); CHECK(ret == -1, "T6.1: nullptr host returns -1"); CHECK(resp == nullptr, "T6.2: response_body set to nullptr"); CHECK(code == -1, "T6.3: status_code set to -1"); } { char* resp = nullptr; int code = 0; int ret = http_post_json("host", nullptr, "/", "{}", "{}", &resp, &code); CHECK(ret == -1, "T6.4: nullptr port returns -1"); CHECK(code == -1, "T6.5: status_code = -1"); } { char* resp = nullptr; int code = 0; int ret = http_post_json("host", "443", "/", "{}", nullptr, &resp, &code); CHECK(ret == -1, "T6.6: nullptr headers_json allowed (passed to parser)"); // headers_json can be nullptr; parse_headers_json handles it } { char* resp = (char*)0xDEAD; int code = 0; int ret = http_post_json("host", "443", "/", "{}", "{}", nullptr, &code); CHECK(ret == -1, "T6.7: nullptr response_body returns -1"); CHECK(code == -1, "T6.8: status_code = -1"); } { char* resp = nullptr; int ret = http_post_json("host", "443", "/", "{}", "{}", &resp, nullptr); CHECK(ret == -1, "T6.9: nullptr status_code returns -1 (before strdup crash)"); } { // nullptr body (missing body pointer) char* resp = nullptr; int code = 0; int ret = http_post_json("host", "443", "/", nullptr, "{}", &resp, &code); CHECK(ret == -1, "T6.10: nullptr body returns -1"); } // ================================================================ // Test Block 7: http_post_stream — 参数校验 // ================================================================ std::cout << "\n--- Block 7: http_post_stream parameter validation ---\n"; { char* resp = nullptr; int code = 0; int ret = http_post_stream(nullptr, "443", "/", "{}", "{}", nullptr, nullptr, &resp, &code); CHECK(ret == -1, "T7.1: nullptr host (stream) returns -1"); } // ================================================================ // Test Block 8: 异常保护 — catch(...) 不 crash // ================================================================ #if 0 // Block 8 disabled: needs live network + OpenSSL runtime DLL path std::cout << "\n--- Block 8: exception protection (catch...) ---\n"; { // Set up mock host so g_host->strdup() doesn't crash // (do_post_stream 的 done: 标签会调用 g_host->strdup) g_host = &g_mock_host; // Connect to 127.0.0.1:2 — nothing listening, connect() throws // system_error which is caught by catch(const std::exception&) char* resp = nullptr; int code = 0; int ret = http_post_json("127.0.0.1", "2", "/", "{}", "{}", &resp, &code); CHECK(ret == -1, "T8.1: connection refused caught, returns -1"); CHECK(code == -1, "T8.2: status_code = -1 on exception"); CHECK(resp != nullptr, "T8.3: error message populated via mock strdup"); // Reset g_host g_host = nullptr; } #endif // 0 — Block 8 disabled (needs live network) // ================================================================ // Summary // ================================================================ std::cout << "\n"; if (g_failures == 0) { std::cout << "=== All network plugin tests passed ===\n"; } else { std::cerr << "=== " << g_failures << " test(s) FAILED ===\n"; } // _exit() avoids static-destructor ordering issues between // OpenSSL / Boost.ASIO globals when #include'ing plugin source // into a test executable that links openssl::openssl. std::cout.flush(); std::cerr.flush(); _exit(g_failures > 0 ? 1 : 0); }