/** * utils.js 文件单元测试 */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as utils from '../../src/utils.js'; import { assertError } from '../helpers/assertions.js'; import { mockEnvVars } from '../helpers/env-mock.js'; describe('测试 utils.js 文件', () => { let restoreEnv; let consoleSpy; beforeEach(() => { // Mock console methods consoleSpy = { log: vi.spyOn(console, 'log').mockImplementation(() => {}), warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), error: vi.spyOn(console, 'error').mockImplementation(() => {}), }; }); afterEach(() => { // Restore console methods Object.values(consoleSpy).forEach(spy => spy.mockRestore()); // Restore environment variables if (restoreEnv) { restoreEnv(); restoreEnv = null; } }); describe('测试 LOG_LEVELS 常量', () => { it('应该导出正确的日志级别', () => { expect(utils.LOG_LEVELS).toEqual({ DEBUG: 'DEBUG', INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR', }); }); }); describe('测试 getTimestamp 函数', () => { it('应该返回 ISO 字符串时间戳', () => { const timestamp = utils.getTimestamp(); expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); expect(() => new Date(timestamp)).not.toThrow(); }); it('应该为连续调用返回不同的时间戳', async () => { const timestamp1 = utils.getTimestamp(); // Add a small delay to ensure different timestamps await new Promise(resolve => setTimeout(resolve, 10)); const timestamp2 = utils.getTimestamp(); expect(timestamp1).not.toBe(timestamp2); }); }); describe('测试 formatLogMessage 函数', () => { it('应该使用级别和消息格式化日志消息', () => { const message = utils.formatLogMessage('INFO', 'Test message'); expect(message).toMatch( /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[INFO\] Test message$/, ); }); it('应该在提供元数据时包含它', () => { const meta = { key: 'value', number: 123 }; const message = utils.formatLogMessage('DEBUG', 'Test message', meta); expect(message).toContain('Test message | {"key":"value","number":123}'); }); it('应该处理 null 元数据', () => { const message = utils.formatLogMessage('WARN', 'Test message', null); expect(message).not.toContain('|'); expect(message).toContain('Test message'); }); it('应该处理 undefined 元数据', () => { const message = utils.formatLogMessage('ERROR', 'Test message'); expect(message).not.toContain('|'); expect(message).toContain('Test message'); }); }); describe('测试 logDebug 函数', () => { it('应该在 DEBUG=true 时记录调试消息', () => { restoreEnv = mockEnvVars({ DEBUG: 'true' }); utils.logDebug('Debug message', { test: true }); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining('[DEBUG] Debug message | {"test":true}'), ); }); it('应该在 NODE_ENV=development 时记录调试消息', () => { restoreEnv = mockEnvVars({ NODE_ENV: 'development' }); utils.logDebug('Debug message'); expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining('[DEBUG] Debug message')); }); it('不应该在生产环境中记录调试消息', () => { restoreEnv = mockEnvVars({ DEBUG: 'false', NODE_ENV: 'production' }); utils.logDebug('Debug message'); expect(consoleSpy.log).not.toHaveBeenCalled(); }); }); describe('测试 logInfo 函数', () => { it('应该始终记录信息消息', () => { utils.logInfo('Info message', { data: 'test' }); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining('[INFO] Info message | {"data":"test"}'), ); }); }); describe('测试 logWarn 函数', () => { it('应该始终记录警告消息', () => { utils.logWarn('Warning message'); expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining('[WARN] Warning message'), ); }); }); describe('测试 logError 函数', () => { it('应该始终记录错误消息', () => { utils.logError('Error message', { error: 'details' }); expect(consoleSpy.error).toHaveBeenCalledWith( expect.stringContaining('[ERROR] Error message | {"error":"details"}'), ); }); }); describe('测试 safeJsonParse 函数', () => { it('应该解析有效的 JSON 字符串', () => { const obj = { key: 'value', number: 123 }; const jsonString = JSON.stringify(obj); const result = utils.safeJsonParse(jsonString); expect(result).toEqual(obj); }); it('应该为无效的 JSON 返回默认值', () => { const result = utils.safeJsonParse('invalid json', { default: true }); expect(result).toEqual({ default: true }); expect(consoleSpy.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to parse JSON')); }); it('应该在未提供默认值时返回 null', () => { const result = utils.safeJsonParse('invalid json'); expect(result).toBeNull(); }); it('应该处理空字符串', () => { const result = utils.safeJsonParse('', 'default'); expect(result).toBe('default'); }); }); describe('测试 safeJsonStringify 函数', () => { it('应该序列化有效对象', () => { const obj = { key: 'value', number: 123 }; const result = utils.safeJsonStringify(obj); const parsed = JSON.parse(result); expect(parsed).toEqual(obj); }); it('应该为循环引用返回默认值', () => { const obj = {}; obj.circular = obj; const result = utils.safeJsonStringify(obj, '{"error":"circular"}'); expect(result).toBe('{"error":"circular"}'); expect(consoleSpy.warn).toHaveBeenCalledWith( expect.stringContaining('Failed to stringify object'), ); }); it('应该在未提供默认值时返回空对象', () => { const obj = {}; obj.circular = obj; const result = utils.safeJsonStringify(obj); expect(result).toBe('{}'); }); }); describe('测试 isEmpty 函数', () => { it('应该对空字符串返回 true', () => { expect(utils.isEmpty('')).toBe(true); }); it('应该对仅包含空白的字符串返回 true', () => { expect(utils.isEmpty(' ')).toBe(true); expect(utils.isEmpty('\t\n')).toBe(true); }); it('应该对 null 返回 true', () => { expect(utils.isEmpty(null)).toBe(true); }); it('应该对 undefined 返回 true', () => { expect(utils.isEmpty(undefined)).toBe(true); }); it('应该对非字符串值返回 true', () => { expect(utils.isEmpty(123)).toBe(true); expect(utils.isEmpty({})).toBe(true); expect(utils.isEmpty([])).toBe(true); }); it('应该对非空字符串返回 false', () => { expect(utils.isEmpty('hello')).toBe(false); expect(utils.isEmpty(' hello ')).toBe(false); }); }); describe('测试 truncateString 函数', () => { it('如果字符串短于最大长度应该返回原字符串', () => { const str = 'hello'; const result = utils.truncateString(str, 10); expect(result).toBe(str); }); it('应该使用默认后缀截断字符串', () => { const str = 'hello world'; const result = utils.truncateString(str, 8); expect(result).toBe('hello...'); expect(result.length).toBe(8); }); it('应该使用自定义后缀截断字符串', () => { const str = 'hello world'; const result = utils.truncateString(str, 10, ' [more]'); expect(result).toBe('hel [more]'); expect(result.length).toBe(10); }); it('应该处理 null 输入', () => { expect(utils.truncateString(null, 10)).toBe(''); }); it('应该处理非字符串输入', () => { expect(utils.truncateString(123, 10)).toBe(''); }); it('应该处理最大长度等于后缀长度的边界情况', () => { const result = utils.truncateString('hello', 3); expect(result).toBe('...'); }); }); describe('测试 cleanMobile 函数', () => { it('应该清理并验证有效的手机号码', () => { expect(utils.cleanMobile('138-0013-8000')).toBe('13800138000'); expect(utils.cleanMobile('139 0013 9000')).toBe('13900139000'); expect(utils.cleanMobile('(150) 1234-5678')).toBe('15012345678'); }); it('应该对无效的手机号码返回 null', () => { expect(utils.cleanMobile('12345678901')).toBeNull(); // 不是1开头 expect(utils.cleanMobile('1234567890')).toBeNull(); // 位数不够 expect(utils.cleanMobile('123456789012')).toBeNull(); // 位数太多 expect(utils.cleanMobile('12012345678')).toBeNull(); // 第二位不是3-9 }); it('应该对非字符串输入返回 null', () => { expect(utils.cleanMobile(null)).toBeNull(); expect(utils.cleanMobile(undefined)).toBeNull(); expect(utils.cleanMobile(123)).toBeNull(); }); it('应该对空字符串返回 null', () => { expect(utils.cleanMobile('')).toBeNull(); }); it('应该处理所有有效的手机号前缀', () => { const validPrefixes = ['13', '14', '15', '16', '17', '18', '19']; validPrefixes.forEach(prefix => { const mobile = `${prefix}012345678`; expect(utils.cleanMobile(mobile)).toBe(mobile); }); }); }); describe('测试 isValidUrl 函数', () => { it('应该对有效的 URL 返回 true', () => { expect(utils.isValidUrl('https://example.com')).toBe(true); expect(utils.isValidUrl('http://example.com')).toBe(true); expect(utils.isValidUrl('ftp://example.com')).toBe(true); expect(utils.isValidUrl('https://example.com/path?query=value')).toBe(true); }); it('应该对无效的 URL 返回 false', () => { expect(utils.isValidUrl('not-a-url')).toBe(false); expect(utils.isValidUrl('http://')).toBe(false); expect(utils.isValidUrl('')).toBe(false); }); it('应该对非字符串输入返回 false', () => { expect(utils.isValidUrl(null)).toBe(false); expect(utils.isValidUrl(undefined)).toBe(false); expect(utils.isValidUrl(123)).toBe(false); }); }); describe('测试 isValidHttpUrl 函数', () => { it('应该对有效的 HTTP/HTTPS URL 返回 true', () => { expect(utils.isValidHttpUrl('https://example.com')).toBe(true); expect(utils.isValidHttpUrl('http://example.com')).toBe(true); }); it('应该对非 HTTP URL 返回 false', () => { expect(utils.isValidHttpUrl('ftp://example.com')).toBe(false); expect(utils.isValidHttpUrl('file:///path/to/file')).toBe(false); }); it('应该对无效的 URL 返回 false', () => { expect(utils.isValidHttpUrl('not-a-url')).toBe(false); expect(utils.isValidHttpUrl('')).toBe(false); }); }); describe('测试 createError 函数', () => { it('应该只使用消息创建错误', () => { const error = utils.createError('Test error'); expect(error).toBeInstanceOf(Error); expect(error.message).toBe('Test error'); expect(error.code).toBeUndefined(); expect(error.details).toBeUndefined(); }); it('应该使用消息和代码创建错误', () => { const error = utils.createError('Test error', 'TEST_CODE'); expect(error.message).toBe('Test error'); expect(error.code).toBe('TEST_CODE'); }); it('应该使用消息、代码和详情创建错误', () => { const details = { key: 'value' }; const error = utils.createError('Test error', 'TEST_CODE', details); expect(error.message).toBe('Test error'); expect(error.code).toBe('TEST_CODE'); expect(error.details).toBe(details); }); }); describe('测试 setActionOutput 函数', () => { it('应该在未设置 GITHUB_OUTPUT 时使用旧格式', () => { restoreEnv = mockEnvVars({ GITHUB_OUTPUT: '' }); utils.setActionOutput('test_name', 'test_value'); expect(consoleSpy.log).toHaveBeenCalledWith('::set-output name=test_name::test_value'); }); it('应该在设置 GITHUB_OUTPUT 时使用新的 GitHub Actions 格式', () => { // Create a temporary file path for testing const tempFile = '/tmp/test_github_output'; restoreEnv = mockEnvVars({ GITHUB_OUTPUT: tempFile }); // Since we can't easily mock require('fs') in this context, // we'll test that the function doesn't throw and doesn't log expect(() => { utils.setActionOutput('test_name', 'test_value'); }).not.toThrow(); // Verify that console.log was not called (which would indicate legacy format) expect(consoleSpy.log).not.toHaveBeenCalledWith('::set-output name=test_name::test_value'); }); }); describe('测试 setActionFailed 函数', () => { it('应该记录错误并退出进程', () => { const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {}); utils.setActionFailed('Test failure'); expect(consoleSpy.log).toHaveBeenCalledWith('::error::Test failure'); expect(mockExit).toHaveBeenCalledWith(1); mockExit.mockRestore(); }); }); describe('测试 getEnv 函数', () => { it('应该返回环境变量值', () => { restoreEnv = mockEnvVars({ TEST_VAR: 'test_value' }); const result = utils.getEnv('TEST_VAR'); expect(result).toBe('test_value'); }); it('应该在未设置环境变量时返回默认值', () => { const result = utils.getEnv('NON_EXISTENT_VAR', 'default_value'); expect(result).toBe('default_value'); }); it('应该在未提供默认值时返回空字符串', () => { const result = utils.getEnv('NON_EXISTENT_VAR'); expect(result).toBe(''); }); }); describe('测试 isDebugMode 函数', () => { it('应该在 DEBUG=true 时返回 true', () => { restoreEnv = mockEnvVars({ DEBUG: 'true' }); expect(utils.isDebugMode()).toBe(true); }); it('应该在 NODE_ENV=development 时返回 true', () => { restoreEnv = mockEnvVars({ NODE_ENV: 'development' }); expect(utils.isDebugMode()).toBe(true); }); it('应该在生产模式中返回 false', () => { restoreEnv = mockEnvVars({ DEBUG: 'false', NODE_ENV: 'production' }); expect(utils.isDebugMode()).toBe(false); }); it('应该处理不区分大小写的 DEBUG 值', () => { restoreEnv = mockEnvVars({ DEBUG: 'TRUE' }); expect(utils.isDebugMode()).toBe(true); }); }); describe('测试 formatDuration 函数', () => { it('应该格式化毫秒', () => { expect(utils.formatDuration(500)).toBe('500ms'); expect(utils.formatDuration(999)).toBe('999ms'); }); it('应该格式化秒', () => { expect(utils.formatDuration(1000)).toBe('1.00s'); expect(utils.formatDuration(1500)).toBe('1.50s'); expect(utils.formatDuration(2345)).toBe('2.35s'); }); it('应该处理零持续时间', () => { expect(utils.formatDuration(0)).toBe('0ms'); }); }); describe('测试 createRetryConfig 函数', () => { it('应该创建默认重试配置', () => { const config = utils.createRetryConfig(); expect(config.maxRetries).toBe(3); expect(config.delay).toBe(1000); expect(config.backoffMultiplier).toBe(1.5); expect(typeof config.getDelay).toBe('function'); }); it('应该创建自定义重试配置', () => { const config = utils.createRetryConfig(5, 2000, 2); expect(config.maxRetries).toBe(5); expect(config.delay).toBe(2000); expect(config.backoffMultiplier).toBe(2); }); it('应该为尝试次数计算正确的延迟', () => { const config = utils.createRetryConfig(3, 1000, 2); expect(config.getDelay(0)).toBe(1000); // 1000 * 2^0 expect(config.getDelay(1)).toBe(2000); // 1000 * 2^1 expect(config.getDelay(2)).toBe(4000); // 1000 * 2^2 }); }); describe('测试 maskSensitiveInfo 函数', () => { it('应该在文本中屏蔽敏感信息', () => { const result = utils.maskSensitiveInfo('1234567890', 2, 2); expect(result).toBe('12******90'); }); it('应该使用自定义屏蔽字符', () => { const result = utils.maskSensitiveInfo('1234567890', 2, 2, 'X'); expect(result).toBe('12XXXXXX90'); }); it('如果字符串太短应该屏蔽整个字符串', () => { const result = utils.maskSensitiveInfo('123', 2, 2); expect(result).toBe('***'); }); it('应该处理非字符串输入', () => { expect(utils.maskSensitiveInfo(null)).toBe(''); expect(utils.maskSensitiveInfo(123)).toBe(''); }); it('应该处理空字符串', () => { expect(utils.maskSensitiveInfo('')).toBe(''); }); it('应该使用默认参数', () => { const result = utils.maskSensitiveInfo('1234567890123456'); expect(result).toBe('1234********3456'); }); }); describe('测试 validateRequiredEnvVars 函数', () => { it('应该在所有必需变量都存在时不抛出异常', () => { restoreEnv = mockEnvVars({ VAR1: 'value1', VAR2: 'value2', VAR3: 'value3', }); expect(() => { utils.validateRequiredEnvVars(['VAR1', 'VAR2', 'VAR3']); }).not.toThrow(); }); it('应该在缺少必需变量时抛出异常', () => { restoreEnv = mockEnvVars({ VAR1: 'value1' }); expect(() => { utils.validateRequiredEnvVars(['VAR1', 'VAR2', 'VAR3']); }).toThrow('Missing required environment variables: VAR2, VAR3'); }); it('应该抛出带有正确代码和详情的错误', () => { try { utils.validateRequiredEnvVars(['MISSING_VAR']); } catch (error) { assertError(error, 'Missing required environment variables', 'MISSING_ENV_VARS'); expect(error.details.missing).toEqual(['MISSING_VAR']); } }); it('应该处理空数组', () => { expect(() => { utils.validateRequiredEnvVars([]); }).not.toThrow(); }); }); });