Files
dingtalk-bot/tests/helpers/http-mock.js

268 lines
6.8 KiB
JavaScript

/**
* HTTP Mock 工具
* 用于在测试中模拟 HTTP 请求和响应
*/
import { EventEmitter } from 'events';
import { vi } from 'vitest';
/**
* Mock HTTP 响应
*/
export class MockResponse extends EventEmitter {
constructor(statusCode = 200, data = {}) {
super();
this.statusCode = statusCode;
this.data = data;
this.headers = {};
}
emit(event, ...args) {
// 异步触发事件,模拟真实网络延迟
setImmediate(() => super.emit(event, ...args));
}
simulateResponse() {
// 如果 data 已经是字符串,直接使用;否则进行 JSON 序列化
const responseData = typeof this.data === 'string' ? this.data : JSON.stringify(this.data);
this.emit('data', responseData);
this.emit('end');
}
simulateError(error) {
this.emit('error', error);
}
simulateTimeout() {
this.emit('timeout');
}
setHeader(name, value) {
this.headers[name] = value;
}
getHeader(name) {
return this.headers[name];
}
}
/**
* Mock HTTP 请求
*/
export class MockRequest extends EventEmitter {
constructor() {
super();
this.written = [];
this.ended = false;
this.destroyed = false;
this.headers = {};
}
write(data) {
this.written.push(data);
return true;
}
end(data) {
if (data) {
this.write(data);
}
this.ended = true;
this.emit('end');
}
destroy() {
this.destroyed = true;
this.emit('close');
}
setTimeout(timeout, callback) {
setTimeout(() => {
if (!this.ended && !this.destroyed) {
this.emit('timeout');
if (callback) callback();
}
}, timeout);
}
setHeader(name, value) {
this.headers[name] = value;
}
getHeader(name) {
return this.headers[name];
}
getWrittenData() {
return this.written.join('');
}
}
/**
* 创建模拟的 HTTPS 模块用于测试
*
* @typedef {object} HttpsMockModule
* @property {import('vitest').Mock} request - 模拟的 HTTPS 请求方法
* @property {function(): Array} getRequests - 获取所有已发送的请求列表
* @property {function(number, any): void} addResponse - 添加预定义的响应
* @property {function(Error): void} addErrorResponse - 添加错误响应
* @property {function(): void} addTimeoutResponse - 添加超时响应
* @property {function(any): void} mockRequestOnce - 为单次请求设置响应
* @property {function(): void} reset - 重置所有请求和响应状态
* @property {function(): object} getLastRequest - 获取最后一次请求的详细信息
* @property {function(): number} getRequestCount - 获取已发送请求的数量
*
* @returns {HttpsMockModule} 模拟的 HTTPS 模块对象
*/
export function createHttpsMock() {
const requests = [];
const responses = [];
const mockHttps = {
request: vi.fn((options, callback) => {
const req = new MockRequest();
const res = responses.shift() || new MockResponse();
requests.push({ options, req, res });
// 异步调用回调
setImmediate(() => {
if (callback) callback(res);
res.simulateResponse();
});
return req;
}),
// 测试辅助方法
getRequests: () => requests,
addResponse: (statusCode, data) => {
responses.push(new MockResponse(statusCode, data));
},
addErrorResponse: error => {
const res = new MockResponse();
responses.push(res);
setImmediate(() => res.simulateError(error));
},
addTimeoutResponse: () => {
const res = new MockResponse();
responses.push(res);
setImmediate(() => res.simulateTimeout());
},
// 添加单次请求响应
mockRequestOnce: responseData => {
if (typeof responseData === 'object' && responseData.statusCode !== undefined) {
// 如果传入的是包含 statusCode 的对象
responses.push(
new MockResponse(responseData.statusCode, responseData.data || responseData),
);
} else {
// 如果传入的是普通响应数据,默认使用 200 状态码
responses.push(new MockResponse(200, responseData));
}
},
reset: () => {
requests.length = 0;
responses.length = 0;
mockHttps.request.mockClear();
},
// 获取最后一次请求的详细信息
getLastRequest: () => {
return requests[requests.length - 1];
},
// 获取请求数量
getRequestCount: () => requests.length,
};
return mockHttps;
}
/**
* 创建简单的 HTTP Mock
*
* @param {number|object} statusCodeOrResponse - 状态码或包含 statusCode 和 data 的响应对象
* @param {object} data - 响应数据(当第一个参数是状态码时使用)
* @returns {object} Mock 对象
*/
export function createSimpleHttpMock(statusCodeOrResponse = 200, data = {}) {
const mockHttps = createHttpsMock();
if (typeof statusCodeOrResponse === 'object' && statusCodeOrResponse !== null) {
// 如果传入的是响应对象,检查是否有 statusCode 和 data 属性
if (statusCodeOrResponse.statusCode && statusCodeOrResponse.data) {
mockHttps.addResponse(statusCodeOrResponse.statusCode, statusCodeOrResponse.data);
} else {
// 如果只是数据对象,默认使用 200 状态码
mockHttps.addResponse(200, statusCodeOrResponse);
}
} else {
// 传统的调用方式:状态码和数据分开传递
mockHttps.addResponse(statusCodeOrResponse, data);
}
return mockHttps;
}
/**
* 创建网络错误 Mock
*
* @param {string} errorCode - 错误代码
* @returns {object} Mock 对象
*/
export function createNetworkErrorMock(errorCode = 'ECONNREFUSED') {
const mockHttps = createHttpsMock();
const error = new Error(`Network error: ${errorCode}`);
error.code = errorCode;
mockHttps.addErrorResponse(error);
return mockHttps;
}
/**
* 创建超时 Mock
*
* @returns {object} Mock 对象
*/
export function createTimeoutMock() {
const mockHttps = createHttpsMock();
mockHttps.addTimeoutResponse();
return mockHttps;
}
/**
* 验证请求选项
*
* @param {object} request - 请求对象
* @param {object} expectedOptions - 期望的选项
*/
export function assertRequestOptions(request, expectedOptions) {
const { options } = request;
for (const [key, value] of Object.entries(expectedOptions)) {
if (options[key] !== value) {
throw new Error(`Expected ${key} to be ${value}, but got ${options[key]}`);
}
}
}
/**
* 验证请求体
*
* @param {object} request - 请求对象
* @param {object} expectedBody - 期望的请求体
*/
export function assertRequestBody(request, expectedBody) {
const actualBody = JSON.parse(request.req.getWrittenData());
for (const [key, value] of Object.entries(expectedBody)) {
if (JSON.stringify(actualBody[key]) !== JSON.stringify(value)) {
throw new Error(
`Expected ${key} to be ${JSON.stringify(value)}, but got ${JSON.stringify(actualBody[key])}`,
);
}
}
}