feat: 初始化 Gitea Action 发送钉钉机器人消息项目
This commit is contained in:
243
tests/helpers/assertions.js
Normal file
243
tests/helpers/assertions.js
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 自定义断言工具
|
||||
* 提供项目特定的断言方法
|
||||
*/
|
||||
|
||||
import { expect } from 'vitest';
|
||||
|
||||
/**
|
||||
* 验证钉钉消息格式
|
||||
*
|
||||
* @param {object} message - 消息对象
|
||||
* @param {string} expectedType - 期望的消息类型
|
||||
*/
|
||||
export function assertDingTalkMessage(message, expectedType) {
|
||||
expect(message).toBeDefined();
|
||||
expect(message).toBeTypeOf('object');
|
||||
expect(message.msgtype).toBe(expectedType);
|
||||
|
||||
switch (expectedType) {
|
||||
case 'text':
|
||||
expect(message.text).toBeDefined();
|
||||
expect(message.text.content).toBeTypeOf('string');
|
||||
break;
|
||||
case 'markdown':
|
||||
expect(message.markdown).toBeDefined();
|
||||
expect(message.markdown.title).toBeTypeOf('string');
|
||||
expect(message.markdown.text).toBeTypeOf('string');
|
||||
break;
|
||||
case 'link':
|
||||
expect(message.link).toBeDefined();
|
||||
expect(message.link.title).toBeTypeOf('string');
|
||||
expect(message.link.text).toBeTypeOf('string');
|
||||
expect(message.link.messageUrl).toBeTypeOf('string');
|
||||
break;
|
||||
case 'actionCard':
|
||||
expect(message.actionCard).toBeDefined();
|
||||
expect(message.actionCard.title).toBeTypeOf('string');
|
||||
expect(message.actionCard.text).toBeTypeOf('string');
|
||||
break;
|
||||
case 'feedCard':
|
||||
expect(message.feedCard).toBeDefined();
|
||||
expect(message.feedCard.links).toBeInstanceOf(Array);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 @ 功能配置
|
||||
*
|
||||
* @param {object} message - 消息对象
|
||||
* @param {object} expectedAt - 期望的 @ 配置
|
||||
*/
|
||||
export function assertAtConfiguration(message, expectedAt) {
|
||||
expect(message.at).toBeDefined();
|
||||
|
||||
if (expectedAt.atMobiles) {
|
||||
expect(message.at.atMobiles).toEqual(expectedAt.atMobiles);
|
||||
}
|
||||
|
||||
if (expectedAt.atUserIds) {
|
||||
expect(message.at.atUserIds).toEqual(expectedAt.atUserIds);
|
||||
}
|
||||
|
||||
if (expectedAt.isAtAll !== undefined) {
|
||||
expect(message.at.isAtAll).toBe(expectedAt.isAtAll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 ActionCard 按钮配置
|
||||
*
|
||||
* @param {object} actionCard - ActionCard 对象
|
||||
* @param {object} expectedButtons - 期望的按钮配置
|
||||
*/
|
||||
export function assertActionCardButtons(actionCard, expectedButtons) {
|
||||
if (expectedButtons.single) {
|
||||
expect(actionCard.singleTitle).toBe(expectedButtons.single.title);
|
||||
expect(actionCard.singleURL).toBe(expectedButtons.single.url);
|
||||
expect(actionCard.btns).toBeUndefined();
|
||||
} else if (expectedButtons.multiple) {
|
||||
expect(actionCard.btns).toBeInstanceOf(Array);
|
||||
expect(actionCard.btns).toHaveLength(expectedButtons.multiple.length);
|
||||
|
||||
expectedButtons.multiple.forEach((expectedBtn, index) => {
|
||||
expect(actionCard.btns[index].title).toBe(expectedBtn.title);
|
||||
expect(actionCard.btns[index].actionURL).toBe(expectedBtn.actionURL);
|
||||
});
|
||||
|
||||
expect(actionCard.singleTitle).toBeUndefined();
|
||||
expect(actionCard.singleURL).toBeUndefined();
|
||||
}
|
||||
|
||||
if (expectedButtons.orientation !== undefined) {
|
||||
expect(actionCard.btnOrientation).toBe(expectedButtons.orientation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 FeedCard 链接配置
|
||||
*
|
||||
* @param {object} feedCard - FeedCard 对象
|
||||
* @param {Array} expectedLinks - 期望的链接配置
|
||||
*/
|
||||
export function assertFeedCardLinks(feedCard, expectedLinks) {
|
||||
expect(feedCard.links).toBeInstanceOf(Array);
|
||||
expect(feedCard.links).toHaveLength(expectedLinks.length);
|
||||
|
||||
expectedLinks.forEach((expectedLink, index) => {
|
||||
const link = feedCard.links[index];
|
||||
expect(link.title).toBe(expectedLink.title);
|
||||
expect(link.messageURL).toBe(expectedLink.messageURL);
|
||||
|
||||
if (expectedLink.picURL) {
|
||||
expect(link.picURL).toBe(expectedLink.picURL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置对象结构
|
||||
*
|
||||
* @param {object} config - 配置对象
|
||||
* @param {Array} requiredFields - 必需字段列表
|
||||
*/
|
||||
export function assertConfigStructure(config, requiredFields = []) {
|
||||
expect(config).toBeDefined();
|
||||
expect(config).toBeTypeOf('object');
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
expect(config).toHaveProperty(field);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证错误对象
|
||||
*
|
||||
* @param {Error} error - 错误对象
|
||||
* @param {string} expectedMessage - 期望的错误消息(可选)
|
||||
* @param {string} expectedCode - 期望的错误代码(可选)
|
||||
*/
|
||||
export function assertError(error, expectedMessage, expectedCode) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
|
||||
if (expectedMessage) {
|
||||
expect(error.message).toContain(expectedMessage);
|
||||
}
|
||||
|
||||
if (expectedCode) {
|
||||
expect(error.code).toBe(expectedCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 HTTP 请求选项
|
||||
*
|
||||
* @param {object} options - 请求选项
|
||||
* @param {object} expected - 期望的选项
|
||||
*/
|
||||
export function assertHttpOptions(options, expected) {
|
||||
expect(options).toBeDefined();
|
||||
expect(options).toBeTypeOf('object');
|
||||
|
||||
if (expected.hostname) {
|
||||
expect(options.hostname).toBe(expected.hostname);
|
||||
}
|
||||
|
||||
if (expected.path) {
|
||||
expect(options.path).toBe(expected.path);
|
||||
}
|
||||
|
||||
if (expected.method) {
|
||||
expect(options.method).toBe(expected.method);
|
||||
}
|
||||
|
||||
if (expected.headers) {
|
||||
expect(options.headers).toEqual(expect.objectContaining(expected.headers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 URL 格式
|
||||
*
|
||||
* @param {string} url - URL 字符串
|
||||
*/
|
||||
export function assertValidUrl(url) {
|
||||
expect(url).toBeTypeOf('string');
|
||||
expect(url.length).toBeGreaterThan(0);
|
||||
expect(() => new URL(url)).not.toThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JSON 字符串
|
||||
*
|
||||
* @param {string} jsonString - JSON 字符串
|
||||
* @returns {object} 解析后的对象
|
||||
*/
|
||||
export function assertValidJson(jsonString) {
|
||||
expect(jsonString).toBeTypeOf('string');
|
||||
|
||||
let parsed;
|
||||
expect(() => {
|
||||
parsed = JSON.parse(jsonString);
|
||||
}).not.toThrow();
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数组包含特定元素
|
||||
*
|
||||
* @param {Array} array - 数组
|
||||
* @param {*} element - 要查找的元素
|
||||
*/
|
||||
export function assertArrayContains(array, element) {
|
||||
expect(array).toBeInstanceOf(Array);
|
||||
expect(array).toContain(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象深度相等
|
||||
*
|
||||
* @param {object} actual - 实际对象
|
||||
* @param {object} expected - 期望对象
|
||||
*/
|
||||
export function assertDeepEqual(actual, expected) {
|
||||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证函数抛出特定错误
|
||||
*
|
||||
* @param {Function} fn - 要测试的函数
|
||||
* @param {string|RegExp} expectedError - 期望的错误消息或正则表达式
|
||||
*/
|
||||
export function assertThrows(fn, expectedError) {
|
||||
if (typeof expectedError === 'string') {
|
||||
expect(fn).toThrow(expectedError);
|
||||
} else if (expectedError instanceof RegExp) {
|
||||
expect(fn).toThrow(expectedError);
|
||||
} else {
|
||||
expect(fn).toThrow();
|
||||
}
|
||||
}
|
||||
111
tests/helpers/env-mock.js
Normal file
111
tests/helpers/env-mock.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 环境变量 Mock 工具
|
||||
* 用于在测试中模拟和管理环境变量
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mock 环境变量
|
||||
*
|
||||
* @param {object} envVars - 要设置的环境变量
|
||||
* @returns {Function} 恢复函数
|
||||
*/
|
||||
export function mockEnvVars(envVars) {
|
||||
const originalEnv = {};
|
||||
|
||||
// 保存原始值
|
||||
for (const key in envVars) {
|
||||
originalEnv[key] = process.env[key];
|
||||
process.env[key] = envVars[key];
|
||||
}
|
||||
|
||||
// 返回恢复函数
|
||||
return () => {
|
||||
for (const key in envVars) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = originalEnv[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准的 Action 环境变量
|
||||
*
|
||||
* @param {object} overrides - 覆盖的环境变量
|
||||
* @returns {object} 环境变量对象
|
||||
*/
|
||||
export function createActionEnv(overrides = {}) {
|
||||
return {
|
||||
INPUT_WEBHOOK_URL: 'https://oapi.dingtalk.com/robot/send?access_token=test123',
|
||||
INPUT_MESSAGE_TYPE: 'text',
|
||||
INPUT_CONTENT: 'Test message',
|
||||
INPUT_TITLE: '',
|
||||
INPUT_AT_MOBILES: '',
|
||||
INPUT_AT_ALL: 'false',
|
||||
INPUT_LINK_URL: '',
|
||||
INPUT_PIC_URL: '',
|
||||
INPUT_BTN_ORIENTATION: '0',
|
||||
INPUT_SINGLE_TITLE: '',
|
||||
INPUT_SINGLE_URL: '',
|
||||
INPUT_BUTTONS: '',
|
||||
INPUT_FEED_LINKS: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有 INPUT_ 开头的环境变量
|
||||
*
|
||||
* @returns {Function} 恢复函数
|
||||
*/
|
||||
export function clearActionEnv() {
|
||||
const originalEnv = {};
|
||||
const inputKeys = Object.keys(process.env).filter(key => key.startsWith('INPUT_'));
|
||||
|
||||
inputKeys.forEach(key => {
|
||||
originalEnv[key] = process.env[key];
|
||||
delete process.env[key];
|
||||
});
|
||||
|
||||
return () => {
|
||||
Object.assign(process.env, originalEnv);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用的环境变量快照
|
||||
*
|
||||
* @returns {object} 当前环境变量的副本
|
||||
*/
|
||||
export function createEnvSnapshot() {
|
||||
return { ...process.env };
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复环境变量到指定快照
|
||||
*
|
||||
* @param {object} snapshot - 环境变量快照
|
||||
*/
|
||||
export function restoreEnvSnapshot(snapshot) {
|
||||
// 清除当前环境变量
|
||||
for (const key in process.env) {
|
||||
if (!(key in snapshot)) {
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复快照中的环境变量
|
||||
Object.assign(process.env, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建环境变量 Mock(别名函数)
|
||||
*
|
||||
* @param {object} envVars - 要设置的环境变量
|
||||
* @returns {Function} 恢复函数
|
||||
*/
|
||||
export function createEnvMock(envVars) {
|
||||
return mockEnvVars(envVars);
|
||||
}
|
||||
267
tests/helpers/http-mock.js
Normal file
267
tests/helpers/http-mock.js
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user