Files
files-hash/tests/index.test.js

1022 lines
33 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import path from 'path';
import fs from 'fs';
import {
FilesHashAction,
FileDiscovery,
HashCalculator,
OutputFormatter,
ActionError,
ErrorType,
ActionInputs,
ActionOutputs,
} from '../index.js';
import { createTestFile, createTestDir, cleanupTestFiles, cleanupTestDirs } from './utils.js';
describe('测试 FileDiscovery 类', () => {
describe('测试 FileDiscovery.parseFilePatterns 方法', () => {
it('应该解析包含多个模式的字符串输入', () => {
const discovery = new FileDiscovery();
const patterns = discovery.parseFilePatterns('file1.txt\nfile2.txt\n\n file3.txt \n');
expect(patterns).toHaveLength(3);
expect(patterns[0]).toBe('file1.txt');
expect(patterns[2]).toBe('file3.txt');
});
it('应该解析包含空白和空格模式的数组输入', () => {
const discovery = new FileDiscovery();
const patterns = discovery.parseFilePatterns(['file1.txt', '', ' file2.txt ']);
expect(patterns).toHaveLength(2);
expect(patterns).toEqual(['file1.txt', 'file2.txt']);
});
});
describe('测试 FileDiscovery.matchPattern 方法', () => {
const discovery = new FileDiscovery();
it('应该匹配确切的文件名', () => {
expect(discovery.matchPattern('test.js', 'test.js')).toBe(true);
expect(discovery.matchPattern('test.js', 'other.js')).toBe(false);
});
it('应该匹配通配符模式', () => {
expect(discovery.matchPattern('test.js', '*.js')).toBe(true);
expect(discovery.matchPattern('file.txt', 'file.*')).toBe(true);
expect(discovery.matchPattern('test.js', '*.txt')).toBe(false);
});
it('应该匹配问号通配符', () => {
expect(discovery.matchPattern('test1.js', 'test?.js')).toBe(true);
expect(discovery.matchPattern('test12.js', 'test?.js')).toBe(false);
});
});
describe('测试 FileDiscovery.findFiles 方法', () => {
const testFiles = ['test-dir/file1.js', 'test-dir/file2.txt', 'test-dir/subdir/file3.js'];
beforeEach(async () => {
// 创建测试文件
for (const file of testFiles) {
await createTestFile(file, 'test content');
}
});
afterEach(async () => {
await cleanupTestFiles(testFiles);
await cleanupTestDirs(['test-dir/subdir', 'test-dir']);
});
it('应该找到确切的文件', async () => {
const discovery = new FileDiscovery();
const files = await discovery.findFiles(['test-dir/file1.js']);
expect(files).toHaveLength(1);
expect(files[0]).toBe('test-dir/file1.js');
});
it('应该使用通配符模式查找文件', async () => {
const discovery = new FileDiscovery();
const files = await discovery.findFiles(['test-dir/*.js']);
expect(files).toHaveLength(1);
});
it('应该递归查找文件', async () => {
const discovery = new FileDiscovery();
const files = await discovery.findFiles(['test-dir/**/*.js']);
expect(files).toHaveLength(2);
});
it('应该处理空目录', async () => {
const tempDir = await createTestDir();
const discovery = new FileDiscovery();
const files = await discovery.findFiles([tempDir]);
expect(files).toEqual([]);
cleanupTestDirs(tempDir);
});
it('应该处理无效模式', async () => {
const discovery = new FileDiscovery();
const files = await discovery.findFiles(['nonexistent/**/*.txt']);
expect(files).toEqual([]);
});
it('应该处理混合有效和无效模式', async () => {
const tempDir = await createTestDir();
const testFile = path.join(tempDir, 'test.txt');
await createTestFile(testFile, 'content');
const discovery = new FileDiscovery();
const files = await discovery.findFiles([path.join(tempDir, '*.txt'), 'nonexistent/**/*.txt']);
expect(files).toContain(testFile);
cleanupTestFiles([testFile]);
cleanupTestDirs(tempDir);
});
it('应该处理无权限的目录', async () => {
const tempDir = await createTestDir();
const restrictedDir = path.join(tempDir, 'restricted');
const discovery = new FileDiscovery();
try {
fs.mkdirSync(restrictedDir, { mode: 0o000 });
const files = await discovery.findFiles([restrictedDir + '/*.txt']);
expect(files).toEqual([]);
} catch (error) {
// 某些系统可能不允许创建无权限目录,这是预期的
expect(error.code).toBe('EACCES');
} finally {
// 清理:先恢复权限再删除
try {
fs.chmodSync(restrictedDir, 0o755);
cleanupTestDirs(tempDir);
} catch (cleanupError) {
// 忽略清理错误
}
}
});
it('应该忽略隐藏文件(默认)', async () => {
const tempDir = await createTestDir();
const hiddenFile = path.join(tempDir, '.hidden.txt');
const visibleFile = path.join(tempDir, 'visible.txt');
const discovery = new FileDiscovery();
await createTestFile(hiddenFile, 'hidden content');
await createTestFile(visibleFile, 'visible content');
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]);
expect(files).toContain(visibleFile);
expect(files).not.toContain(hiddenFile);
cleanupTestFiles(hiddenFile, visibleFile);
cleanupTestDirs(tempDir);
});
it('应该处理空文件', async () => {
const tempDir = await createTestDir();
const emptyFile = path.join(tempDir, 'empty.txt');
const discovery = new FileDiscovery();
await createTestFile(emptyFile, '');
const files = await discovery.findFiles([path.join(tempDir, '*.txt')]);
expect(files).toContain(emptyFile);
cleanupTestFiles(emptyFile);
cleanupTestDirs(tempDir);
});
it('应该处理嵌套很深的目录', async () => {
const tempDir = await createTestDir();
const deepFile = path.join(tempDir, 'level1', 'level2', 'level3', 'deep.txt');
const discovery = new FileDiscovery();
fs.mkdirSync(path.dirname(deepFile), { recursive: true });
await createTestFile(deepFile, 'deep content');
const files = await discovery.findFiles([path.join(tempDir, '**/*.txt')]);
expect(files).toContain(deepFile);
cleanupTestFiles(deepFile);
cleanupTestDirs(tempDir);
});
});
describe('测试 FileDiscovery.validateFiles 方法', () => {
const existingFile = 'existing-file.txt';
const nonExistingFile = 'non-existing-file.txt';
beforeEach(async () => {
await createTestFile(existingFile, 'test content');
});
afterEach(async () => {
await cleanupTestFiles([existingFile]);
});
it('应该验证存在和不存在的文件', async () => {
const discovery = new FileDiscovery();
const results = await discovery.validateFiles([existingFile, nonExistingFile]);
expect(results).toHaveLength(2);
const existingResult = results.find(r => r.file === existingFile);
const nonExistingResult = results.find(r => r.file === nonExistingFile);
expect(existingResult.exists).toBe(true);
expect(existingResult.readable).toBe(true);
expect(existingResult.size).toBeGreaterThan(0);
expect(nonExistingResult.exists).toBe(false);
expect(nonExistingResult.readable).toBe(false);
});
});
});
describe('测试 HashCalculator 类', () => {
describe('测试 HashCalculator.validateAlgorithm 方法', () => {
const calculator = new HashCalculator();
it('应该接受有效的算法', () => {
expect(() => calculator.validateAlgorithm('sha256')).not.toThrow();
expect(() => calculator.validateAlgorithm('MD5')).not.toThrow();
});
it('应该对无效算法抛出错误', () => {
expect(() => calculator.validateAlgorithm('invalid')).toThrow(ActionError);
try {
calculator.validateAlgorithm('invalid');
} catch (error) {
expect(error.type).toBe(ErrorType.INVALID_ALGORITHM);
}
});
it('应该处理算法名称大小写', () => {
// 验证算法验证方法是否大小写不敏感
expect(() => calculator.validateAlgorithm('SHA256')).not.toThrow();
expect(() => calculator.validateAlgorithm('sha256')).not.toThrow();
expect(() => calculator.validateAlgorithm('Sha256')).not.toThrow();
expect(() => calculator.validateAlgorithm('md5')).not.toThrow();
expect(() => calculator.validateAlgorithm('MD5')).not.toThrow();
});
});
describe('测试 HashCalculator.calculateFileHash 方法', () => {
const testFile = 'test-temp-file.txt';
const testContent = 'Hello, World!';
beforeEach(async () => {
await createTestFile(testFile, testContent);
});
afterEach(async () => {
await cleanupTestFiles([testFile]);
});
it('应该正确计算SHA256哈希', async () => {
const calculator = new HashCalculator();
const hash = await calculator.calculateFileHash(testFile, 'sha256');
expect(hash).toHaveLength(64);
expect(hash).toMatch(/^[a-f0-9]+$/);
});
it('应该产生一致的哈希值', async () => {
const calculator = new HashCalculator();
const hash1 = await calculator.calculateFileHash(testFile, 'sha256');
const hash2 = await calculator.calculateFileHash(testFile, 'sha256');
expect(hash1).toBe(hash2);
});
it('应该处理空文件', async () => {
const tempDir = await createTestDir();
const emptyFile = path.join(tempDir, 'empty.txt');
await createTestFile(emptyFile, '');
const calculator = new HashCalculator();
const hash = await calculator.calculateFileHash(emptyFile, 'sha256');
// 空文件的 SHA256 哈希值
expect(hash).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
cleanupTestFiles(emptyFile);
cleanupTestDirs(tempDir);
});
it('应该处理大文件', async () => {
const tempDir = await createTestDir();
const largeFile = path.join(tempDir, 'large.txt');
// 创建 1MB 大小的文件
const largeContent = 'x'.repeat(1024 * 1024);
await createTestFile(largeFile, largeContent);
const calculator = new HashCalculator();
const hash = await calculator.calculateFileHash(largeFile, 'sha256');
expect(hash).toBeTruthy();
expect(hash.length).toBe(64); // SHA256 哈希长度
cleanupTestFiles(largeFile);
cleanupTestDirs(tempDir);
});
it('应该处理不存在的文件', async () => {
const calculator = new HashCalculator();
await expect(async () => {
await calculator.calculateFileHash('/nonexistent/file.txt', 'sha256');
}).rejects.toThrow();
});
});
describe('测试 HashCalculator.calculateCombinedHash 方法', () => {
const testFiles = ['test-file1.txt', 'test-file2.txt'];
beforeEach(async () => {
await createTestFile(testFiles[0], 'Content 1');
await createTestFile(testFiles[1], 'Content 2');
});
afterEach(async () => {
await cleanupTestFiles(testFiles);
});
it('应该计算组合哈希', async () => {
const calculator = new HashCalculator();
const hash = await calculator.calculateCombinedHash(testFiles, 'sha256');
expect(hash).toHaveLength(64);
});
it('应该与顺序无关', async () => {
const calculator = new HashCalculator();
const hash1 = await calculator.calculateCombinedHash(testFiles, 'sha256');
const hash2 = await calculator.calculateCombinedHash([testFiles[1], testFiles[0]], 'sha256');
expect(hash1).toBe(hash2);
});
it('应该处理空文件列表的组合哈希', async () => {
const calculator = new HashCalculator();
const hash = await calculator.calculateCombinedHash([], 'sha256');
// 空列表的组合哈希应该是空字符串的哈希
expect(hash).toBeTruthy();
});
it('应该处理单文件的组合哈希', async () => {
const tempDir = await createTestDir();
const testFile = path.join(tempDir, 'test.txt');
await createTestFile(testFile, 'test content');
const calculator = new HashCalculator();
const hash = await calculator.calculateCombinedHash([testFile], 'sha256');
expect(hash).toBeTruthy();
expect(hash.length).toBe(64);
cleanupTestFiles(testFile);
cleanupTestDirs(tempDir);
});
it('应该处理多文件的组合哈希', async () => {
const tempDir = await createTestDir();
const file1 = path.join(tempDir, 'file1.txt');
const file2 = path.join(tempDir, 'file2.txt');
await createTestFile(file1, 'content1');
await createTestFile(file2, 'content2');
const calculator = new HashCalculator();
const hash = await calculator.calculateCombinedHash([file1, file2], 'sha256');
expect(hash).toBeTruthy();
expect(hash.length).toBe(64);
cleanupTestFiles(file1, file2);
cleanupTestDirs(tempDir);
});
it('应该处理文件顺序无关的组合哈希', async () => {
const tempDir = await createTestDir();
const file1 = path.join(tempDir, 'file1.txt');
const file2 = path.join(tempDir, 'file2.txt');
await createTestFile(file1, 'content1');
await createTestFile(file2, 'content2');
const calculator = new HashCalculator();
const hash1 = await calculator.calculateCombinedHash([file1, file2], 'sha256');
const hash2 = await calculator.calculateCombinedHash([file2, file1], 'sha256');
expect(hash1).toBe(hash2);
cleanupTestFiles(file1, file2);
cleanupTestDirs(tempDir);
});
});
describe('测试 HashCalculator.getSupportedAlgorithms 方法', () => {
it('应该获取支持的算法列表', () => {
const calculator = new HashCalculator();
const algorithms = calculator.getSupportedAlgorithms();
expect(algorithms).toContain('sha256');
expect(algorithms).toContain('md5');
expect(algorithms).toContain('sha1');
expect(algorithms).toContain('sha512');
});
});
});
describe('测试 OutputFormatter 类', () => {
describe('测试 OutputFormatter.formatOutput 方法', () => {
it('应该正确格式化输出', () => {
const formatter = new OutputFormatter();
const result = formatter.formatOutput(['file1.txt', 'file2.txt'], 'sha256', 'test-hash');
expect(result).toContain('file1.txt');
expect(result).toContain('file2.txt');
expect(result).toContain('Algorithm: sha256');
expect(result).toContain('Combined Hash: test-hash');
});
it('应该处理空文件列表', () => {
const formatter = new OutputFormatter();
const result = formatter.formatOutput([], 'sha256', 'test-hash');
expect(result).toContain('No files found');
expect(result).toContain('Algorithm: sha256');
expect(result).toContain('Combined Hash: test-hash');
});
it('应该处理空算法名称', () => {
const formatter = new OutputFormatter();
const result = formatter.formatOutput(['file1.txt'], '', 'test-hash');
expect(result).toContain('Algorithm: ');
expect(result).toContain('Combined Hash: test-hash');
});
it('应该处理空哈希值', () => {
const formatter = new OutputFormatter();
const result = formatter.formatOutput(['file1.txt'], 'sha256', '');
expect(result).toContain('Algorithm: sha256');
expect(result).toContain('Combined Hash: ');
});
it('应该处理特殊字符文件名', () => {
const formatter = new OutputFormatter();
const specialFiles = [
'file with spaces.txt',
'file-with-dashes.txt',
'file_with_underscores.txt',
'file.with.dots.txt',
'file@with#symbols.txt',
];
const result = formatter.formatOutput(specialFiles, 'sha256', 'test-hash');
specialFiles.forEach(file => {
expect(result).toContain(file);
});
});
it('应该处理很长的文件列表', () => {
const formatter = new OutputFormatter();
const manyFiles = Array.from({ length: 100 }, (_, i) => `file${i}.txt`);
const result = formatter.formatOutput(manyFiles, 'sha256', 'test-hash');
expect(result).toContain('Total files: 100');
expect(result).toContain('Algorithm: sha256');
expect(result).toContain('Combined Hash: test-hash');
});
it('应该处理包含路径的文件名', () => {
const formatter = new OutputFormatter();
const pathFiles = [
'/absolute/path/file1.txt',
'./relative/path/file2.txt',
'../parent/path/file3.txt',
'C:\\Windows\\path\\file4.txt',
];
const result = formatter.formatOutput(pathFiles, 'sha256', 'test-hash');
pathFiles.forEach(file => {
expect(result).toContain(file);
});
});
});
describe('测试 OutputFormatter.setGitHubOutput 方法', () => {
let originalEnv;
beforeEach(() => {
originalEnv = process.env.GITHUB_OUTPUT;
});
afterEach(() => {
process.env.GITHUB_OUTPUT = originalEnv;
});
it('应该处理无 GitHub 输出文件的情况', () => {
delete process.env.GITHUB_OUTPUT;
const consoleSpy = vi.spyOn(console, 'log');
const formatter = new OutputFormatter();
formatter.setGitHubOutput('abcd1234', 2);
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::abcd1234');
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::2');
consoleSpy.mockRestore();
});
it('应该处理空的 GitHub 输出值', () => {
delete process.env.GITHUB_OUTPUT;
const consoleSpy = vi.spyOn(console, 'log');
const formatter = new OutputFormatter();
formatter.setGitHubOutput('', 0);
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::');
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::0');
consoleSpy.mockRestore();
});
it('应该处理包含特殊字符的 GitHub 输出', () => {
delete process.env.GITHUB_OUTPUT;
const consoleSpy = vi.spyOn(console, 'log');
const formatter = new OutputFormatter();
formatter.setGitHubOutput('special-value!@#', 1);
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=hash::special-value!@#');
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=file-count::1');
consoleSpy.mockRestore();
});
});
});
describe('测试 ActionInputs 类', () => {
describe('测试 ActionInputs.getInput 方法', () => {
it('应该获取环境变量中的输入值', () => {
process.env.INPUT_TEST_VALUE = 'test input';
const result = ActionInputs.getInput('test-value');
expect(result).toBe('test input');
delete process.env.INPUT_TEST_VALUE;
});
it('应该返回空字符串当环境变量不存在', () => {
const result = ActionInputs.getInput('non-existent');
expect(result).toBe('');
});
it('应该处理带连字符的参数名', () => {
process.env.INPUT_MY_TEST_INPUT = 'value with dash';
const result = ActionInputs.getInput('my-test-input');
expect(result).toBe('value with dash');
delete process.env.INPUT_MY_TEST_INPUT;
});
it('应该在必需参数缺失时抛出错误', () => {
expect(() => ActionInputs.getInput('required-param', true)).toThrow(ActionError);
});
it('不应该在必需参数存在时抛出错误', () => {
process.env.INPUT_REQUIRED_PARAM = 'exists';
expect(() => ActionInputs.getInput('required-param', true)).not.toThrow();
delete process.env.INPUT_REQUIRED_PARAM;
});
});
describe('测试 ActionInputs.getBooleanInput 方法', () => {
it('应该正确解析 true 值', () => {
process.env.INPUT_TEST_BOOL = 'true';
const result = ActionInputs.getBooleanInput('test-bool');
expect(result).toBe(true);
delete process.env.INPUT_TEST_BOOL;
});
it('应该正确解析 1 为 true', () => {
process.env.INPUT_TEST_BOOL = '1';
const result = ActionInputs.getBooleanInput('test-bool');
expect(result).toBe(true);
delete process.env.INPUT_TEST_BOOL;
});
it('应该正确解析 false 值', () => {
process.env.INPUT_TEST_BOOL = 'false';
const result = ActionInputs.getBooleanInput('test-bool');
expect(result).toBe(false);
delete process.env.INPUT_TEST_BOOL;
});
it('应该对非布尔值返回 false', () => {
process.env.INPUT_TEST_BOOL = 'random-string';
const result = ActionInputs.getBooleanInput('test-bool');
expect(result).toBe(false);
delete process.env.INPUT_TEST_BOOL;
});
it('应该处理大小写不敏感', () => {
process.env.INPUT_TEST_BOOL = 'TRUE';
const result = ActionInputs.getBooleanInput('test-bool');
expect(result).toBe(true);
delete process.env.INPUT_TEST_BOOL;
});
});
describe('测试 ActionInputs.getMultilineInput 方法', () => {
it('应该正确解析多行输入', () => {
process.env.INPUT_MULTI_LINE = 'line1\nline2\nline3';
const result = ActionInputs.getMultilineInput('multi-line');
expect(result).toHaveLength(3);
expect(result).toEqual(['line1', 'line2', 'line3']);
delete process.env.INPUT_MULTI_LINE;
});
it('应该过滤空行', () => {
process.env.INPUT_MULTI_LINE = 'line1\n\n \nline2\n \nline3';
const result = ActionInputs.getMultilineInput('multi-line');
expect(result).toHaveLength(3);
expect(result).toEqual(['line1', 'line2', 'line3']);
delete process.env.INPUT_MULTI_LINE;
});
it('应该处理空输入', () => {
process.env.INPUT_MULTI_LINE = '';
const result = ActionInputs.getMultilineInput('multi-line');
expect(result).toHaveLength(0);
delete process.env.INPUT_MULTI_LINE;
});
});
});
describe('测试 ActionOutputs 类', () => {
describe('测试 ActionOutputs.setOutput 方法', () => {
let originalEnv;
let outputContent = '';
beforeEach(() => {
originalEnv = process.env.GITHUB_OUTPUT;
// 模拟文件写入
const fs = require('fs');
fs.appendFileSync = (file, content) => {
outputContent += content;
};
});
afterEach(() => {
process.env.GITHUB_OUTPUT = originalEnv;
outputContent = '';
});
it('应该写入 GitHub 输出文件', () => {
process.env.GITHUB_OUTPUT = '/tmp/github_output.txt';
ActionOutputs.setOutput('test-name', 'test-value');
expect(outputContent).toBe('test-name=test-value\n');
});
it('应该在无输出文件时使用控制台输出', () => {
delete process.env.GITHUB_OUTPUT;
const consoleSpy = vi.spyOn(console, 'log');
ActionOutputs.setOutput('test-name', 'test-value');
expect(consoleSpy).toHaveBeenCalledWith('::set-output name=test-name::test-value');
consoleSpy.mockRestore();
});
});
describe('测试 ActionOutputs 日志方法', () => {
it('info 应该输出到控制台', () => {
const consoleSpy = vi.spyOn(console, 'log');
ActionOutputs.info('test message');
expect(consoleSpy).toHaveBeenCalledWith('test message');
consoleSpy.mockRestore();
});
it('warning 应该输出带格式的警告', () => {
const consoleSpy = vi.spyOn(console, 'log');
ActionOutputs.warning('test warning');
expect(consoleSpy).toHaveBeenCalledWith('::warning::test warning');
consoleSpy.mockRestore();
});
it('error 应该输出带格式的错误', () => {
const consoleSpy = vi.spyOn(console, 'log');
ActionOutputs.error('test error');
expect(consoleSpy).toHaveBeenCalledWith('::error::test error');
consoleSpy.mockRestore();
});
});
describe('测试 ActionOutputs.setFailed 方法', () => {
it('应该输出错误并退出进程', () => {
const consoleSpy = vi.spyOn(console, 'log');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
ActionOutputs.setFailed('test failure');
expect(consoleSpy).toHaveBeenCalledWith('::error::test failure');
expect(exitSpy).toHaveBeenCalledWith(1);
consoleSpy.mockRestore();
exitSpy.mockRestore();
});
});
});
describe('测试 ActionError 类', () => {
it('应该创建带有正确属性的错误实例', () => {
const error = new ActionError(ErrorType.FILE_NOT_FOUND, 'File not found', 'test.txt', 'Additional details');
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(ActionError);
expect(error.type).toBe(ErrorType.FILE_NOT_FOUND);
expect(error.message).toBe('File not found');
expect(error.file).toBe('test.txt');
expect(error.details).toBe('Additional details');
expect(error.name).toBe('ActionError');
});
it('应该处理可选参数为 null 的情况', () => {
const error = new ActionError(ErrorType.INVALID_ALGORITHM, 'Invalid algorithm');
expect(error.type).toBe(ErrorType.INVALID_ALGORITHM);
expect(error.message).toBe('Invalid algorithm');
expect(error.file).toBeNull();
expect(error.details).toBeNull();
});
it('应该包含正确的错误类型常量', () => {
expect(ErrorType.FILE_NOT_FOUND).toBe('FILE_NOT_FOUND');
expect(ErrorType.PERMISSION_DENIED).toBe('PERMISSION_DENIED');
expect(ErrorType.INVALID_ALGORITHM).toBe('INVALID_ALGORITHM');
expect(ErrorType.HASH_CALCULATION_FAILED).toBe('HASH_CALCULATION_FAILED');
});
});
describe('测试 FilesHashAction 类', () => {
describe('测试 FilesHashAction 边界情况 - 无文件和空输入', () => {
it('应该处理空的文件模式', async () => {
const action = new FilesHashAction();
// 模拟空的文件模式输入
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'sha256';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return [''];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
expect(exitSpy).toHaveBeenCalledWith(1);
consoleSpy.mockRestore();
exitSpy.mockRestore();
});
it('应该处理无效的文件模式', async () => {
const action = new FilesHashAction();
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'sha256';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return ['nonexistent/**/*.txt'];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
expect(exitSpy).toHaveBeenCalledWith(1);
consoleSpy.mockRestore();
exitSpy.mockRestore();
});
});
describe('测试 FilesHashAction 边界情况 - 权限和文件系统错误', () => {
it('应该处理文件读取权限错误', async () => {
const action = new FilesHashAction();
const tempDir = await createTestDir();
const restrictedFile = path.join(tempDir, 'restricted.txt');
await createTestFile(restrictedFile, 'content');
// 模拟文件读取错误
vi.spyOn(action.hashCalculator, 'calculateFileHash').mockRejectedValue(
new ActionError(ErrorType.HASH_CALCULATION_FAILED, 'Permission denied', restrictedFile)
);
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'sha256';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return [restrictedFile];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
expect(exitSpy).toHaveBeenCalledWith(1);
cleanupTestFiles(restrictedFile);
cleanupTestDirs(tempDir);
consoleSpy.mockRestore();
exitSpy.mockRestore();
});
});
describe('测试 FilesHashAction 边界情况 - 算法和配置错误', () => {
it('应该处理不支持算法', async () => {
const action = new FilesHashAction();
const tempDir = await createTestDir();
const testFile = path.join(tempDir, 'test.txt');
await createTestFile(testFile, 'content');
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'unsupported-algorithm';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return [testFile];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('::error::'));
expect(exitSpy).toHaveBeenCalledWith(1);
cleanupTestFiles(testFile);
cleanupTestDirs(tempDir);
consoleSpy.mockRestore();
exitSpy.mockRestore();
});
});
describe('测试 FilesHashAction 边界情况 - 输出和格式边界情况', () => {
it('应该处理很长的文件列表输出', async () => {
const action = new FilesHashAction();
const tempDir = await createTestDir();
// 创建多个文件
const files = [];
for (let i = 0; i < 10; i++) {
const file = path.join(tempDir, `file${i}.txt`);
await createTestFile(file, `content${i}`);
files.push(file);
}
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'sha256';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return [path.join(tempDir, '*.txt')];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File count: 10'));
cleanupTestFiles(...files);
cleanupTestDirs(tempDir);
consoleSpy.mockRestore();
});
it('应该处理空文件内容', async () => {
const action = new FilesHashAction();
const tempDir = await createTestDir();
const emptyFile = path.join(tempDir, 'empty.txt');
await createTestFile(emptyFile, '');
vi.spyOn(ActionInputs, 'getInput').mockImplementation(name => {
if (name === 'algorithm') return 'sha256';
return '';
});
vi.spyOn(ActionInputs, 'getMultilineInput').mockImplementation(name => {
if (name === 'files') return [emptyFile];
return [];
});
vi.spyOn(ActionInputs, 'getBooleanInput').mockReturnValue(false);
const consoleSpy = vi.spyOn(console, 'log');
await action.run();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Combined hash:'));
cleanupTestFiles(emptyFile);
cleanupTestDirs(tempDir);
consoleSpy.mockRestore();
});
});
});
describe('测试 FilesHashAction 集成测试', () => {
describe('测试 FilesHashAction 完整工作流', () => {
const testFiles = ['integration-test/file1.txt', 'integration-test/file2.js', 'integration-test/subdir/file3.json'];
beforeEach(async () => {
// 创建测试文件
await createTestFile(testFiles[0], 'File 1 content');
await createTestFile(testFiles[1], 'console.log("Hello");');
await createTestFile(testFiles[2], '{"test": true}');
// 模拟环境变量
process.env.INPUT_FILES = 'integration-test/**/*';
process.env.INPUT_ALGORITHM = 'sha256';
});
afterEach(async () => {
// 清理环境变量
delete process.env.INPUT_FILES;
delete process.env.INPUT_ALGORITHM;
// 清理测试文件
await cleanupTestFiles(testFiles);
await cleanupTestDirs(['integration-test/subdir', 'integration-test']);
});
it('应该成功完成完整工作流程', async () => {
const action = new FilesHashAction();
// 测试文件发现
const patterns = action.fileDiscovery.parseFilePatterns(['integration-test/**/*']);
const foundFiles = await action.fileDiscovery.findFiles(patterns);
expect(foundFiles.length).toBeGreaterThanOrEqual(3);
// 测试哈希计算
const hash = await action.hashCalculator.calculateCombinedHash(foundFiles, 'sha256');
expect(hash).toHaveLength(64);
});
});
});