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