cocoon/tests/unit/test_config.c
xfy911 dd41464b9c feat(middleware): 实现中间件框架(CORS / Basic Auth / Rate Limit)
- 新增 middleware.c/middleware.h:注册表 + 链式执行机制
  - 内置 CORS 中间件:支持预检请求、跨域头
  - 内置 Basic Auth 中间件:HTTP 基础认证
  - 内置 Rate Limit 中间件:基于 IP 的秒级限流
- 修复 config_merge 参数不匹配(新增 has_cors_enabled/has_auth_user/has_auth_pass/has_rate_limit)
- 修复 server_destroy 与 main() 双重释放字符串指针
- 修复 main.c 启动段错误(access_log_path 未初始化)
- 修复 rate_limit 哈希函数包含端口导致同一 IP 不同 bucket 的问题
- 修复 rate_limit 中间件 user_data 悬空指针(改为 ctx->mw_config)
- 修复 401/429 响应 Content-Length 与 body 长度不匹配导致 curl 挂起
- 修复集成测试服务器启动循环:使用 nc 代替 curl,避免触发限流计数
- 修复集成测试 kill_server:使用 kill -9 确保进程退出,并检查端口释放
- 单元测试 18/18 通过,集成测试 66/66 通过
2026-06-06 00:32:22 +08:00

343 lines
11 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.

#include "unity.h"
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
/* 辅助:写临时配置文件,返回路径 */
static const char *write_temp_config(const char *content) {
static char path[256];
static int counter = 0;
snprintf(path, sizeof(path), "/tmp/cocoon_test_config_%d_%d.json",
(int)getpid(), counter++);
FILE *fp = fopen(path, "w");
if (fp) {
fwrite(content, 1, strlen(content), fp);
fclose(fp);
}
return path;
}
static void cleanup(const char *path) {
remove(path);
}
/* ===== config_load_from_file ===== */
void test_load_valid_config(void) {
const char *p = write_temp_config(
"{\n"
" \"root_dir\": \"/var/www\",\n"
" \"port\": 8080,\n"
" \"threaded\": true,\n"
" \"num_workers\": 8,\n"
" \"max_connections\": 5000,\n"
" \"timeout_ms\": 60000,\n"
" \"log_level\": \"debug\",\n"
" \"gzip_enabled\": false\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL_STRING("/var/www", cfg.root_dir);
TEST_ASSERT_EQUAL(8080, cfg.port);
TEST_ASSERT_TRUE(cfg.threaded);
TEST_ASSERT_EQUAL(8, cfg.num_workers);
TEST_ASSERT_EQUAL(5000, cfg.max_connections);
TEST_ASSERT_EQUAL(60000, cfg.timeout_ms);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, cfg.log_level);
TEST_ASSERT_FALSE(cfg.gzip_enabled);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_valid_minimal(void) {
const char *p = write_temp_config("{\"port\": 3000}");
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(3000, cfg.port);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_valid_with_comments(void) {
const char *p = write_temp_config(
"{\n"
" // this is a comment\n"
" \"port\": 9090\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(9090, cfg.port);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_invalid_json(void) {
const char *p = write_temp_config("not json");
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(p, &cfg));
cleanup(p);
}
void test_load_missing_file(void) {
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(
"/tmp/cocoon_nonexist_12345.json", &cfg));
}
void test_load_empty_file(void) {
const char *p = write_temp_config("");
cocoon_config_t cfg = {0};
TEST_ASSERT_FALSE(config_load_from_file(p, &cfg));
cleanup(p);
}
void test_load_null_args(void) {
TEST_ASSERT_FALSE(config_load_from_file(NULL, NULL));
TEST_ASSERT_FALSE(config_load_from_file("/tmp/x", NULL));
}
/* ===== config_merge ===== */
void test_merge_override_all(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.threaded = false,
.num_workers = 2,
.max_connections = 100,
.timeout_ms = 30000,
.log_level = LOG_LEVEL_INFO,
.gzip_enabled = true,
.brotli_enabled = true
};
cocoon_config_t cmdline = {
.root_dir = strdup("/new"),
.port = 9090,
.threaded = true,
.num_workers = 4,
.max_connections = 200,
.timeout_ms = 60000,
.log_level = LOG_LEVEL_DEBUG,
.gzip_enabled = false,
.brotli_enabled = false
};
config_merge(&base, &cmdline,
true, true, true, true, true, true, true, true, true, true, true, true,
false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir);
TEST_ASSERT_EQUAL(9090, base.port);
TEST_ASSERT_TRUE(base.threaded);
TEST_ASSERT_EQUAL(4, base.num_workers);
TEST_ASSERT_EQUAL(200, base.max_connections);
TEST_ASSERT_EQUAL(60000, base.timeout_ms);
TEST_ASSERT_EQUAL(LOG_LEVEL_DEBUG, base.log_level);
TEST_ASSERT_FALSE(base.gzip_enabled);
TEST_ASSERT_FALSE(base.brotli_enabled);
free((void *)base.root_dir);
free((void *)cmdline.root_dir);
}
void test_merge_no_override(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.threaded = false,
.num_workers = 2,
.log_level = LOG_LEVEL_INFO
};
cocoon_config_t cmdline = {
.root_dir = "/new",
.port = 9090,
.num_workers = 4,
.log_level = LOG_LEVEL_DEBUG
};
config_merge(&base, &cmdline,
false, false, false, false, false, false, false, false, false, false, false, false,
false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir);
TEST_ASSERT_EQUAL(8080, base.port);
TEST_ASSERT_FALSE(base.threaded);
TEST_ASSERT_EQUAL(2, base.num_workers);
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, base.log_level);
free((void *)base.root_dir);
}
void test_merge_partial_override(void) {
cocoon_config_t base = {
.root_dir = strdup("/old"),
.port = 8080,
.num_workers = 2,
.log_level = LOG_LEVEL_INFO
};
cocoon_config_t cmdline = {
.root_dir = "/new",
.port = 9090,
.num_workers = 4,
.log_level = LOG_LEVEL_DEBUG
};
config_merge(&base, &cmdline,
true, false, false, true, false, false, false, false, false, false, false, false,
false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/new", base.root_dir); /* overridden */
TEST_ASSERT_EQUAL(8080, base.port); /* not overridden */
TEST_ASSERT_EQUAL(2, base.num_workers); /* not overridden */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, base.log_level); /* not overridden */
free((void *)base.root_dir);
}
void test_load_invalid_field_type(void) {
/* port 为字符串而非数字,应解析失败或忽略 */
const char *p = write_temp_config("{\"port\": \"not_a_number\"}");
cocoon_config_t cfg = {0};
/* 极简解析器可能忽略非法类型,不 crash 即可 */
config_load_from_file(p, &cfg);
/* port 保持默认值 0 */
TEST_ASSERT_EQUAL(0, cfg.port);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_log_level_invalid(void) {
const char *p = write_temp_config("{\"log_level\": \"verbose\"}");
cocoon_config_t cfg = {0};
config_load_from_file(p, &cfg);
/* 非法 log_level 保持默认值 INFO */
TEST_ASSERT_EQUAL(LOG_LEVEL_INFO, cfg.log_level);
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_nested_ignored(void) {
/* 极简解析器遇到嵌套对象会失败,但至少不 crash */
const char *p = write_temp_config(
"{\n"
" \"port\": 7777,\n"
" \"nested\": {\"a\": 1}\n"
"}\n"
);
cocoon_config_t cfg = {0};
/* 当前极简解析器不支持嵌套对象,返回 false但不应 segfault */
config_load_from_file(p, &cfg);
TEST_ASSERT_TRUE(1); /* 走到这里说明没 crash */
free((void *)cfg.root_dir);
cleanup(p);
}
void test_load_array_ignored(void) {
/* 极简解析器遇到数组会失败,但至少不 crash */
const char *p = write_temp_config(
"{\n"
" \"port\": 7777,\n"
" \"arr\": [1, 2, 3]\n"
"}\n"
);
cocoon_config_t cfg = {0};
config_load_from_file(p, &cfg);
TEST_ASSERT_TRUE(1); /* 走到这里说明没 crash */
free((void *)cfg.root_dir);
cleanup(p);
}
void test_merge_cmdline_null_root_dir(void) {
/* cmdline root_dir 为 NULL不应覆盖 base */
cocoon_config_t base = {.root_dir = strdup("/old"), .port = 8080};
cocoon_config_t cmdline = {.root_dir = NULL, .port = 9090};
config_merge(&base, &cmdline, true, true, false, false, false, false, false, false, false, false, false, false,
false, false, false, false);
TEST_ASSERT_EQUAL_STRING("/old", base.root_dir); /* NULL 不覆盖 */
TEST_ASSERT_EQUAL(9090, base.port); /* port 覆盖 */
free((void *)base.root_dir);
}
void test_merge_null_safety(void) {
/* 不应 crash */
config_merge(NULL, NULL, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true);
TEST_ASSERT_TRUE(1);
}
void test_load_gzip_enabled(void) {
/* gzip_enabled: true */
const char *p1 = write_temp_config("{\"gzip_enabled\": true}");
cocoon_config_t cfg1 = {0};
TEST_ASSERT_TRUE(config_load_from_file(p1, &cfg1));
TEST_ASSERT_TRUE(cfg1.gzip_enabled);
free((void *)cfg1.root_dir);
cleanup(p1);
/* gzip_enabled: false */
const char *p2 = write_temp_config("{\"gzip_enabled\": false}");
cocoon_config_t cfg2 = {0};
TEST_ASSERT_TRUE(config_load_from_file(p2, &cfg2));
TEST_ASSERT_FALSE(cfg2.gzip_enabled);
free((void *)cfg2.root_dir);
cleanup(p2);
/* 默认值应为 true在 main 中设置,但这里 0 初始化后为 false */
const char *p3 = write_temp_config("{\"port\": 3000}");
cocoon_config_t cfg3 = {.gzip_enabled = true};
TEST_ASSERT_TRUE(config_load_from_file(p3, &cfg3));
TEST_ASSERT_TRUE(cfg3.gzip_enabled); /* 未指定时保持原值 */
free((void *)cfg3.root_dir);
cleanup(p3);
}
void test_load_brotli_enabled(void) {
/* brotli_enabled: true */
const char *p1 = write_temp_config("{\"brotli_enabled\": true}");
cocoon_config_t cfg1 = {0};
TEST_ASSERT_TRUE(config_load_from_file(p1, &cfg1));
TEST_ASSERT_TRUE(cfg1.brotli_enabled);
free((void *)cfg1.root_dir);
cleanup(p1);
/* brotli_enabled: false */
const char *p2 = write_temp_config("{\"brotli_enabled\": false}");
cocoon_config_t cfg2 = {0};
TEST_ASSERT_TRUE(config_load_from_file(p2, &cfg2));
TEST_ASSERT_FALSE(cfg2.brotli_enabled);
free((void *)cfg2.root_dir);
cleanup(p2);
/* 默认值应为 true */
const char *p3 = write_temp_config("{\"port\": 3000}");
cocoon_config_t cfg3 = {.brotli_enabled = true};
TEST_ASSERT_TRUE(config_load_from_file(p3, &cfg3));
TEST_ASSERT_TRUE(cfg3.brotli_enabled);
free((void *)cfg3.root_dir);
cleanup(p3);
}
void setUp(void) {}
void tearDown(void) {}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_load_valid_config);
RUN_TEST(test_load_valid_minimal);
RUN_TEST(test_load_valid_with_comments);
RUN_TEST(test_load_invalid_json);
RUN_TEST(test_load_missing_file);
RUN_TEST(test_load_empty_file);
RUN_TEST(test_load_null_args);
RUN_TEST(test_load_invalid_field_type);
RUN_TEST(test_load_log_level_invalid);
RUN_TEST(test_load_nested_ignored);
RUN_TEST(test_load_array_ignored);
RUN_TEST(test_merge_override_all);
RUN_TEST(test_merge_no_override);
RUN_TEST(test_merge_partial_override);
RUN_TEST(test_merge_null_safety);
RUN_TEST(test_merge_cmdline_null_root_dir);
RUN_TEST(test_load_gzip_enabled);
RUN_TEST(test_load_brotli_enabled);
return UNITY_END();
}