- Simplify parseApkFilename to use version delimiter split - Update tests to match new data structure with rawEnvironment - Remove unnecessary test cases and assertions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import { validatePath, getProjects, getVersions, getApks } from '../fs-utils';
|
|
import * as fs from 'fs/promises';
|
|
|
|
// Mock fs module
|
|
jest.mock('fs/promises', () => ({
|
|
readdir: jest.fn(),
|
|
stat: jest.fn(),
|
|
}));
|
|
|
|
const mockedFs = fs as jest.Mocked<typeof fs>;
|
|
|
|
describe('validatePath', () => {
|
|
test('应该验证正常路径', () => {
|
|
expect(validatePath('/base', 'dir')).toBe('/base/dir');
|
|
expect(validatePath('/base', 'dir', 'subdir')).toBe('/base/dir/subdir');
|
|
});
|
|
|
|
test('应该解析相对路径', () => {
|
|
expect(validatePath('/base', './dir')).toBe('/base/dir');
|
|
expect(validatePath('/base/../base', 'dir')).toBe('/base/dir');
|
|
});
|
|
|
|
test('应该检测并拒绝路径遍历攻击', () => {
|
|
expect(() => validatePath('/base', '../etc')).toThrow('Invalid path: path traversal detected');
|
|
expect(() => validatePath('/base', 'dir', '../../etc')).toThrow('Invalid path: path traversal detected');
|
|
expect(() => validatePath('/base', '../base')).not.toThrow(); // 同级目录应被允许
|
|
});
|
|
|
|
test('应该处理特殊字符的路径', () => {
|
|
expect(validatePath('/base', 'dir with spaces')).toBe('/base/dir with spaces');
|
|
expect(validatePath('/base', 'dir-with-dashes')).toBe('/base/dir-with-dashes');
|
|
});
|
|
});
|
|
|
|
describe('getProjects', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('应该返回项目列表', async () => {
|
|
const mockDirs = ['project1', 'project2', 'not-a-dir.txt'];
|
|
mockedFs.readdir.mockResolvedValue(mockDirs as any);
|
|
|
|
// Mock stat for each directory
|
|
mockedFs.stat
|
|
.mockResolvedValueOnce({ isDirectory: () => true } as any)
|
|
.mockResolvedValueOnce({ isDirectory: () => true } as any)
|
|
.mockResolvedValueOnce({ isDirectory: () => false } as any);
|
|
|
|
const result = await getProjects();
|
|
|
|
expect(result).toEqual(['project1', 'project2']);
|
|
});
|
|
|
|
test('应该处理读取目录错误', async () => {
|
|
mockedFs.readdir.mockRejectedValue(new Error('Permission denied'));
|
|
|
|
await expect(getProjects()).rejects.toThrow();
|
|
});
|
|
|
|
test('应该处理单个文件的状态检查错误', async () => {
|
|
mockedFs.readdir.mockResolvedValue(['project1', 'project2']);
|
|
|
|
mockedFs.stat
|
|
.mockResolvedValueOnce({ isDirectory: () => true } as any)
|
|
.mockRejectedValueOnce(new Error('ENOENT'));
|
|
|
|
const result = await getProjects();
|
|
|
|
expect(result).toEqual(['project1']);
|
|
});
|
|
});
|
|
|
|
describe('getVersions', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('应该返回指定项目的版本列表并排序', async () => {
|
|
const mockVersions = ['1_0', '0_42', '2_1'];
|
|
mockedFs.readdir.mockResolvedValue(mockVersions as any);
|
|
|
|
mockedFs.stat.mockResolvedValue({ isDirectory: () => true } as any);
|
|
|
|
const result = await getVersions('test-project');
|
|
|
|
expect(mockedFs.readdir).toHaveBeenCalledWith(expect.stringContaining('test-project'));
|
|
expect(result).toEqual(['0_42', '1_0', '2_1']);
|
|
});
|
|
|
|
test('应该过滤掉非目录文件', async () => {
|
|
const mockVersions = ['1_0', 'readme.txt', '2_1'];
|
|
mockedFs.readdir.mockResolvedValue(mockVersions as any);
|
|
|
|
mockedFs.stat
|
|
.mockResolvedValueOnce({ isDirectory: () => true } as any)
|
|
.mockResolvedValueOnce({ isDirectory: () => false } as any)
|
|
.mockResolvedValueOnce({ isDirectory: () => true } as any);
|
|
|
|
const result = await getVersions('test-project');
|
|
|
|
expect(result).toEqual(['1_0', '2_1']);
|
|
});
|
|
});
|
|
|
|
describe('getApks', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('应该返回指定版本的 APK 列表并按修改时间排序', async () => {
|
|
const mockFiles = ['app-v1.apk', 'app-v2.apk', 'readme.txt'];
|
|
mockedFs.readdir.mockResolvedValue(mockFiles as any);
|
|
|
|
const baseTime = new Date('2024-01-01T00:00:00');
|
|
mockedFs.stat
|
|
.mockResolvedValueOnce({
|
|
isDirectory: () => false,
|
|
size: 1024 * 1024,
|
|
mtime: baseTime,
|
|
} as any)
|
|
.mockResolvedValueOnce({
|
|
isDirectory: () => false,
|
|
size: 2 * 1024 * 1024,
|
|
mtime: new Date('2024-01-02T00:00:00'),
|
|
} as any)
|
|
.mockResolvedValueOnce({
|
|
isDirectory: () => false,
|
|
size: 100,
|
|
mtime: new Date('2024-01-03T00:00:00'),
|
|
} as any);
|
|
|
|
const result = await getApks('test-project', '1_0');
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].name).toBe('app-v2.apk');
|
|
expect(result[1].name).toBe('app-v1.apk');
|
|
});
|
|
|
|
test('应该从文件名中提取 commit id', async () => {
|
|
const mockFiles = ['app-abc1234567.apk', 'app.apk'];
|
|
mockedFs.readdir.mockResolvedValue(mockFiles as any);
|
|
|
|
mockedFs.stat.mockResolvedValue({
|
|
isDirectory: () => false,
|
|
size: 1024 * 1024,
|
|
mtime: new Date('2024-01-01T00:00:00'),
|
|
} as any);
|
|
|
|
const result = await getApks('test-project', '1_0');
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].commit).toBe('abc1234567');
|
|
expect(result[1].commit).toBeNull();
|
|
});
|
|
|
|
test('应该按 commitId 过滤 APK', async () => {
|
|
const mockFiles = ['app-abc1234567.apk', 'app-def4567890.apk'];
|
|
mockedFs.readdir.mockResolvedValue(mockFiles as any);
|
|
|
|
const time1 = new Date('2024-01-01T00:00:00');
|
|
const time2 = new Date('2024-01-02T00:00:00');
|
|
mockedFs.stat
|
|
.mockResolvedValueOnce({
|
|
isDirectory: () => false,
|
|
size: 1024 * 1024,
|
|
mtime: time1,
|
|
} as any)
|
|
.mockResolvedValueOnce({
|
|
isDirectory: () => false,
|
|
size: 2 * 1024 * 1024,
|
|
mtime: time2,
|
|
} as any);
|
|
|
|
const result = await getApks('test-project', '1_0', 'abc123');
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('app-abc1234567.apk');
|
|
});
|
|
|
|
test('应该忽略 commitId 的大小写', async () => {
|
|
const mockFiles = ['app-ABC1234567.apk'];
|
|
mockedFs.readdir.mockResolvedValue(mockFiles as any);
|
|
|
|
mockedFs.stat.mockResolvedValue({
|
|
isDirectory: () => false,
|
|
size: 1024 * 1024,
|
|
mtime: new Date('2024-01-01T00:00:00'),
|
|
} as any);
|
|
|
|
const result = await getApks('test-project', '1_0', 'abc');
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].commit).toBe('ABC1234567');
|
|
});
|
|
|
|
test('当 apks 目录不存在时应该返回空数组', async () => {
|
|
mockedFs.readdir.mockRejectedValue(new Error('ENOENT'));
|
|
|
|
const result = await getApks('test-project', '1_0');
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test('应该只返回 .apk 文件', async () => {
|
|
const mockFiles = ['app.apk', 'readme.txt', 'config.json', 'another.apk'];
|
|
mockedFs.readdir.mockResolvedValue(mockFiles as any);
|
|
|
|
mockedFs.stat.mockResolvedValue({
|
|
isDirectory: () => false,
|
|
size: 1024 * 1024,
|
|
mtime: new Date('2024-01-01T00:00:00'),
|
|
} as any);
|
|
|
|
const result = await getApks('test-project', '1_0');
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.every(apk => apk.name.endsWith('.apk'))).toBe(true);
|
|
});
|
|
});
|