cocoon/tests/unit/test_fastcgi.c
xfy911 57948d9dc1 feat: FastCGI 协议核心实现 + 17 项单元测试
- 新增 fastcgi.c/h:FastCGI 1.0 协议客户端实现
  - 记录编码/解码(BEGIN_REQUEST/PARAMS/STDIN/STDOUT/STDERR/END_REQUEST)
  - 名值对(NV)编码(支持短/长长度)
  - 参数管理(SCRIPT_NAME/REQUEST_METHOD 等 CGI 环境变量)
  - 响应解析与状态码提取(Status: 404 → 404)
  - 响应体提取(从 stdout 数据中提取 body)
  - 连接池(预创建连接、获取/归还)
  - 完整请求流程(发送 BeginRequest + Params + Stdin + 接收响应)

- 新增 tests/unit/test_fastcgi.c:17 项单元测试
  - 记录编码/解码
  - 名值对编码(短/长长度)
  - 参数管理
  - 响应解析
  - 状态码/Body 提取
  - 请求初始化

- 更新 Makefile:添加 fastcgi.c 编译规则和 test_fastcgi 测试规则
- 更新 .gitignore:修复 tests/unit/test_* 规则,确保 .c 源文件被跟踪

编译零警告,458 单元测试 + 115 集成测试全部通过 ✓
2026-06-15 08:28:57 +08:00

320 lines
10 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.

/*
* test_fastcgi.c - FastCGI 模块单元测试
*/
#include "unity.h"
#include "fastcgi.h"
#include <string.h>
#include <stdlib.h>
void setUp(void) {}
void tearDown(void) {}
/* ========== 记录编码测试 ========== */
static void test_fcgi_build_record(void) {
uint8_t buf[256];
uint8_t data[] = "hello";
int n = fcgi_build_record(FCGI_PARAMS, 1, data, 5, buf, sizeof(buf));
TEST_ASSERT_GREATER_THAN(0, n);
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_PARAMS, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]); /* requestId high */
TEST_ASSERT_EQUAL(1, buf[3]); /* requestId low */
TEST_ASSERT_EQUAL(0, buf[4]); /* contentLength high */
TEST_ASSERT_EQUAL(5, buf[5]); /* contentLength low */
TEST_ASSERT_EQUAL(3, buf[6]); /* padding = 8-5%8 = 3 */
TEST_ASSERT_EQUAL(0, buf[7]); /* reserved */
TEST_ASSERT_EQUAL_MEMORY(data, buf + 8, 5);
}
static void test_fcgi_build_begin_request(void) {
uint8_t buf[256];
int n = fcgi_build_begin_request(42, FCGI_RESPONDER, FCGI_KEEP_CONN, buf, sizeof(buf));
TEST_ASSERT_EQUAL(16, n); /* header 8 + body 8 + padding 0 */
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_BEGIN_REQUEST, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]);
TEST_ASSERT_EQUAL(42, buf[3]);
/* role 应该是大端FCGI_RESPONDER = 1 */
TEST_ASSERT_EQUAL(0, buf[8]);
TEST_ASSERT_EQUAL(1, buf[9]); /* role low byte */
TEST_ASSERT_EQUAL(FCGI_KEEP_CONN, buf[10]); /* flags */
}
static void test_fcgi_build_empty_stdin(void) {
uint8_t buf[256];
int n = fcgi_build_empty_stdin(7, buf, sizeof(buf));
TEST_ASSERT_EQUAL(8, n); /* 只有 header无内容 */
TEST_ASSERT_EQUAL(FCGI_VERSION_1, buf[0]);
TEST_ASSERT_EQUAL(FCGI_STDIN, buf[1]);
TEST_ASSERT_EQUAL(0, buf[2]);
TEST_ASSERT_EQUAL(7, buf[3]);
TEST_ASSERT_EQUAL(0, buf[4]); /* contentLength = 0 */
TEST_ASSERT_EQUAL(0, buf[5]);
TEST_ASSERT_EQUAL(0, buf[6]); /* padding = 0 */
}
/* ========== 名值对编码测试 ========== */
static void test_fcgi_encode_name_value_short(void) {
uint8_t buf[256];
size_t len;
bool ok = fcgi_encode_name_value_len("name", "value", buf, &len);
TEST_ASSERT_TRUE(ok);
/* name_len=4 (1 byte), value_len=5 (1 byte) */
TEST_ASSERT_EQUAL(4, buf[0]);
TEST_ASSERT_EQUAL(5, buf[1]);
TEST_ASSERT_EQUAL_MEMORY("name", buf + 2, 4);
TEST_ASSERT_EQUAL_MEMORY("value", buf + 6, 5);
TEST_ASSERT_EQUAL(2 + 4 + 5, len);
}
static void test_fcgi_encode_name_value_long(void) {
uint8_t buf[256];
size_t len;
/* 构造一个长度 > 127 的 name */
char long_name[200];
memset(long_name, 'a', 199);
long_name[199] = '\0';
bool ok = fcgi_encode_name_value_len(long_name, "v", buf, &len);
TEST_ASSERT_TRUE(ok);
/* 长度编码应该用 4 bytes */
TEST_ASSERT_TRUE((buf[0] & 0x80) != 0); /* 高位置1 */
}
/* ========== 参数管理测试 ========== */
static void test_fcgi_add_param(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
TEST_ASSERT_TRUE(fcgi_add_param(&req, "SCRIPT_NAME", "/index.php"));
TEST_ASSERT_TRUE(fcgi_add_param(&req, "REQUEST_METHOD", "GET"));
TEST_ASSERT_NOT_NULL(req.params);
fcgi_request_free(&req);
}
static void test_fcgi_build_params(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
fcgi_add_param(&req, "SCRIPT_NAME", "/index.php");
fcgi_add_param(&req, "REQUEST_METHOD", "GET");
uint8_t buf[1024];
int n = fcgi_build_params(1, req.params, buf, sizeof(buf));
TEST_ASSERT_GREATER_THAN(0, n);
/* 应该至少包含一个 PARAMS 记录和一个空的 PARAMS 结束记录 */
TEST_ASSERT_GREATER_OR_EQUAL(16, n); /* 两个 header */
fcgi_request_free(&req);
}
static void test_fcgi_build_params_empty(void) {
fcgi_request_t req;
fcgi_request_init(&req, 1);
uint8_t buf[1024];
int n = fcgi_build_params(1, req.params, buf, sizeof(buf));
TEST_ASSERT_EQUAL(8, n); /* 只有空的 PARAMS 记录 */
fcgi_request_free(&req);
}
/* ========== 记录解析测试 ========== */
static void test_fcgi_parse_record(void) {
uint8_t data[] = {
1, FCGI_STDOUT, 0, 42, 0, 5, 3, 0, /* header */
'h', 'e', 'l', 'l', 'o', /* content */
0, 0, 0 /* padding */
};
fcgi_header_t header;
const uint8_t *content = NULL;
size_t consumed = 0;
bool ok = fcgi_parse_record(data, sizeof(data), &header, &content, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(FCGI_VERSION_1, header.version);
TEST_ASSERT_EQUAL(FCGI_STDOUT, header.type);
TEST_ASSERT_EQUAL(42, header.requestId);
TEST_ASSERT_EQUAL(5, header.contentLength);
TEST_ASSERT_EQUAL(3, header.paddingLength);
TEST_ASSERT_EQUAL(16, consumed);
TEST_ASSERT_EQUAL_MEMORY("hello", content, 5);
}
static void test_fcgi_parse_record_insufficient(void) {
uint8_t data[] = { 1, FCGI_STDOUT, 0, 42 }; /* 只有 4 bytes不够 header */
fcgi_header_t header;
const uint8_t *content = NULL;
size_t consumed = 0;
bool ok = fcgi_parse_record(data, sizeof(data), &header, &content, &consumed);
TEST_ASSERT_FALSE(ok);
}
/* ========== 响应解析测试 ========== */
static void test_fcgi_parse_response_stdout(void) {
uint8_t stdout_rec[] = {
1, FCGI_STDOUT, 0, 1, 0, 5, 3, 0,
'h', 'e', 'l', 'l', 'o',
0, 0, 0
};
uint8_t end_rec[] = {
1, FCGI_END_REQUEST, 0, 1, 0, 8, 0, 0,
0, 0, 0, 0, /* appStatus = 0 */
0, /* protocolStatus = FCGI_REQUEST_COMPLETE */
0, 0, 0
};
uint8_t data[sizeof(stdout_rec) + sizeof(end_rec)];
memcpy(data, stdout_rec, sizeof(stdout_rec));
memcpy(data + sizeof(stdout_rec), end_rec, sizeof(end_rec));
fcgi_response_t resp;
memset(&resp, 0, sizeof(resp));
size_t consumed = 0;
bool ok = fcgi_parse_response(data, sizeof(data), &resp, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_TRUE(resp.complete);
TEST_ASSERT_EQUAL(0, resp.appStatus);
TEST_ASSERT_EQUAL(5, resp.stdout_len);
TEST_ASSERT_EQUAL_MEMORY("hello", resp.stdout_data, 5);
fcgi_response_free(&resp);
}
static void test_fcgi_parse_response_status(void) {
/* 模拟 PHP 输出 Status: 404 */
uint8_t stdout_rec[] = {
1, FCGI_STDOUT, 0, 1,
0, 27, 5, 0, /* contentLength = 27 */
'S', 't', 'a', 't', 'u', 's', ':', ' ', '4', '0', '4', '\r', '\n',
'C', 'o', 'n', 't', 'e', 'n', 't', '-', 't', 'y', 'p', 'e', ':',
' ', 't', 'e', 'x', 't', '/', 'h', 't', 'm', 'l', '\r', '\n',
'\r', '\n',
'N', 'o', 't', ' ', 'F', 'o', 'u', 'n', 'd',
0, 0, 0, 0, 0 /* padding */
};
/* 修正长度计算... */
fcgi_response_t resp;
memset(&resp, 0, sizeof(resp));
size_t consumed = 0;
/* 用简单测试 */
const char *status_line = "Status: 404\r\n\r\n";
uint8_t rec[64];
rec[0] = 1; rec[1] = FCGI_STDOUT; rec[2] = 0; rec[3] = 1;
rec[4] = 0; rec[5] = strlen(status_line); rec[6] = 0; rec[7] = 0;
memcpy(rec + 8, status_line, strlen(status_line));
uint8_t end[] = { 1, FCGI_END_REQUEST, 0, 1, 0, 8, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0 };
uint8_t data[128];
memcpy(data, rec, 8 + strlen(status_line));
memcpy(data + 8 + strlen(status_line), end, 16);
bool ok = fcgi_parse_response(data, 8 + strlen(status_line) + 16, &resp, &consumed);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_TRUE(resp.complete);
TEST_ASSERT_EQUAL(404, fcgi_extract_status(resp.stdout_data, resp.stdout_len));
fcgi_response_free(&resp);
}
/* ========== 状态码/Body 提取测试 ========== */
static void test_fcgi_extract_status(void) {
const char *data = "Status: 404\r\nContent-type: text/html\r\n\r\nNot found";
int status = fcgi_extract_status(data, strlen(data));
TEST_ASSERT_EQUAL(404, status);
}
static void test_fcgi_extract_status_default(void) {
const char *data = "Content-type: text/html\r\n\r\nHello";
int status = fcgi_extract_status(data, strlen(data));
TEST_ASSERT_EQUAL(200, status);
}
static void test_fcgi_extract_body(void) {
const char *data = "Content-type: text/html\r\n\r\nHello World";
const char *body = NULL;
size_t body_len = 0;
bool ok = fcgi_extract_body(data, strlen(data), &body, &body_len);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(11, body_len);
TEST_ASSERT_EQUAL_MEMORY("Hello World", body, 11);
}
static void test_fcgi_extract_body_lf_only(void) {
const char *data = "Content-type: text/html\n\nHello World";
const char *body = NULL;
size_t body_len = 0;
bool ok = fcgi_extract_body(data, strlen(data), &body, &body_len);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(11, body_len);
}
/* ========== 请求 ID 管理 ========== */
static void test_fcgi_request_init(void) {
fcgi_request_t req;
bool ok = fcgi_request_init(&req, 42);
TEST_ASSERT_TRUE(ok);
TEST_ASSERT_EQUAL(42, req.requestId);
TEST_ASSERT_EQUAL(-1, req.backend_fd);
TEST_ASSERT_TRUE(req.keep_conn);
TEST_ASSERT_NULL(req.params);
}
/* ========== 主函数 ========== */
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_fcgi_build_record);
RUN_TEST(test_fcgi_build_begin_request);
RUN_TEST(test_fcgi_build_empty_stdin);
RUN_TEST(test_fcgi_encode_name_value_short);
RUN_TEST(test_fcgi_encode_name_value_long);
RUN_TEST(test_fcgi_add_param);
RUN_TEST(test_fcgi_build_params);
RUN_TEST(test_fcgi_build_params_empty);
RUN_TEST(test_fcgi_parse_record);
RUN_TEST(test_fcgi_parse_record_insufficient);
RUN_TEST(test_fcgi_parse_response_stdout);
RUN_TEST(test_fcgi_parse_response_status);
RUN_TEST(test_fcgi_extract_status);
RUN_TEST(test_fcgi_extract_status_default);
RUN_TEST(test_fcgi_extract_body);
RUN_TEST(test_fcgi_extract_body_lf_only);
RUN_TEST(test_fcgi_request_init);
return UNITY_END();
}