From 5d4c78c8cb4a3504329f92b1c503cd0b3be968b0 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Tue, 9 Jun 2026 21:10:49 +0800 Subject: [PATCH] =?UTF-8?q?test(proxy):=20=E6=96=B0=E5=A2=9E=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86=E6=A8=A1=E5=9D=97=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=8831=E9=A1=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖: - proxy_init: 配置初始化清零 - proxy_add_rule: 添加规则、追加后端、参数校验(空值/空字符串) - proxy_match: 精确匹配、前缀匹配、多规则、NULL参数 - select_backend_sww: 平滑加权轮询(单后端、等权重、不等权重3:1、跳过不健康、全不健康) - select_backend_sww_all: fallback包含不健康后端 - parse_backend_url: http/https/无scheme/无端口/带路径 - proxy_build_forwarded_path: 无目标路径/有目标路径/空剩余路径 - proxy_update_health: 成功阈值恢复、失败阈值标记、计数器重置 - proxy_pool_init: 默认大小、超上限裁剪、零值回退 编译零警告,190个单元测试 + 106项集成测试全部通过。 --- .gitignore | 2 + Makefile | 5 +- tests/unit/test_proxy.c | 422 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_proxy.c diff --git a/.gitignore b/.gitignore index b5fdcc2..897f686 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ tests/unit/test_multipart tests/unit/test_static tests/unit/test_server tests/unit/test_websocket +tests/unit/test_healthcheck +tests/unit/test_proxy tests/fixtures/uploads/ plugins/*.so www/uploads/ diff --git a/Makefile b/Makefile index a3841b3..26d0c84 100644 --- a/Makefile +++ b/Makefile @@ -115,9 +115,8 @@ $(UNIT_TEST_DIR)/test_healthcheck: $(UNIT_TEST_DIR)/test_healthcheck.c proxy.c p $(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm -# 编译规则 -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ +$(UNIT_TEST_DIR)/test_proxy: $(UNIT_TEST_DIR)/test_proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_proxy.c proxy_tls.c http.c log.c platform.c $(UNITY_SRC) $(LDFLAGS) # 安装 install: $(TARGET) diff --git a/tests/unit/test_proxy.c b/tests/unit/test_proxy.c new file mode 100644 index 0000000..0a0d378 --- /dev/null +++ b/tests/unit/test_proxy.c @@ -0,0 +1,422 @@ +#include "unity.h" +#include "proxy.h" +#include +#include +#include + +/* 白盒测试:包含 proxy.c 以访问静态函数 */ +#define UNIT_TEST +#include "proxy.c" +#undef UNIT_TEST + +/* 绕过 gethostbyname 相关函数,避免测试中进行真实 DNS 查询 */ +/* proxy_connect_backend 不会被调用,所以无需 mock */ + +void setUp(void) { } +void tearDown(void) { } + +/* ===== proxy_init ===== */ + +void test_init_zeros(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + TEST_ASSERT_EQUAL(0, cfg.count); + for (size_t i = 0; i < sizeof(cfg.rules) / sizeof(cfg.rules[0]); i++) { + TEST_ASSERT_EQUAL(0, cfg.rules[i].backend_count); + } +} + +/* ===== proxy_add_rule ===== */ + +void test_add_rule_basic(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + + bool ok = proxy_add_rule(&cfg, "/api/", "http://localhost:3000", 4, 1, NULL); + TEST_ASSERT_TRUE(ok); + TEST_ASSERT_EQUAL(1, cfg.count); + TEST_ASSERT_EQUAL_STRING("/api/", cfg.rules[0].path_prefix); + TEST_ASSERT_EQUAL(1, cfg.rules[0].backend_count); + TEST_ASSERT_EQUAL_STRING("localhost", cfg.rules[0].backends[0].target_host); + TEST_ASSERT_EQUAL(3000, cfg.rules[0].backends[0].target_port); + TEST_ASSERT_FALSE(cfg.rules[0].backends[0].target_https); +} + +void test_add_rule_append_backend(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + + proxy_add_rule(&cfg, "/api/", "http://localhost:3000", 4, 1, NULL); + bool ok = proxy_add_rule(&cfg, "/api/", "http://localhost:3001", 4, 2, NULL); + TEST_ASSERT_TRUE(ok); + TEST_ASSERT_EQUAL(1, cfg.count); /* 仍是1条规则 */ + TEST_ASSERT_EQUAL(2, cfg.rules[0].backend_count); /* 2个后端 */ + TEST_ASSERT_EQUAL(3000, cfg.rules[0].backends[0].target_port); + TEST_ASSERT_EQUAL(3001, cfg.rules[0].backends[1].target_port); + TEST_ASSERT_EQUAL(2, cfg.rules[0].backends[1].weight); +} + +void test_add_rule_null_prefix(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + TEST_ASSERT_FALSE(proxy_add_rule(&cfg, NULL, "http://localhost:3000", 4, 1, NULL)); + TEST_ASSERT_FALSE(proxy_add_rule(&cfg, "", "http://localhost:3000", 4, 1, NULL)); +} + +void test_add_rule_null_target(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + TEST_ASSERT_FALSE(proxy_add_rule(&cfg, "/api/", NULL, 4, 1, NULL)); + TEST_ASSERT_FALSE(proxy_add_rule(&cfg, "/api/", "", 4, 1, NULL)); +} + +/* ===== proxy_match ===== */ + +void test_match_exact(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + proxy_add_rule(&cfg, "/api/", "http://localhost:3000", 4, 1, NULL); + + cocoon_proxy_rule_t *rule = proxy_match(&cfg, "/api/users"); + TEST_ASSERT_NOT_NULL(rule); + TEST_ASSERT_EQUAL_STRING("/api/", rule->path_prefix); +} + +void test_match_no_match(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + proxy_add_rule(&cfg, "/api/", "http://localhost:3000", 4, 1, NULL); + + TEST_ASSERT_NULL(proxy_match(&cfg, "/static/file.css")); + TEST_ASSERT_NULL(proxy_match(&cfg, "/")); +} + +void test_match_multiple_rules(void) { + cocoon_proxy_config_t cfg; + proxy_init(&cfg); + proxy_add_rule(&cfg, "/api/", "http://localhost:3000", 4, 1, NULL); + proxy_add_rule(&cfg, "/static/", "http://localhost:4000", 4, 1, NULL); + + TEST_ASSERT_EQUAL_STRING("/api/", proxy_match(&cfg, "/api/v1")->path_prefix); + TEST_ASSERT_EQUAL_STRING("/static/", proxy_match(&cfg, "/static/img.jpg")->path_prefix); +} + +void test_match_null(void) { + TEST_ASSERT_NULL(proxy_match(NULL, "/api/")); + TEST_ASSERT_NULL(proxy_match(NULL, NULL)); +} + +/* ===== select_backend_sww ===== */ + +void test_sww_single(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 1; + rule.backends[0].healthy = true; + rule.backends[0].weight = 1; + rule.backends[0].current_weight = 0; + + cocoon_proxy_backend_t *b = select_backend_sww(&rule); + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_EQUAL(1, b->weight); + /* 1 + 1 - 1 = 1 */ + TEST_ASSERT_EQUAL(0, b->current_weight); +} + +void test_sww_equal_weights(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 2; + rule.backends[0].healthy = true; rule.backends[0].weight = 1; rule.backends[0].current_weight = 0; + rule.backends[1].healthy = true; rule.backends[1].weight = 1; rule.backends[1].current_weight = 0; + + /* 第1轮:cw=[1,1],选第1个(或第2个,取决于实现),但 Nginx 算法是选最大 */ + /* current_weight += weight → [1,1], total=2, 选第一个(1), 然后 -=2 → [-1,1] */ + cocoon_proxy_backend_t *b1 = select_backend_sww(&rule); + TEST_ASSERT_NOT_NULL(b1); + + /* 第2轮:cw=[0,2], total=2, 选 backends[1] (2>0), 然后 -=2 → [0,0] */ + cocoon_proxy_backend_t *b2 = select_backend_sww(&rule); + TEST_ASSERT_NOT_NULL(b2); + + /* 两轮应该分别选到不同后端(等权重) */ + TEST_ASSERT_NOT_EQUAL(b1, b2); + + /* 第3轮:cw=[1,1], 回到和第1轮相同状态 */ + cocoon_proxy_backend_t *b3 = select_backend_sww(&rule); + TEST_ASSERT_EQUAL_PTR(b1, b3); +} + +void test_sww_unequal_weights(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 2; + rule.backends[0].healthy = true; rule.backends[0].weight = 3; rule.backends[0].current_weight = 0; + rule.backends[1].healthy = true; rule.backends[1].weight = 1; rule.backends[1].current_weight = 0; + + /* 记录 10 次选择,验证 3:1 分布 */ + int count[2] = {0, 0}; + for (int i = 0; i < 8; i++) { /* 8次 = 2个周期(total=4) */ + cocoon_proxy_backend_t *b = select_backend_sww(&rule); + TEST_ASSERT_NOT_NULL(b); + if (b == &rule.backends[0]) count[0]++; + else count[1]++; + } + /* 3:1 权重,8次选择应该 backends[0] 出现 6 次,backends[1] 出现 2 次 */ + TEST_ASSERT_EQUAL(6, count[0]); + TEST_ASSERT_EQUAL(2, count[1]); +} + +void test_sww_skips_unhealthy(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 2; + rule.backends[0].healthy = false; rule.backends[0].weight = 1; rule.backends[0].current_weight = 0; + rule.backends[1].healthy = true; rule.backends[1].weight = 1; rule.backends[1].current_weight = 0; + + cocoon_proxy_backend_t *b = select_backend_sww(&rule); + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_EQUAL_PTR(&rule.backends[1], b); +} + +void test_sww_all_unhealthy(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 2; + rule.backends[0].healthy = false; rule.backends[0].weight = 1; + rule.backends[1].healthy = false; rule.backends[1].weight = 1; + + TEST_ASSERT_NULL(select_backend_sww(&rule)); +} + +/* ===== select_backend_sww_all ===== */ + +void test_sww_all_includes_unhealthy(void) { + cocoon_proxy_rule_t rule = {0}; + rule.backend_count = 2; + rule.backends[0].healthy = false; rule.backends[0].weight = 1; rule.backends[0].current_weight = 0; + rule.backends[1].healthy = false; rule.backends[1].weight = 1; rule.backends[1].current_weight = 0; + + /* sww_all 应该包含不健康后端 */ + cocoon_proxy_backend_t *b = select_backend_sww_all(&rule); + TEST_ASSERT_NOT_NULL(b); +} + +/* ===== parse_backend_url ===== */ + +void test_parse_url_http(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("http://localhost:3000/api", &backend, 4); + TEST_ASSERT_FALSE(backend.target_https); + TEST_ASSERT_EQUAL(3000, backend.target_port); + TEST_ASSERT_EQUAL_STRING("localhost", backend.target_host); + TEST_ASSERT_EQUAL_STRING("/api", backend.target_path); +} + +void test_parse_url_https(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("https://example.com:443/", &backend, 4); + TEST_ASSERT_TRUE(backend.target_https); + TEST_ASSERT_EQUAL(443, backend.target_port); + TEST_ASSERT_EQUAL_STRING("example.com", backend.target_host); + TEST_ASSERT_EQUAL_STRING("/", backend.target_path); +} + +void test_parse_url_no_scheme(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("127.0.0.1:8080", &backend, 4); + TEST_ASSERT_FALSE(backend.target_https); + TEST_ASSERT_EQUAL(8080, backend.target_port); + TEST_ASSERT_EQUAL_STRING("127.0.0.1", backend.target_host); + TEST_ASSERT_EQUAL_STRING("", backend.target_path); +} + +void test_parse_url_no_port(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("http://localhost", &backend, 4); + TEST_ASSERT_FALSE(backend.target_https); + TEST_ASSERT_EQUAL(80, backend.target_port); + TEST_ASSERT_EQUAL_STRING("localhost", backend.target_host); +} + +void test_parse_url_https_no_port(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("https://secure.example.com", &backend, 4); + TEST_ASSERT_TRUE(backend.target_https); + TEST_ASSERT_EQUAL(443, backend.target_port); + TEST_ASSERT_EQUAL_STRING("secure.example.com", backend.target_host); +} + +void test_parse_url_with_path(void) { + cocoon_proxy_backend_t backend = {0}; + parse_backend_url("http://api.internal:5000/v2/users", &backend, 4); + TEST_ASSERT_FALSE(backend.target_https); + TEST_ASSERT_EQUAL(5000, backend.target_port); + TEST_ASSERT_EQUAL_STRING("api.internal", backend.target_host); + TEST_ASSERT_EQUAL_STRING("/v2/users", backend.target_path); +} + +/* ===== proxy_build_forwarded_path ===== */ + +void test_build_path_no_target_path(void) { + cocoon_proxy_rule_t rule = {0}; + strncpy(rule.path_prefix, "/api/", sizeof(rule.path_prefix) - 1); + cocoon_proxy_backend_t backend = {0}; + backend.target_path[0] = '\0'; + + char out[256]; + proxy_build_forwarded_path(&rule, &backend, "/api/users/123", out, sizeof(out)); + TEST_ASSERT_EQUAL_STRING("users/123", out); +} + +void test_build_path_with_target_path(void) { + cocoon_proxy_rule_t rule = {0}; + strncpy(rule.path_prefix, "/api/", sizeof(rule.path_prefix) - 1); + cocoon_proxy_backend_t backend = {0}; + strncpy(backend.target_path, "/v1", sizeof(backend.target_path) - 1); + + char out[256]; + proxy_build_forwarded_path(&rule, &backend, "/api/users", out, sizeof(out)); + TEST_ASSERT_EQUAL_STRING("/v1users", out); +} + +void test_build_path_empty_remaining(void) { + cocoon_proxy_rule_t rule = {0}; + strncpy(rule.path_prefix, "/api", sizeof(rule.path_prefix) - 1); + cocoon_proxy_backend_t backend = {0}; + backend.target_path[0] = '\0'; + + char out[256]; + proxy_build_forwarded_path(&rule, &backend, "/api", out, sizeof(out)); + TEST_ASSERT_EQUAL_STRING("/", out); +} + +/* ===== proxy_update_health ===== */ + +void test_update_health_success(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = false; + backend.success_count = 0; + + /* 第1次成功 — 仍不健康 */ + proxy_update_health(&backend, true); + TEST_ASSERT_FALSE(backend.healthy); + TEST_ASSERT_EQUAL(1, backend.success_count); + + /* 第2次成功 — 达到阈值 */ + proxy_update_health(&backend, true); + TEST_ASSERT_TRUE(backend.healthy); + TEST_ASSERT_EQUAL(2, backend.success_count); +} + +void test_update_health_failure_threshold(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + backend.fail_count = 0; + + /* 连续3次失败 */ + proxy_update_health(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); /* 第1次 */ + proxy_update_health(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); /* 第2次 */ + proxy_update_health(&backend, false); + TEST_ASSERT_FALSE(backend.healthy); /* 第3次,标记不健康 */ +} + +void test_update_health_reset_counts(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + + /* 失败1次 */ + proxy_update_health(&backend, false); + TEST_ASSERT_EQUAL(1, backend.fail_count); + + /* 成功1次 — fail_count 重置 */ + proxy_update_health(&backend, true); + TEST_ASSERT_EQUAL(0, backend.fail_count); + TEST_ASSERT_EQUAL(1, backend.success_count); +} + +void test_update_health_stays_healthy_after_single_failure(void) { + cocoon_proxy_backend_t backend = {0}; + backend.healthy = true; + + proxy_update_health(&backend, false); + TEST_ASSERT_TRUE(backend.healthy); + TEST_ASSERT_EQUAL(1, backend.fail_count); +} + +/* ===== proxy_pool_init ===== */ + +void test_pool_init_default_size(void) { + cocoon_proxy_backend_t backend = {0}; + proxy_pool_init(&backend, 4); + TEST_ASSERT_EQUAL(4, backend.pool.max_size); + proxy_pool_destroy(&backend); +} + +void test_pool_init_capped(void) { + cocoon_proxy_backend_t backend = {0}; + proxy_pool_init(&backend, 100); /* 超过上限 16 */ + TEST_ASSERT_EQUAL(COCOON_POOL_MAX_CAPACITY, backend.pool.max_size); + proxy_pool_destroy(&backend); +} + +void test_pool_init_zero_uses_default(void) { + cocoon_proxy_backend_t backend = {0}; + proxy_pool_init(&backend, 0); + TEST_ASSERT_EQUAL(COCOON_POOL_DEFAULT_SIZE, backend.pool.max_size); + proxy_pool_destroy(&backend); +} + +/* ===== 主函数 ===== */ + +int main(void) { + UNITY_BEGIN(); + + /* proxy_init */ + RUN_TEST(test_init_zeros); + + /* proxy_add_rule */ + RUN_TEST(test_add_rule_basic); + RUN_TEST(test_add_rule_append_backend); + RUN_TEST(test_add_rule_null_prefix); + RUN_TEST(test_add_rule_null_target); + + /* proxy_match */ + RUN_TEST(test_match_exact); + RUN_TEST(test_match_no_match); + RUN_TEST(test_match_multiple_rules); + RUN_TEST(test_match_null); + + /* select_backend_sww */ + RUN_TEST(test_sww_single); + RUN_TEST(test_sww_equal_weights); + RUN_TEST(test_sww_unequal_weights); + RUN_TEST(test_sww_skips_unhealthy); + RUN_TEST(test_sww_all_unhealthy); + + /* select_backend_sww_all */ + RUN_TEST(test_sww_all_includes_unhealthy); + + /* parse_backend_url */ + RUN_TEST(test_parse_url_http); + RUN_TEST(test_parse_url_https); + RUN_TEST(test_parse_url_no_scheme); + RUN_TEST(test_parse_url_no_port); + RUN_TEST(test_parse_url_https_no_port); + RUN_TEST(test_parse_url_with_path); + + /* proxy_build_forwarded_path */ + RUN_TEST(test_build_path_no_target_path); + RUN_TEST(test_build_path_with_target_path); + RUN_TEST(test_build_path_empty_remaining); + + /* proxy_update_health */ + RUN_TEST(test_update_health_success); + RUN_TEST(test_update_health_failure_threshold); + RUN_TEST(test_update_health_reset_counts); + RUN_TEST(test_update_health_stays_healthy_after_single_failure); + + /* proxy_pool_init */ + RUN_TEST(test_pool_init_default_size); + RUN_TEST(test_pool_init_capped); + RUN_TEST(test_pool_init_zero_uses_default); + + return UNITY_END(); +}