477 lines
14 KiB
JavaScript
477 lines
14 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
});
|
|
});
|