- Implemented the OpenAI-compatible AI provider plugin, including configuration, chat, and chat_stream functionalities. - Added support for SSE streaming and tool calls. - Integrated Boost.JSON for JSON handling. - Created CMake configuration for the plugin. - Added error handling and logging throughout the plugin.
370 lines
15 KiB
C++
370 lines
15 KiB
C++
/*
|
||
* @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.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);
|
||
}
|