feat: 初始化 Gitea Action 发送钉钉机器人消息项目
This commit is contained in:
476
tests/unit/http.test.js
Normal file
476
tests/unit/http.test.js
Normal file
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user