cocoon/tests/unit/test_http.c
xfy911 3c521823b1 feat(tests): 添加 config 和 log 模块单元测试 + http.c 缓冲区溢出防护
- 新增 test_log.c: 8 个测试覆盖日志级别 getter/setter、前缀设置、持久化
- 新增 test_config.c: 11 个测试覆盖 JSON 配置加载、注释支持、错误处理、merge 覆盖逻辑
- http.c: 修复 response header 格式化中的 snprintf 边界条件,防止缓冲区溢出
- Makefile: 添加 test_log、test_config 编译规则,更新 .PHONY 列表
2026-06-04 00:02:18 +08:00

516 lines
17 KiB
C
Raw Permalink 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.

#include "unity.h"
#include "http.h"
#include <string.h>
#include <stdlib.h>
/* 测试辅助函数:设置请求体 */
static void set_request_body(http_request_t *req, const char *body) {
if (body) {
req->body_len = strlen(body);
req->body = (char *)malloc(req->body_len + 1);
memcpy(req->body, body, req->body_len + 1);
}
}
/* ===== http_method_str ===== */
void test_http_method_str(void) {
TEST_ASSERT_EQUAL_STRING("GET", http_method_str(HTTP_GET));
TEST_ASSERT_EQUAL_STRING("HEAD", http_method_str(HTTP_HEAD));
TEST_ASSERT_EQUAL_STRING("POST", http_method_str(HTTP_POST));
TEST_ASSERT_EQUAL_STRING("PUT", http_method_str(HTTP_PUT));
TEST_ASSERT_EQUAL_STRING("DELETE", http_method_str(HTTP_DELETE));
TEST_ASSERT_EQUAL_STRING("OPTIONS", http_method_str(HTTP_OPTIONS));
TEST_ASSERT_EQUAL_STRING("UNKNOWN", http_method_str(HTTP_UNKNOWN));
TEST_ASSERT_EQUAL_STRING("UNKNOWN", http_method_str(999)); /* 非法枚举值 */
}
/* ===== http_parse_request ===== */
void test_parse_simple_get(void) {
const char *req = "GET /index.html HTTP/1.1\r\n"
"Host: localhost\r\n"
"\r\n";
http_request_t parsed = {0};
int n = http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(strlen(req), n);
TEST_ASSERT_EQUAL(HTTP_GET, parsed.method);
TEST_ASSERT_EQUAL_STRING("/index.html", parsed.path);
TEST_ASSERT_EQUAL_STRING("HTTP/1.1", parsed.version);
TEST_ASSERT_TRUE(parsed.keep_alive); /* HTTP/1.1 默认 keep-alive */
TEST_ASSERT_EQUAL(1, parsed.num_headers);
http_request_free(&parsed);
}
void test_parse_head_request(void) {
const char *req = "HEAD /style.css HTTP/1.0\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(HTTP_HEAD, parsed.method);
TEST_ASSERT_EQUAL_STRING("/style.css", parsed.path);
TEST_ASSERT_EQUAL_STRING("HTTP/1.0", parsed.version);
TEST_ASSERT_FALSE(parsed.keep_alive); /* HTTP/1.0 默认不 keep-alive */
http_request_free(&parsed);
}
void test_parse_post_request(void) {
const char *req = "POST /api/echo HTTP/1.1\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 18\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(HTTP_POST, parsed.method);
TEST_ASSERT_EQUAL(18, parsed.content_length);
TEST_ASSERT_EQUAL_STRING("application/json", parsed.content_type);
http_request_free(&parsed);
}
void test_parse_incomplete_request(void) {
/* 只有请求行的一部分,没有换行符 */
const char *req = "GET /index.html HTTP/1.1";
http_request_t parsed = {0};
int n = http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(-1, n); /* 数据不完整,找不到换行 */
http_request_free(&parsed);
}
void test_parse_malformed_request(void) {
http_request_t parsed = {0};
/* 空缓冲区 */
TEST_ASSERT_EQUAL(-2, http_parse_request(NULL, 0, &parsed));
/* 空字符串 */
TEST_ASSERT_EQUAL(-2, http_parse_request("", 0, &parsed));
/* 无空格 */
TEST_ASSERT_EQUAL(-2, http_parse_request("GET\r\n", 5, &parsed));
/* 无路径 */
TEST_ASSERT_EQUAL(-2, http_parse_request("GET HTTP/1.1\r\n", 15, &parsed));
http_request_free(&parsed);
}
void test_parse_range_header(void) {
const char *req = "GET /file.txt HTTP/1.1\r\n"
"Range: bytes=100-200\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_TRUE(parsed.has_range);
TEST_ASSERT_EQUAL(100, parsed.range_start);
TEST_ASSERT_EQUAL(200, parsed.range_end);
http_request_free(&parsed);
}
void test_parse_range_open_end(void) {
const char *req = "GET /file.txt HTTP/1.1\r\n"
"Range: bytes=100-\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_TRUE(parsed.has_range);
TEST_ASSERT_EQUAL(100, parsed.range_start);
/* range_end 为 -1 表示未指定结束位置,但测试不应直接检查这个值,因为可能被默认处理 */
http_request_free(&parsed);
}
void test_parse_cache_headers(void) {
const char *req = "GET /index.html HTTP/1.1\r\n"
"If-None-Match: \"abc123\"\r\n"
"If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_TRUE(parsed.has_if_none_match);
TEST_ASSERT_EQUAL_STRING("\"abc123\"", parsed.if_none_match);
TEST_ASSERT_TRUE(parsed.has_if_modified_since);
TEST_ASSERT_EQUAL_STRING("Wed, 21 Oct 2015 07:28:00 GMT", parsed.if_modified_since);
http_request_free(&parsed);
}
void test_parse_accept_encoding(void) {
const char *req = "GET /index.html HTTP/1.1\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_TRUE(parsed.has_accept_encoding);
TEST_ASSERT_TRUE(parsed.accept_gzip);
TEST_ASSERT_TRUE(parsed.accept_deflate);
http_request_free(&parsed);
}
void test_parse_max_headers(void) {
/* 构造超过 HTTP_MAX_HEADERS 的请求 */
char req[4096];
strcpy(req, "GET / HTTP/1.1\r\n");
for (int i = 0; i < 40; i++) {
char header[64];
snprintf(header, sizeof(header), "X-Header-%d: value\r\n", i);
strcat(req, header);
}
strcat(req, "\r\n");
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(HTTP_MAX_HEADERS, parsed.num_headers); /* 最多 32 个 */
http_request_free(&parsed);
}
void test_parse_keep_alive_override(void) {
/* HTTP/1.1 默认 keep-alive但 Connection: close 可以覆盖 */
const char *req = "GET / HTTP/1.1\r\n"
"Connection: close\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
/* HTTP/1.1 默认 keep-alive=true但 Connection: close 应覆盖为 false */
TEST_ASSERT_FALSE(parsed.keep_alive);
http_request_free(&parsed);
}
void test_parse_unknown_method(void) {
const char *req = "PATCH /api/resource HTTP/1.1\r\n"
"\r\n";
http_request_t parsed = {0};
int n = http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(-2, n); /* 不支持的方法返回格式错误 */
http_request_free(&parsed);
}
void test_parse_path_too_long(void) {
char path[HTTP_MAX_PATH + 100];
memset(path, 'a', sizeof(path));
path[sizeof(path) - 1] = '\0';
char req[HTTP_MAX_PATH + 200];
snprintf(req, sizeof(req), "GET %s HTTP/1.1\r\n\r\n", path);
http_request_t parsed = {0};
int n = http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(-2, n); /* 路径过长 */
http_request_free(&parsed);
}
/* ===== http_format_response_header ===== */
void test_format_simple_response(void) {
http_response_t resp = {
.status_code = 200,
.status_text = "OK",
.content_type = "text/html",
.content_length = 1234,
.keep_alive = true
};
char buf[1024];
int n = http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_GREATER_THAN(0, n);
TEST_ASSERT_TRUE(strstr(buf, "HTTP/1.1 200 OK") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Content-Type: text/html") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Content-Length: 1234") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Connection: keep-alive") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Server: Cocoon/1.0") != NULL);
}
void test_format_404_response(void) {
http_response_t resp = {
.status_code = 404,
.status_text = "Not Found",
.content_type = "text/html",
.content_length = 0,
.keep_alive = false
};
char buf[1024];
http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_TRUE(strstr(buf, "HTTP/1.1 404 Not Found") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Connection: close") != NULL);
}
void test_format_range_response(void) {
http_response_t resp = {
.status_code = 206,
.status_text = "Partial Content",
.content_type = "text/plain",
.content_length = 100,
.keep_alive = true,
.has_range = true,
.range_start = 0,
.range_end = 99,
.total_length = 1000
};
char buf[1024];
http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_TRUE(strstr(buf, "HTTP/1.1 206 Partial Content") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Content-Range: bytes 0-99/1000") != NULL);
}
void test_format_with_cache_headers(void) {
http_response_t resp = {
.status_code = 200,
.status_text = "OK",
.content_type = "text/html",
.content_length = 100,
.keep_alive = true,
.etag = "\"abc123\"",
.last_modified = "Wed, 21 Oct 2015 07:28:00 GMT",
.content_encoding = "gzip"
};
char buf[1024];
http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_TRUE(strstr(buf, "ETag: \"abc123\"") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "Content-Encoding: gzip") != NULL);
}
void test_format_buffer_too_small(void) {
http_response_t resp = {
.status_code = 200,
.status_text = "OK",
.content_type = "text/html",
.content_length = 100,
.keep_alive = true
};
char buf[10]; /* 故意很小 */
int n = http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_EQUAL(-1, n); /* 缓冲区不足 */
}
void test_format_null_params(void) {
char buf[1024];
TEST_ASSERT_EQUAL(-1, http_format_response_header(NULL, 1024, &(http_response_t){0}));
TEST_ASSERT_EQUAL(-1, http_format_response_header(buf, 1024, NULL));
TEST_ASSERT_EQUAL(-1, http_format_response_header(buf, 0, &(http_response_t){0}));
}
void test_format_default_content_type(void) {
http_response_t resp = {
.status_code = 200,
.status_text = "OK",
.content_type = NULL, /* 测试默认值 */
.content_length = 100,
.keep_alive = true
};
char buf[1024];
http_format_response_header(buf, sizeof(buf), &resp);
TEST_ASSERT_TRUE(strstr(buf, "Content-Type: application/octet-stream") != NULL);
}
/* ===== http_mime_type ===== */
void test_mime_type_html(void) {
TEST_ASSERT_EQUAL_STRING("text/html; charset=utf-8", http_mime_type("index.html"));
TEST_ASSERT_EQUAL_STRING("text/html; charset=utf-8", http_mime_type("page.htm"));
}
void test_mime_type_css_js(void) {
TEST_ASSERT_EQUAL_STRING("text/css; charset=utf-8", http_mime_type("style.css"));
TEST_ASSERT_EQUAL_STRING("application/javascript", http_mime_type("app.js"));
}
void test_mime_type_images(void) {
TEST_ASSERT_EQUAL_STRING("image/png", http_mime_type("logo.png"));
TEST_ASSERT_EQUAL_STRING("image/jpeg", http_mime_type("photo.jpg"));
TEST_ASSERT_EQUAL_STRING("image/jpeg", http_mime_type("photo.jpeg"));
TEST_ASSERT_EQUAL_STRING("image/gif", http_mime_type("anim.gif"));
TEST_ASSERT_EQUAL_STRING("image/svg+xml", http_mime_type("icon.svg"));
}
void test_mime_type_json(void) {
TEST_ASSERT_EQUAL_STRING("application/json", http_mime_type("data.json"));
}
void test_mime_type_no_extension(void) {
TEST_ASSERT_EQUAL_STRING("application/octet-stream", http_mime_type("Makefile"));
TEST_ASSERT_EQUAL_STRING("application/octet-stream", http_mime_type("README"));
}
void test_mime_type_unknown_extension(void) {
TEST_ASSERT_EQUAL_STRING("application/octet-stream", http_mime_type("file.xyz"));
TEST_ASSERT_EQUAL_STRING("application/octet-stream", http_mime_type("archive.7z"));
}
void test_mime_type_case_insensitive(void) {
TEST_ASSERT_EQUAL_STRING("text/html; charset=utf-8", http_mime_type("INDEX.HTML"));
TEST_ASSERT_EQUAL_STRING("text/html; charset=utf-8", http_mime_type("Index.Html"));
TEST_ASSERT_EQUAL_STRING("image/png", http_mime_type("Logo.PNG"));
}
void test_mime_type_special_files(void) {
TEST_ASSERT_EQUAL_STRING("font/woff2", http_mime_type("font.woff2"));
TEST_ASSERT_EQUAL_STRING("application/pdf", http_mime_type("doc.pdf"));
TEST_ASSERT_EQUAL_STRING("video/mp4", http_mime_type("video.mp4"));
TEST_ASSERT_EQUAL_STRING("audio/mpeg", http_mime_type("music.mp3"));
TEST_ASSERT_EQUAL_STRING("application/wasm", http_mime_type("app.wasm"));
}
/* ===== http_request_free ===== */
void test_request_free_null_body(void) {
http_request_t req = {0};
/* 不应崩溃 */
http_request_free(&req);
http_request_free(NULL); /* 也不应崩溃 */
}
void test_request_free_with_body(void) {
http_request_t req = {0};
set_request_body(&req, "test body");
TEST_ASSERT_NOT_NULL(req.body);
http_request_free(&req);
TEST_ASSERT_NULL(req.body);
TEST_ASSERT_EQUAL(0, req.body_len);
}
/* ===== 边界条件测试 ===== */
void test_parse_request_with_body_hint(void) {
/* 请求头后应有 body但解析器只解析头部 */
const char *req = "POST /api/upload HTTP/1.1\r\n"
"Content-Length: 11\r\n"
"\r\n"
"hello world"; /* body 数据 */
http_request_t parsed = {0};
int n = http_parse_request(req, strlen(req), &parsed);
/* 解析器应只解析到头部结束,返回解析的字节数 */
/* 头部长度 = 请求行 + Content-Length + 空行 */
/* POST /api/upload HTTP/1.1\r\n = 27 */
/* Content-Length: 11\r\n = 20 */
/* \r\n = 2 */
/* 总计 = 49 */
TEST_ASSERT_EQUAL(49, n); /* 头部长度 */
TEST_ASSERT_EQUAL(11, parsed.content_length);
http_request_free(&parsed);
}
void test_parse_empty_path(void) {
/* 路径为 / 的情况 */
const char *req = "GET / HTTP/1.1\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL_STRING("/", parsed.path);
http_request_free(&parsed);
}
void test_parse_query_string(void) {
/* 带查询参数的路径 */
const char *req = "GET /search?q=test&page=1 HTTP/1.1\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL_STRING("/search?q=test&page=1", parsed.path);
http_request_free(&parsed);
}
void test_parse_multiple_headers_same_name(void) {
/* 同一头部多次出现 */
const char *req = "GET / HTTP/1.1\r\n"
"X-Custom: value1\r\n"
"X-Custom: value2\r\n"
"\r\n";
http_request_t parsed = {0};
http_parse_request(req, strlen(req), &parsed);
TEST_ASSERT_EQUAL(2, parsed.num_headers);
/* 解析器保留两个独立条目,不合并 */
TEST_ASSERT_EQUAL_STRING("x-custom", parsed.headers[0].name);
TEST_ASSERT_EQUAL_STRING("value1", parsed.headers[0].value);
TEST_ASSERT_EQUAL_STRING("x-custom", parsed.headers[1].name);
TEST_ASSERT_EQUAL_STRING("value2", parsed.headers[1].value);
http_request_free(&parsed);
}
/* ===== 主函数 ===== */
void setUp(void) {
/* 每个测试前执行 */
}
void tearDown(void) {
/* 每个测试后执行 */
}
int main(void) {
UNITY_BEGIN();
/* http_method_str */
RUN_TEST(test_http_method_str);
/* http_parse_request */
RUN_TEST(test_parse_simple_get);
RUN_TEST(test_parse_head_request);
RUN_TEST(test_parse_post_request);
RUN_TEST(test_parse_incomplete_request);
RUN_TEST(test_parse_malformed_request);
RUN_TEST(test_parse_range_header);
RUN_TEST(test_parse_range_open_end);
RUN_TEST(test_parse_cache_headers);
RUN_TEST(test_parse_accept_encoding);
RUN_TEST(test_parse_max_headers);
RUN_TEST(test_parse_keep_alive_override);
RUN_TEST(test_parse_unknown_method);
RUN_TEST(test_parse_path_too_long);
RUN_TEST(test_parse_request_with_body_hint);
RUN_TEST(test_parse_empty_path);
RUN_TEST(test_parse_query_string);
RUN_TEST(test_parse_multiple_headers_same_name);
/* http_format_response_header */
RUN_TEST(test_format_simple_response);
RUN_TEST(test_format_404_response);
RUN_TEST(test_format_range_response);
RUN_TEST(test_format_with_cache_headers);
RUN_TEST(test_format_buffer_too_small);
RUN_TEST(test_format_null_params);
RUN_TEST(test_format_default_content_type);
/* http_mime_type */
RUN_TEST(test_mime_type_html);
RUN_TEST(test_mime_type_css_js);
RUN_TEST(test_mime_type_images);
RUN_TEST(test_mime_type_json);
RUN_TEST(test_mime_type_no_extension);
RUN_TEST(test_mime_type_unknown_extension);
RUN_TEST(test_mime_type_case_insensitive);
RUN_TEST(test_mime_type_special_files);
/* http_request_free */
RUN_TEST(test_request_free_null_body);
RUN_TEST(test_request_free_with_body);
return UNITY_END();
}