Files
dstalk/tests/network_plugin_test.cpp
XiuChengWu 28ae90a6cc
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
CI / Sanitizer (ASan+UBSan) / ubuntu-24.04 (push) Has been cancelled
CI / Coverage (gcovr) / ubuntu-24.04 (push) Has been cancelled
W23: close mailroom metadata and network validation tests
- Refresh agents STATUS to W22.6 and exclude mailroom from metadata scans
- Add mailroom dispatch checklist and defensive rules
- Register F-23.D-1 and tag network input validation defense-in-depth
- Update network plugin tests for header length limits
- Fix LSP test metadata and remove orphan anthropic_internal.hpp

Verification:
- cmake --build build --config Release: 0 error, 0 warning
- ctest --test-dir build --output-on-failure: 10/10 passed
- ctest --test-dir build -R dstalk_smoke_test --output-on-failure: passed
- python scripts/check_agents_metadata.py --strict: passed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:56:45 +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_middle/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.empty(), "T4.3: 1000-char key rejected by MAX_HEADER_KEY_LENGTH");
CHECK(h.find(long_key) == h.end(), "T4.4: overlong key not inserted");
}
{
std::string huge(10000, 'Z');
std::string json = "{\"huge\":\"" + huge + "\"}";
auto h = parse_headers_json(json.c_str());
CHECK(h.size() == 1, "T4.5: overlong value key parsed");
CHECK(h["huge"].size() == 8192, "T4.6: value truncated at MAX_HEADER_VALUE_LENGTH");
}
{
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);
}