496 lines
15 KiB
JavaScript
496 lines
15 KiB
JavaScript
/**
|
|
* index.js 集成测试
|
|
* 测试主入口文件的完整执行流程
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { main } from '../../src/index.js';
|
|
import { invalidConfigs } from '../fixtures/configs/invalid-configs.js';
|
|
import { validConfigs } from '../fixtures/configs/valid-configs.js';
|
|
import { dingTalkResponses } from '../fixtures/responses/dingtalk-responses.js';
|
|
import {
|
|
clearActionEnv,
|
|
createEnvMock,
|
|
createEnvSnapshot,
|
|
restoreEnvSnapshot,
|
|
} from '../helpers/env-mock.js';
|
|
|
|
// Mock fetch API
|
|
const mockFetch = vi.fn();
|
|
vi.stubGlobal('fetch', mockFetch);
|
|
|
|
describe('index.js 集成测试', () => {
|
|
let envSnapshot;
|
|
let consoleSpy;
|
|
|
|
/**
|
|
* 设置 HTTP Mock 的辅助函数
|
|
*
|
|
* @param {object} mockResponse - 模拟的 HTTP 响应,格式: { statusCode, data }
|
|
*/
|
|
async function setupHttpMock(mockResponse) {
|
|
const { statusCode = 200, data = {} } = mockResponse;
|
|
const responseText = typeof data === 'string' ? data : JSON.stringify(data);
|
|
|
|
mockFetch.mockResolvedValueOnce({
|
|
status: statusCode,
|
|
text: async () => responseText,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 设置网络错误 Mock 的辅助函数
|
|
*
|
|
* @param {string} errorMessage - 错误消息
|
|
*/
|
|
async function setupNetworkErrorMock(errorMessage = 'Network error') {
|
|
mockFetch.mockRejectedValueOnce(new Error(errorMessage));
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
// 保存环境变量快照
|
|
envSnapshot = createEnvSnapshot();
|
|
|
|
// 清理 Action 环境变量
|
|
clearActionEnv();
|
|
|
|
// Mock console 方法
|
|
consoleSpy = {
|
|
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
|
|
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
|
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
|
debug: vi.spyOn(console, 'debug').mockImplementation(() => {}),
|
|
};
|
|
|
|
// Mock process.exit
|
|
vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
|
|
// Mock Date.now for consistent timing
|
|
vi.spyOn(Date, 'now').mockReturnValue(1640995200000); // 2022-01-01 00:00:00
|
|
});
|
|
|
|
afterEach(() => {
|
|
// 恢复环境变量
|
|
restoreEnvSnapshot(envSnapshot);
|
|
|
|
// 清理 fetch mock
|
|
mockFetch.mockClear();
|
|
|
|
// 恢复所有 mocks
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('成功场景测试', () => {
|
|
it('应该成功发送文本消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.withMessageId });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.messageId).toBe('msg789012');
|
|
expect(result.details).toMatchObject({
|
|
duration: expect.any(Number),
|
|
statusCode: 200,
|
|
messageSummary: expect.stringContaining('text'),
|
|
});
|
|
|
|
// 验证日志输出
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('🚀 DingTalk Gitea Action started'),
|
|
);
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('✅ Message sent successfully!'),
|
|
);
|
|
|
|
// 验证 fetch 被调用
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该成功发送 Markdown 消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.markdown);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.details.messageSummary).toContain('markdown');
|
|
|
|
// 验证 fetch 被调用
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该成功发送 Link 消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.link);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.details.messageSummary).toContain('link');
|
|
|
|
// 验证 fetch 被调用
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该成功发送 ActionCard 消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.actionCard);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.details.messageSummary).toContain('actionCard');
|
|
|
|
// 验证 fetch 被调用
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该成功发送 FeedCard 消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.feedCard);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.details.messageSummary).toContain('feedCard');
|
|
|
|
// 验证 fetch 被调用
|
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该在调试模式下输出详细信息', async () => {
|
|
// 设置环境变量(包含调试模式)
|
|
createEnvMock({
|
|
...validConfigs.envVars.text,
|
|
DEBUG: 'true',
|
|
});
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
|
|
// 验证调试日志
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('[INFO] Debug mode: true'),
|
|
);
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('[DEBUG] Message payload:'),
|
|
);
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(
|
|
expect.stringContaining('[DEBUG] Full response:'),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('失败场景测试', () => {
|
|
it('应该处理配置解析错误', async () => {
|
|
// 设置无效的环境变量
|
|
createEnvMock(invalidConfigs.envVars.missingWebhook);
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Action execution failed');
|
|
expect(result.details.errorType).toBe('Error');
|
|
|
|
// 验证错误日志
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Action execution failed'),
|
|
);
|
|
});
|
|
|
|
it('应该处理消息验证错误', async () => {
|
|
// 设置会导致消息验证失败的环境变量
|
|
createEnvMock({
|
|
INPUT_WEBHOOK_URL: 'https://oapi.dingtalk.com/robot/send?access_token=test',
|
|
INPUT_MESSAGE_TYPE: 'text',
|
|
INPUT_CONTENT: '', // 空内容会导致验证失败
|
|
});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Action execution failed');
|
|
});
|
|
|
|
it('应该处理 DingTalk API 错误', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求返回错误
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.error.invalidToken });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Failed to send message');
|
|
expect(result.details.errcode).toBe(310000);
|
|
expect(result.details.errmsg).toBe('keywords not in content');
|
|
|
|
// 验证错误日志
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to send message'),
|
|
);
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(
|
|
expect.stringContaining('DingTalk error code: 310000'),
|
|
);
|
|
});
|
|
|
|
it('应该处理网络错误', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock 网络错误
|
|
await setupNetworkErrorMock('ECONNREFUSED');
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.details.networkError).toContain('ECONNREFUSED');
|
|
|
|
// 验证错误日志
|
|
expect(consoleSpy.error).toHaveBeenCalledWith(expect.stringContaining('Network error'));
|
|
});
|
|
|
|
it('应该处理超时错误', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock 超时错误
|
|
await setupNetworkErrorMock('ETIMEDOUT');
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.details.networkError).toContain('ETIMEDOUT');
|
|
});
|
|
});
|
|
|
|
describe('边界条件测试', () => {
|
|
it('应该处理响应验证失败但仍然成功的情况', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock 返回格式不完整但成功的响应(缺少 errmsg 字段)
|
|
await setupHttpMock({
|
|
statusCode: 200,
|
|
data: {
|
|
errcode: 0,
|
|
// 缺少 errmsg 字段,这会触发验证失败
|
|
},
|
|
});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.messageId).toBeUndefined();
|
|
|
|
// 验证警告日志
|
|
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('Response validation failed'),
|
|
);
|
|
});
|
|
|
|
it('应该处理空的错误消息', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock 返回空错误消息的失败响应
|
|
await setupHttpMock({
|
|
statusCode: 400,
|
|
data: {
|
|
errcode: 300001,
|
|
errmsg: '',
|
|
},
|
|
});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Failed to send message');
|
|
});
|
|
|
|
it('应该正确计算执行时间', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// Mock Date.now 返回递增的时间
|
|
let callCount = 0;
|
|
vi.spyOn(Date, 'now').mockImplementation(() => {
|
|
callCount++;
|
|
return 1640995200000 + callCount * 1000; // 每次调用增加1秒
|
|
});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(result.details.duration).toBeGreaterThan(0);
|
|
|
|
// 验证执行时间日志
|
|
expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining('Total execution time:'));
|
|
});
|
|
});
|
|
|
|
describe('Action 输出测试', () => {
|
|
it('应该在成功时设置正确的 Action 输出', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.withMessageId });
|
|
|
|
// Mock GitHub Actions 输出
|
|
const outputSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
|
|
// 验证 Action 输出
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=success::true');
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=message_id::msg789012');
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=error_message::');
|
|
});
|
|
|
|
it('应该在失败时设置正确的 Action 输出', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求返回错误
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.error.invalidToken });
|
|
|
|
// Mock GitHub Actions 输出
|
|
const outputSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
|
|
// 验证 Action 输出
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=success::false');
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=message_id::');
|
|
expect(outputSpy).toHaveBeenCalledWith(
|
|
expect.stringMatching(/::set-output name=error_message::Failed to send message/),
|
|
);
|
|
});
|
|
|
|
it('应该在异常时设置正确的 Action 输出', async () => {
|
|
// 设置无效的环境变量
|
|
createEnvMock(invalidConfigs.envVars.missingWebhook);
|
|
|
|
// Mock GitHub Actions 输出 (使用 console.log 而不是 process.stdout.write)
|
|
const outputSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(false);
|
|
|
|
// 验证 Action 输出
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=success::false');
|
|
expect(outputSpy).toHaveBeenCalledWith('::set-output name=message_id::');
|
|
expect(outputSpy).toHaveBeenCalledWith(
|
|
expect.stringMatching(/::set-output name=error_message::Action execution failed/),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('性能测试', () => {
|
|
it('应该在合理时间内完成执行', async () => {
|
|
// 设置环境变量
|
|
createEnvMock(validConfigs.envVars.text);
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 记录开始时间
|
|
const startTime = Date.now();
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 计算执行时间
|
|
const executionTime = Date.now() - startTime;
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
expect(executionTime).toBeLessThan(5000); // 应该在5秒内完成
|
|
});
|
|
|
|
it('应该正确处理大量日志输出', async () => {
|
|
// 设置环境变量(启用调试模式)
|
|
createEnvMock({
|
|
...validConfigs.envVars.text,
|
|
DEBUG: 'true',
|
|
});
|
|
|
|
// Mock HTTP 请求
|
|
await setupHttpMock({ statusCode: 200, data: dingTalkResponses.success.standard });
|
|
|
|
// 执行主函数
|
|
const result = await main();
|
|
|
|
// 验证结果
|
|
expect(result.success).toBe(true);
|
|
|
|
// 验证日志调用次数
|
|
expect(consoleSpy.log.mock.calls.length).toBeGreaterThan(10);
|
|
});
|
|
});
|
|
});
|