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