Files
dingtalk-bot/tests/integration/index.test.js

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);
});
});
});