Files
dstalk/tests/network_plugin_test.cpp
XiuChengWu f2da0f2ed4 Add metadata validation script and module documentation
- Introduced a new Python script `check_agents_metadata.py` for validating agent metadata, including YAML parsing, rating ranges, and cross-references.
- Added usage instructions and exit codes for the script.
- Created a new markdown file `模块目录和功能说明.md` to outline the directory structure and functionality of the modules.
- Added a text file `说明此文件不可AI修改.txt` to specify that certain files should not be modified by AI, including important information about the `dstalk` framework and its modules.
2026-05-31 00:00:58 +08:00

370 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* @file network_plugin_test.cpp
* @brief Network plugin unit tests (W22.2): parse_headers_json (normal, empty,
* malformed, long values), SSE line splitting boundaries, and
* http_post_json/http_post_stream parameter validation.
* Network 插件单元测试 (W22.2)parse_headers_json正常、空、畸形、长值、SSE 行解析边界、
* http_post_json/http_post_stream 参数校验。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#define _CRT_SECURE_NO_WARNINGS
#define BOOST_ASIO_DISABLE_STD_TO_ADDRESS
#include "../plugins/network/src/network_plugin.cpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
static int g_failures = 0;
// Lightweight assertion macro: increments g_failures counter on failure
#define CHECK(cond, msg) do { \
if (cond) { \
std::cout << "[OK] " << (msg) << "\n"; \
} else { \
std::cerr << "[FAIL] " << (msg) << "\n"; \
g_failures++; \
} \
} while (0)
// SSE line-split helper: mirrors do_post_stream's emit_lines logic for unit-testing
// SSE chunk parsing without a live network connection.
// SSE 行分割辅助函数:镜像 do_post_stream 的 emit_lines 逻辑,无需实时网络连接即可单元测试 SSE 数据块解析。
static std::vector<std::string> split_sse_lines(std::string fragment) {
std::vector<std::string> 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;
}
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;
}
// Network 插件测试 (W22.2)parse_headers_json 正常/空/畸形/长值、
// SSE 行分割LF、CRLF、空、null 字节、尾部 CR
// http_post_json/http_post_stream 参数校验(空指针)。
// Network plugin tests (W22.2): parse_headers_json normal/empty/malformed/long,
// SSE line splitting (LF, CRLF, empty, null-bytes, trailing CR),
// and http_post_json/http_post_stream parameter validation (null pointers).
int main()
{
// ================================================================
// Test Block 1: parse_headers_json — 正常 JSON
// Test Block 1: parse_headers_json — normal 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");
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");
}
{
auto h = parse_headers_json("{\"key\":\"val\\\"ue\"}");
CHECK(h.size() == 1, "T1.10: escaped quote in value");
CHECK(h["key"] == "val\"ue", "T1.11: literal quote preserved");
}
{
auto h = parse_headers_json("{\"path\":\"C:\\\\tmp\"}");
CHECK(h.size() == 1, "T1.12: escaped backslash");
CHECK(h["path"] == "C:\\tmp", "T1.13: single backslash in result");
}
// ================================================================
// Test Block 2: parse_headers_json — 空 / null 输入
// Test Block 2: parse_headers_json — empty/null input
// ================================================================
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 (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 returns empty");
}
// ================================================================
// Test Block 3: parse_headers_json — 畸形 JSON
// Test Block 3: parse_headers_json — malformed JSON
// ================================================================
std::cout << "\n--- Block 3: parse_headers_json malformed JSON ---\n";
{
auto h = parse_headers_json("{\"key\":\"value\"");
CHECK(h.size() == 1, "T3.1: unclosed brace, parser finds pair");
CHECK(h["key"] == "value", "T3.1b: value correct despite missing }");
}
{
auto h = parse_headers_json("{\"key\":\"value");
CHECK(h.size() == 1, "T3.2: unclosed string, pair still added");
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");
}
{
auto h = parse_headers_json("{\"k\":{\"nested\":1}}");
CHECK(h.size() == 1, "T3.9: nested object, extracts inner string");
CHECK(h["k"] == "nested", "T3.9b: value is 'nested'");
}
{
auto h = parse_headers_json("{\"k\":\"v\"");
CHECK(h.size() == 1, "T3.10: missing closing brace, pair found");
}
{
auto h = parse_headers_json("{");
CHECK(h.empty(), "T3.11: single brace, returns empty (no crash)");
}
// ================================================================
// Test Block 4: parse_headers_json — 超长 header 值
// Test Block 4: parse_headers_json — long values
// ================================================================
std::cout << "\n--- Block 4: parse_headers_json long values ---\n";
{
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: 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");
}
{
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");
}
{
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 行解析边界
// Test Block 5: SSE line splitting boundaries
// ================================================================
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 LF lines");
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 = 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 = two empty lines");
}
{
auto lines = split_sse_lines("data: [DONE]\n");
CHECK(lines.size() == 1, "T5.12: [DONE] marker parsed");
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 = 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");
}
{
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");
}
{
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");
}
{
auto lines = split_sse_lines("data: {\"type\":\"delta\"}\n\n");
CHECK(lines.size() == 2, "T5.21: SSE data + blank line");
CHECK(lines[0] == "data: {\"type\":\"delta\"}", "T5.22: JSON data line");
CHECK(lines[1].empty(), "T5.23: trailing blank line");
}
// ================================================================
// Test Block 6: http_post_json — 参数校验 (null ptr, early return)
// Test Block 6: http_post_json — parameter validation (null ptr, early return)
// ================================================================
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 = nullptr");
CHECK(code == -1, "T6.3: status_code = -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 target returns -1");
}
{
char* resp = nullptr;
int code = 0;
int ret = http_post_json("host", "443", "/", nullptr, "{}", &resp, &code);
CHECK(ret == -1, "T6.7: nullptr body returns -1");
}
{
char* resp = (char*)0xDEAD;
int code = 0;
int ret = http_post_json("host", "443", "/", "{}", "{}", nullptr, &code);
CHECK(ret == -1, "T6.8: nullptr response_body returns -1");
CHECK(code == -1, "T6.9: status_code = -1");
}
{
char* resp = nullptr;
int ret = http_post_json("host", "443", "/", "{}", "{}", &resp, nullptr);
CHECK(ret == -1, "T6.10: nullptr status_code returns -1");
}
// ================================================================
// Test Block 7: http_post_stream — 参数校验
// Test Block 7: http_post_stream — parameter validation
// ================================================================
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");
}
{
char* resp = nullptr;
int code = 0;
int ret = http_post_stream("host", "443", "/", "{}", "{}",
nullptr, nullptr, nullptr, &code);
CHECK(ret == -1, "T7.2: nullptr response_body (stream) returns -1");
}
// ================================================================
// 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";
}
std::cout.flush();
std::cerr.flush();
_exit(g_failures > 0 ? 1 : 0);
}