diff --git a/Makefile b/Makefile index 50fda1b..67b67f7 100644 --- a/Makefile +++ b/Makefile @@ -95,8 +95,8 @@ $(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SR $(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(LDFLAGS) -$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm +$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC) $(LDFLAGS) $(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 diff --git a/tests/unit/test_websocket.c b/tests/unit/test_websocket.c new file mode 100644 index 0000000..85b34ae --- /dev/null +++ b/tests/unit/test_websocket.c @@ -0,0 +1,254 @@ +/** + * @file test_websocket.c + * @brief WebSocket 协议单元测试 + * + * 覆盖帧解析、帧编码、握手、广播注册表等核心逻辑。 + * 使用 socketpair 创建内部通信管道,避免真实网络依赖。 + */ + +#include "unity.h" +#include "websocket.h" +#include +#include +#include +#include + +static int client_fd, server_fd; + +void setUp(void) { + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { + TEST_FAIL_MESSAGE("socketpair 失败"); + } + client_fd = fds[0]; + server_fd = fds[1]; +} + +void tearDown(void) { + close(client_fd); + close(server_fd); +} + +/* 测试 ws_parse_frame — 基本文本帧(无掩码) */ +void test_parse_text_frame(void) { + uint8_t data[] = { 0x81, 0x05, 'H', 'e', 'l', 'l', 'o' }; + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data, sizeof(data), &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(7, consumed); + TEST_ASSERT_TRUE(frame.fin); + TEST_ASSERT_EQUAL_INT(WS_OP_TEXT, frame.opcode); + TEST_ASSERT_FALSE(frame.masked); + TEST_ASSERT_EQUAL_INT(5, frame.payload_len); + TEST_ASSERT_EQUAL_STRING("Hello", (char *)frame.payload); + ws_frame_free(&frame); +} + +/* 测试 ws_parse_frame — 掩码帧(客户端发来的帧) */ +void test_parse_masked_frame(void) { + /* mask_key = {0x12, 0x34, 0x56, 0x78} + * payload = XOR(0x41, 0x12) = 0x53 'S' + * = XOR(0x59, 0x34) = 0x6D 'm' + * = XOR(0x32, 0x56) = 0x64 'd' + * = XOR(0x0C, 0x78) = 0x74 't' + * = XOR(0x5D, 0x12) = 0x4F 'O' + */ + uint8_t data[] = { 0x81, 0x85, 0x12, 0x34, 0x56, 0x78, + 0x41, 0x59, 0x32, 0x0C, 0x5D }; + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data, sizeof(data), &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(11, consumed); + TEST_ASSERT_TRUE(frame.masked); + TEST_ASSERT_EQUAL_INT(5, frame.payload_len); + TEST_ASSERT_EQUAL_STRING("SmdtO", (char *)frame.payload); + ws_frame_free(&frame); +} + +/* 测试 ws_parse_frame — 数据不完整应返回 -1 */ +void test_parse_incomplete_frame(void) { + uint8_t data[] = { 0x81, 0x05, 'H' }; /* 需要 5 字节 payload,只有 1 字节 */ + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(-1, ws_parse_frame(data, sizeof(data), &frame, &consumed)); +} + +/* 测试 ws_parse_frame — 16 位扩展长度(126 ~ 65535) */ +void test_parse_16bit_length(void) { + uint8_t payload[130]; + memset(payload, 'A', 130); + uint8_t data[134] = { 0x82, 0x7E, 0x00, 0x82 }; /* BINARY, 长度=130 */ + memcpy(data + 4, payload, 130); + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data, sizeof(data), &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(134, consumed); + TEST_ASSERT_EQUAL_INT(WS_OP_BINARY, frame.opcode); + TEST_ASSERT_EQUAL_INT(130, frame.payload_len); + TEST_ASSERT_EQUAL_INT(0, memcmp(frame.payload, payload, 130)); + ws_frame_free(&frame); +} + +/* 测试 ws_send_frame + ws_parse_frame 往返(文本帧) */ +void test_send_parse_roundtrip_text(void) { + const char *text = "Cocoon WebSocket"; + TEST_ASSERT_EQUAL_INT(0, ws_send_text(server_fd, text)); + + uint8_t buf[64]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(buf, n, &frame, &consumed)); + TEST_ASSERT_TRUE(frame.fin); + TEST_ASSERT_EQUAL_INT(WS_OP_TEXT, frame.opcode); + TEST_ASSERT_EQUAL_INT(strlen(text), frame.payload_len); + TEST_ASSERT_EQUAL_STRING(text, (char *)frame.payload); + ws_frame_free(&frame); +} + +/* 测试 ws_send_close — 带关闭码和原因 */ +void test_send_close_with_reason(void) { + TEST_ASSERT_EQUAL_INT(0, ws_send_close(server_fd, 1000, "bye")); + uint8_t buf[32]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(buf, n, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(WS_OP_CLOSE, frame.opcode); + TEST_ASSERT_EQUAL_INT(5, frame.payload_len); /* 2 字节 code + 3 字节 "bye" */ + ws_frame_free(&frame); +} + +/* 测试 ws_send_ping / ws_send_pong */ +void test_send_ping_pong(void) { + /* Ping 帧(无负载) */ + TEST_ASSERT_EQUAL_INT(0, ws_send_ping(server_fd)); + uint8_t buf[16]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(buf, n, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(WS_OP_PING, frame.opcode); + TEST_ASSERT_EQUAL_INT(0, frame.payload_len); + ws_frame_free(&frame); + + /* Pong 帧(带负载) */ + uint8_t payload[] = { 0x01, 0x02, 0x03 }; + TEST_ASSERT_EQUAL_INT(0, ws_send_pong(server_fd, payload, 3)); + n = recv(client_fd, buf, sizeof(buf), 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(buf, n, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(WS_OP_PONG, frame.opcode); + TEST_ASSERT_EQUAL_INT(3, frame.payload_len); + ws_frame_free(&frame); +} + +/* 测试 ws_handshake — 验证响应头和 Sec-WebSocket-Accept */ +void test_handshake_accept(void) { + const char *key = "dGhlIHNhbXBsZSBub25jZQ=="; + TEST_ASSERT_EQUAL_INT(0, ws_handshake(server_fd, key)); + + char buf[512]; + ssize_t n = recv(client_fd, buf, sizeof(buf) - 1, 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + buf[n] = '\0'; + + TEST_ASSERT_NOT_NULL(strstr(buf, "HTTP/1.1 101 Switching Protocols")); + TEST_ASSERT_NOT_NULL(strstr(buf, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")); + TEST_ASSERT_NOT_NULL(strstr(buf, "Upgrade: websocket")); + TEST_ASSERT_NOT_NULL(strstr(buf, "Connection: Upgrade")); +} + +/* 测试 ws_connection_count — 初始为 0 */ +void test_connection_count_initial(void) { + TEST_ASSERT_EQUAL_INT(0, ws_connection_count()); +} + +/* 测试 ws_send_frame 大负载(64 位长度编码,>65535) */ +void test_send_64bit_length(void) { + uint8_t *payload = (uint8_t *)malloc(65536); + TEST_ASSERT_NOT_NULL(payload); + memset(payload, 0xAB, 65536); + + TEST_ASSERT_EQUAL_INT(0, ws_send_frame(server_fd, WS_OP_BINARY, payload, 65536)); + + uint8_t *buf = (uint8_t *)malloc(65546); + TEST_ASSERT_NOT_NULL(buf); + ssize_t total = 0; + while (total < 65546) { + ssize_t n = recv(client_fd, buf + total, 65546 - total, 0); + TEST_ASSERT_GREATER_THAN_INT(0, n); + total += n; + } + + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(buf, total, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(WS_OP_BINARY, frame.opcode); + TEST_ASSERT_EQUAL_INT(65536, frame.payload_len); + TEST_ASSERT_EQUAL_INT(0, memcmp(frame.payload, payload, 65536)); + ws_frame_free(&frame); + free(payload); + free(buf); +} + +/* 测试 ws_parse_frame — 多帧连续解析(模拟粘包) */ +void test_parse_multiple_frames(void) { + uint8_t frame1[] = { 0x81, 0x03, 'a', 'b', 'c' }; /* 文本 "abc" */ + uint8_t frame2[] = { 0x82, 0x02, 0x01, 0x02 }; /* 二进制 2 字节 */ + uint8_t data[16]; + memcpy(data, frame1, 5); + memcpy(data + 5, frame2, 4); + + ws_frame_t frame = {0}; + size_t consumed = 0; + size_t offset = 0; + + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data + offset, 9 - offset, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(5, consumed); + TEST_ASSERT_EQUAL_INT(WS_OP_TEXT, frame.opcode); + TEST_ASSERT_EQUAL_STRING("abc", (char *)frame.payload); + ws_frame_free(&frame); + + offset = 5; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data + offset, 9 - offset, &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(4, consumed); + TEST_ASSERT_EQUAL_INT(WS_OP_BINARY, frame.opcode); + TEST_ASSERT_EQUAL_INT(2, frame.payload_len); + ws_frame_free(&frame); +} + +/* 测试 ws_parse_frame — 空负载帧 */ +void test_parse_empty_payload(void) { + uint8_t data[] = { 0x88, 0x00 }; /* close 帧,无负载 */ + ws_frame_t frame = {0}; + size_t consumed = 0; + TEST_ASSERT_EQUAL_INT(0, ws_parse_frame(data, sizeof(data), &frame, &consumed)); + TEST_ASSERT_EQUAL_INT(2, consumed); + TEST_ASSERT_EQUAL_INT(WS_OP_CLOSE, frame.opcode); + TEST_ASSERT_EQUAL_INT(0, frame.payload_len); + TEST_ASSERT_NULL(frame.payload); + ws_frame_free(&frame); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_parse_text_frame); + RUN_TEST(test_parse_masked_frame); + RUN_TEST(test_parse_incomplete_frame); + RUN_TEST(test_parse_16bit_length); + RUN_TEST(test_send_parse_roundtrip_text); + RUN_TEST(test_send_close_with_reason); + RUN_TEST(test_send_ping_pong); + RUN_TEST(test_handshake_accept); + RUN_TEST(test_connection_count_initial); + RUN_TEST(test_send_64bit_length); + RUN_TEST(test_parse_multiple_frames); + RUN_TEST(test_parse_empty_payload); + return UNITY_END(); +}