/** * http.js 单元测试 * 测试 HTTP 客户端功能 */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createRequestSummary, getDingTalkErrorDescription, sendRequest, validateResponse, } from '../../src/http.js'; import { dingTalkResponses } from '../fixtures/responses/dingtalk-responses.js'; // Mock fetch API const mockFetch = vi.fn(); global.fetch = mockFetch; describe('测试 http.js 文件', () => { /** * 设置 HTTP Mock 的辅助函数 * * @param {object} mockResponse - 模拟的 HTTP 响应 */ async function setupHttpMock(mockResponse) { const { statusCode = 200, data } = mockResponse; const responseText = typeof data === 'string' ? data : JSON.stringify(data); mockFetch.mockResolvedValueOnce({ status: statusCode, text: () => Promise.resolve(responseText), }); } /** * 设置网络错误 Mock 的辅助函数 * * @param {string} errorCode - 模拟的 HTTP 错误码 */ function setupNetworkErrorMock(errorCode) { const error = new Error(`Network error: ${errorCode}`); error.code = errorCode; mockFetch.mockRejectedValueOnce(error); } /** * 设置超时 Mock 的辅助函数 */ function setupTimeoutMock() { const error = new Error('Request timeout'); error.code = 'TIMEOUT'; mockFetch.mockRejectedValueOnce(error); } beforeEach(() => { // Mock console 方法 vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.spyOn(console, 'debug').mockImplementation(() => {}); }); afterEach(() => { // 恢复所有 mocks vi.restoreAllMocks(); // 重新设置 fetch mock mockFetch.mockClear(); }); describe('测试 sendRequest 函数', () => { const testUrl = 'https://oapi.dingtalk.com/robot/send?access_token=test'; const testMessage = { msgtype: 'text', text: { content: 'Hello World', }, }; it('应该成功发送请求', async () => { await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(true); expect(result.statusCode).toBe(200); expect(result.data).toEqual({ errcode: 0, errmsg: 'ok', }); expect(result.error).toBeUndefined(); // 验证 fetch 被调用 expect(mockFetch).toHaveBeenCalledWith(testUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': expect.stringContaining('DingTalk-Bot'), }, body: JSON.stringify(testMessage), }); }); it('应该处理钉钉 API 错误响应', async () => { await setupHttpMock({ statusCode: 200, data: dingTalkResponses.error.invalidToken }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); // 钉钉 API 错误码非0时 success 为 false expect(result.statusCode).toBe(200); expect(result.data).toEqual({ errcode: 310000, errmsg: 'keywords not in content', }); expect(result.error).toBeUndefined(); }); it('应该处理网络连接错误', async () => { setupNetworkErrorMock('ECONNREFUSED'); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(0); expect(result.data).toEqual({ errcode: -1, errmsg: 'Network error' }); expect(result.error).toContain('ECONNREFUSED'); }); it('应该处理超时错误', async () => { setupTimeoutMock(); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(0); expect(result.data).toEqual({ errcode: -1, errmsg: 'Network error' }); expect(result.error).toContain('timeout'); }); it('应该处理 DNS 解析错误', async () => { setupNetworkErrorMock('ENOTFOUND'); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(0); expect(result.data).toEqual({ errcode: -1, errmsg: 'Network error' }); expect(result.error).toContain('ENOTFOUND'); }); it('应该处理 SSL 错误', async () => { setupNetworkErrorMock('CERT_HAS_EXPIRED'); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(0); expect(result.data).toEqual({ errcode: -1, errmsg: 'Network error' }); expect(result.error).toContain('CERT_HAS_EXPIRED'); }); it('应该处理非 JSON 响应', async () => { await setupHttpMock({ statusCode: 200, data: 'Not JSON response', }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(200); expect(result.data).toEqual({}); expect(result.error).toContain('JSON parse error'); }); it('应该处理空响应', async () => { await setupHttpMock({ statusCode: 200, data: '', }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.statusCode).toBe(200); expect(result.data).toEqual({}); expect(result.error).toBeUndefined(); }); it('应该正确设置请求头', async () => { await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard }); await sendRequest(testUrl, testMessage); expect(mockFetch).toHaveBeenCalledWith(testUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': expect.stringContaining('DingTalk-Bot'), }, body: JSON.stringify(testMessage), }); }); }); describe('测试 validateResponse 函数', () => { it('应该验证有效的成功响应', () => { const response = { success: true, statusCode: 200, data: { errcode: 0, errmsg: 'ok', message_id: 'msg123456', }, error: null, }; const result = validateResponse(response); expect(result.isValid).toBe(true); expect(result.errors).toEqual([]); }); it('应该验证有效的错误响应', () => { const response = { success: false, statusCode: 400, data: { errcode: 310000, errmsg: 'invalid token', }, error: null, }; const result = validateResponse(response); expect(result.isValid).toBe(true); expect(result.errors).toEqual([]); }); it('应该检测缺少必需字段的响应', () => { const response = { success: true, statusCode: 200, data: { errcode: 0, // 缺少 errmsg }, error: null, }; const result = validateResponse(response); expect(result.isValid).toBe(false); expect(result.errors).toContain('Missing required field: errmsg'); }); it('应该检测字段类型错误', () => { const response = { success: true, statusCode: '200', // 应该是数字 data: { errcode: '0', // 应该是数字 errmsg: 'ok', }, error: null, }; const result = validateResponse(response); expect(result.isValid).toBe(false); expect(result.errors).toContain('statusCode must be a number'); expect(result.errors).toContain('errcode must be a number'); }); it('应该检测无效的响应结构', () => { const response = null; const result = validateResponse(response); expect(result.isValid).toBe(false); expect(result.errors).toContain('Response is null or undefined'); }); it('应该检测缺少 data 字段', () => { const response = { success: true, statusCode: 200, error: null, // 缺少 data }; const result = validateResponse(response); expect(result.isValid).toBe(false); expect(result.errors).toContain('Missing required field: data'); }); }); describe('测试 getDingTalkErrorDescription 函数', () => { it('应该返回已知错误码的描述', () => { expect(getDingTalkErrorDescription(310000)).toBe('无效的 webhook URL 或访问令牌'); expect(getDingTalkErrorDescription(310001)).toBe('无效签名'); expect(getDingTalkErrorDescription(310002)).toBe('无效时间戳'); expect(getDingTalkErrorDescription(310003)).toBe('无效请求格式'); expect(getDingTalkErrorDescription(310004)).toBe('消息内容过长'); expect(getDingTalkErrorDescription(310005)).toBe('消息发送频率超限'); expect(getDingTalkErrorDescription(-1)).toBe('系统繁忙,请稍后再试'); }); it('应该返回未知错误码的默认描述', () => { expect(getDingTalkErrorDescription(999999)).toBe('未知错误'); expect(getDingTalkErrorDescription(0)).toBe('请求成功'); }); it('应该处理非数字错误码', () => { expect(getDingTalkErrorDescription('invalid')).toBe('未知错误'); expect(getDingTalkErrorDescription(null)).toBe('未知错误'); expect(getDingTalkErrorDescription(undefined)).toBe('未知错误'); }); }); describe('测试 createRequestSummary 函数', () => { it('应该为成功响应创建摘要', () => { const response = { success: true, statusCode: 200, data: { errcode: 0, errmsg: 'ok', message_id: 'msg123456', }, error: null, duration: 150, }; const summary = createRequestSummary(response); expect(summary).toContain('Status: 200'); expect(summary).toContain('Duration: 150ms'); expect(summary).toContain('DingTalk Code: 0'); }); it('应该为失败响应创建摘要', () => { const response = { success: false, statusCode: 400, data: { errcode: 310000, errmsg: 'invalid token', }, error: null, duration: 200, }; const summary = createRequestSummary(response); expect(summary).toContain('Status: 400'); expect(summary).toContain('Duration: 200ms'); expect(summary).toContain('DingTalk Code: 310000'); expect(summary).toContain('无效的 webhook URL 或访问令牌'); }); it('应该为网络错误创建摘要', () => { const response = { success: false, statusCode: 0, data: {}, error: 'ECONNREFUSED', duration: 5000, }; const summary = createRequestSummary(response); expect(summary).toContain('Status: 0'); expect(summary).toContain('Duration: 5000ms'); expect(summary).toContain('Error: ECONNREFUSED'); }); it('应该处理缺少消息ID的成功响应', () => { const response = { success: true, statusCode: 200, data: { errcode: 0, errmsg: 'ok', // 缺少 message_id }, error: null, duration: 100, }; const summary = createRequestSummary(response); expect(summary).toContain('Status: 200'); expect(summary).toContain('Duration: 100ms'); expect(summary).toContain('DingTalk Code: 0'); expect(summary).not.toContain('undefined'); }); it('应该处理空的错误消息', () => { const response = { success: false, statusCode: 400, data: { errcode: 300001, errmsg: '', }, error: null, duration: 250, }; const summary = createRequestSummary(response); expect(summary).toContain('Status: 400'); expect(summary).toContain('Duration: 250ms'); expect(summary).toContain('DingTalk Code: 300001'); expect(summary).toContain('未知错误'); }); }); describe('边界条件测试', () => { const testUrl = 'https://oapi.dingtalk.com/robot/send?access_token=test'; const testMessage = { msgtype: 'text', text: { content: 'test' } }; it('应该处理极大的响应数据', async () => { const largeData = { errcode: 0, errmsg: 'ok', message_id: 'msg123456', large_field: 'x'.repeat(10000), // 10KB 的数据 }; await setupHttpMock({ statusCode: 200, data: largeData, }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(true); expect(result.data.large_field).toBe('x'.repeat(10000)); }); it('应该处理特殊字符的响应', async () => { const specialData = { errcode: 0, errmsg: 'ok with 特殊字符 and émojis 🎉', message_id: 'msg123456', }; await setupHttpMock({ statusCode: 200, data: specialData, }); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(true); expect(result.data.errmsg).toBe('ok with 特殊字符 and émojis 🎉'); }); it('应该处理畸形的 JSON 响应', async () => { await setupHttpMock(dingTalkResponses.special.malformedJson); const result = await sendRequest(testUrl, testMessage); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(typeof result.error).toBe('string'); expect(result.error).toContain('JSON parse error'); }); }); });