test: improve branch coverage with edge cases
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { PERMISSIONS, hasPermission, Role, Resource, Action } from './permissions';
|
||||
|
||||
describe('permissions', () => {
|
||||
describe('PERMISSIONS constant', () => {
|
||||
it('should have admin permissions', () => {
|
||||
expect(PERMISSIONS.admin).toBeDefined();
|
||||
expect(PERMISSIONS.admin.content).toContain('create');
|
||||
expect(PERMISSIONS.admin.content).toContain('read');
|
||||
expect(PERMISSIONS.admin.content).toContain('update');
|
||||
expect(PERMISSIONS.admin.content).toContain('delete');
|
||||
expect(PERMISSIONS.admin.content).toContain('publish');
|
||||
});
|
||||
|
||||
it('should have editor permissions', () => {
|
||||
expect(PERMISSIONS.editor).toBeDefined();
|
||||
expect(PERMISSIONS.editor.content).toContain('create');
|
||||
expect(PERMISSIONS.editor.content).toContain('read');
|
||||
expect(PERMISSIONS.editor.content).toContain('update');
|
||||
expect(PERMISSIONS.editor.content).toContain('publish');
|
||||
expect(PERMISSIONS.editor.content).not.toContain('delete');
|
||||
});
|
||||
|
||||
it('should have viewer permissions', () => {
|
||||
expect(PERMISSIONS.viewer).toBeDefined();
|
||||
expect(PERMISSIONS.viewer.content).toContain('read');
|
||||
expect(PERMISSIONS.viewer.content).not.toContain('create');
|
||||
expect(PERMISSIONS.viewer.content).not.toContain('update');
|
||||
expect(PERMISSIONS.viewer.content).not.toContain('delete');
|
||||
});
|
||||
|
||||
it('should have config permissions', () => {
|
||||
expect(PERMISSIONS.admin.config).toContain('read');
|
||||
expect(PERMISSIONS.admin.config).toContain('update');
|
||||
expect(PERMISSIONS.editor.config).toContain('read');
|
||||
expect(PERMISSIONS.editor.config).not.toContain('update');
|
||||
});
|
||||
|
||||
it('should have users permissions', () => {
|
||||
expect(PERMISSIONS.admin.users).toContain('create');
|
||||
expect(PERMISSIONS.admin.users).toContain('read');
|
||||
expect(PERMISSIONS.admin.users).toContain('update');
|
||||
expect(PERMISSIONS.admin.users).toContain('delete');
|
||||
expect(PERMISSIONS.editor.users).toEqual([]);
|
||||
expect(PERMISSIONS.viewer.users).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have logs permissions', () => {
|
||||
expect(PERMISSIONS.admin.logs).toContain('read');
|
||||
expect(PERMISSIONS.editor.logs).toContain('read');
|
||||
expect(PERMISSIONS.viewer.logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermission', () => {
|
||||
describe('admin role', () => {
|
||||
it('should allow all content actions', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'create')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'publish')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow config actions', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'config';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow all users actions', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'users';
|
||||
|
||||
expect(hasPermission(role, resource, 'create')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow logs read', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'logs';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editor role', () => {
|
||||
it('should allow content actions except delete', () => {
|
||||
const role: Role = 'editor';
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'create')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'publish')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow config read only', () => {
|
||||
const role: Role = 'editor';
|
||||
const resource: Resource = 'config';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not allow users actions', () => {
|
||||
const role: Role = 'editor';
|
||||
const resource: Resource = 'users';
|
||||
|
||||
expect(hasPermission(role, resource, 'create')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow logs read', () => {
|
||||
const role: Role = 'editor';
|
||||
const resource: Resource = 'logs';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewer role', () => {
|
||||
it('should only allow content read', () => {
|
||||
const role: Role = 'viewer';
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'create')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'publish')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow config read only', () => {
|
||||
const role: Role = 'viewer';
|
||||
const resource: Resource = 'config';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(true);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not allow users actions', () => {
|
||||
const role: Role = 'viewer';
|
||||
const resource: Resource = 'users';
|
||||
|
||||
expect(hasPermission(role, resource, 'create')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'update')).toBe(false);
|
||||
expect(hasPermission(role, resource, 'delete')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not allow logs actions', () => {
|
||||
const role: Role = 'viewer';
|
||||
const resource: Resource = 'logs';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return false for invalid role', () => {
|
||||
const role = 'invalid' as Role;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid resource', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource = 'invalid' as Resource;
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid action', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'content';
|
||||
const action = 'invalid' as Action;
|
||||
|
||||
expect(hasPermission(role, resource, action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null role', () => {
|
||||
const role = null as any;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined resource', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource = undefined as any;
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty action string', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'content';
|
||||
const action = '' as Action;
|
||||
|
||||
expect(hasPermission(role, resource, action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case sensitivity for roles', () => {
|
||||
const role = 'ADMIN' as Role;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case sensitivity for resources', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource = 'CONTENT' as Resource;
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case sensitivity for actions', () => {
|
||||
const role: Role = 'admin';
|
||||
const resource: Resource = 'content';
|
||||
const action = 'READ' as Action;
|
||||
|
||||
expect(hasPermission(role, resource, action)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined role', () => {
|
||||
const role = undefined as any;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle numeric role', () => {
|
||||
const role = 123 as any;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle object role', () => {
|
||||
const role = {} as any;
|
||||
const resource: Resource = 'content';
|
||||
|
||||
expect(hasPermission(role, resource, 'read')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type Exports', () => {
|
||||
it('should export Role type', () => {
|
||||
const role: Role = 'admin';
|
||||
expect(role).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export Resource type', () => {
|
||||
const resource: Resource = 'content';
|
||||
expect(resource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export Action type', () => {
|
||||
const action: Action = 'read';
|
||||
expect(action).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,446 @@
|
||||
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
||||
import path from 'path';
|
||||
import { writeFile, mkdir, unlink, stat } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('fs');
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: jest.fn(() => 'test-id-12345'),
|
||||
}));
|
||||
|
||||
const mockedWriteFile = writeFile as jest.MockedFunction<typeof writeFile>;
|
||||
const mockedMkdir = mkdir as jest.MockedFunction<typeof mkdir>;
|
||||
const mockedUnlink = unlink as jest.MockedFunction<typeof unlink>;
|
||||
const mockedStat = stat as jest.MockedFunction<typeof stat>;
|
||||
const mockedExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
|
||||
|
||||
describe('Upload Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env.UPLOAD_DIR = './test-uploads';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.UPLOAD_DIR;
|
||||
});
|
||||
|
||||
describe('getFileExtension', () => {
|
||||
it('should return correct extension for JPEG', async () => {
|
||||
const { getFileExtension } = await import('./upload');
|
||||
expect(getFileExtension('image/jpeg')).toBe('.jpg');
|
||||
});
|
||||
|
||||
it('should return correct extension for PNG', async () => {
|
||||
const { getFileExtension } = await import('./upload');
|
||||
expect(getFileExtension('image/png')).toBe('.png');
|
||||
});
|
||||
|
||||
it('should return correct extension for PDF', async () => {
|
||||
const { getFileExtension } = await import('./upload');
|
||||
expect(getFileExtension('application/pdf')).toBe('.pdf');
|
||||
});
|
||||
|
||||
it('should return empty string for unknown MIME type', async () => {
|
||||
const { getFileExtension } = await import('./upload');
|
||||
expect(getFileExtension('unknown/type')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowedType', () => {
|
||||
it('should return true for allowed image types', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('image/jpeg', 'image')).toBe(true);
|
||||
expect(isAllowedType('image/png', 'image')).toBe(true);
|
||||
expect(isAllowedType('image/gif', 'image')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for allowed document types', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('application/pdf', 'document')).toBe(true);
|
||||
expect(isAllowedType('application/msword', 'document')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for disallowed types', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('image/jpeg', 'document')).toBe(false);
|
||||
expect(isAllowedType('application/pdf', 'image')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for unknown MIME types', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('unknown/type', 'image')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null type', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType(null as any, 'image')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined type', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType(undefined as any, 'image')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty string type', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('', 'image')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle invalid category', async () => {
|
||||
const { isAllowedType } = await import('./upload');
|
||||
expect(isAllowedType('image/jpeg', 'invalid' as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFileSignature', () => {
|
||||
it('should validate JPEG signature correctly', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const validJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0x00]);
|
||||
expect(validateFileSignature(validJpegBuffer, 'image/jpeg')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate PNG signature correctly', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const validPngBuffer = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00]);
|
||||
expect(validateFileSignature(validPngBuffer, 'image/png')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate PDF signature correctly', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const validPdfBuffer = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00]);
|
||||
expect(validateFileSignature(validPdfBuffer, 'application/pdf')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid JPEG signature', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const invalidBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||
expect(validateFileSignature(invalidBuffer, 'image/jpeg')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for SVG files', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const svgBuffer = Buffer.from('<svg></svg>');
|
||||
expect(validateFileSignature(svgBuffer, 'image/svg+xml')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for unknown MIME types', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
const buffer = Buffer.from([0x00, 0x00, 0x00]);
|
||||
expect(validateFileSignature(buffer, 'unknown/type')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeFileName', () => {
|
||||
it('should remove special characters', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
expect(sanitizeFileName('test<>:"/\\|?*.jpg')).toBe('test_________.jpg');
|
||||
});
|
||||
|
||||
it('should convert to lowercase', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
expect(sanitizeFileName('TestFile.JPG')).toBe('testfile.jpg');
|
||||
});
|
||||
|
||||
it('should replace multiple dots', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
expect(sanitizeFileName('test..file...jpg')).toBe('test.file.jpg');
|
||||
});
|
||||
|
||||
it('should preserve Chinese characters', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
expect(sanitizeFileName('测试文件.jpg')).toBe('测试文件.jpg');
|
||||
});
|
||||
|
||||
it('should preserve underscores and hyphens', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
expect(sanitizeFileName('test_file-name.jpg')).toBe('test_file-name.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDangerousFile', () => {
|
||||
it('should detect .exe files as dangerous', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('malware.exe')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect .bat files as dangerous', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('script.bat')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect .php files as dangerous', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('webshell.php')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect .js files as dangerous', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('script.js')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not flag safe files as dangerous', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('image.jpg')).toBe(false);
|
||||
expect(isDangerousFile('document.pdf')).toBe(false);
|
||||
});
|
||||
|
||||
it('should be case insensitive', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
expect(isDangerousFile('MALWARE.EXE')).toBe(true);
|
||||
expect(isDangerousFile('Script.BAT')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDatePath', () => {
|
||||
it('should return correct date path format', async () => {
|
||||
const { getDatePath } = await import('./upload');
|
||||
const datePath = getDatePath();
|
||||
const regex = /^\d{4}\/\d{2}\/\d{2}$/;
|
||||
expect(regex.test(datePath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should pad month and day with zeros', async () => {
|
||||
const { getDatePath } = await import('./upload');
|
||||
const datePath = getDatePath();
|
||||
const parts = datePath.split('/');
|
||||
expect(parts[1].length).toBe(2);
|
||||
expect(parts[2].length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
const createMockFile = (overrides: Partial<File> = {}): File => {
|
||||
return {
|
||||
name: 'test.jpg',
|
||||
size: 1024,
|
||||
type: 'image/jpeg',
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(1024)),
|
||||
...overrides,
|
||||
} as any;
|
||||
};
|
||||
|
||||
it('should upload a valid image file successfully', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const validJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0x00]);
|
||||
const mockFile = createMockFile({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(validJpegBuffer),
|
||||
});
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedWriteFile.mockResolvedValue();
|
||||
|
||||
const result = await uploadFile(mockFile, { type: 'image' });
|
||||
|
||||
expect(result.id).toBe('test-id-12345');
|
||||
expect(result.name).toBe('test.jpg');
|
||||
expect(result.type).toBe('image/jpeg');
|
||||
expect(result.url).toContain('/uploads/image/');
|
||||
expect(result.url).toContain('.jpg');
|
||||
});
|
||||
|
||||
it('should reject files exceeding size limit', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const largeFile = createMockFile({
|
||||
size: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
await expect(uploadFile(largeFile, { type: 'image', maxSize: 5 * 1024 * 1024 }))
|
||||
.rejects.toThrow('文件大小超过限制');
|
||||
});
|
||||
|
||||
it('should reject disallowed file types', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const invalidFile = createMockFile({
|
||||
type: 'application/exe',
|
||||
});
|
||||
|
||||
await expect(uploadFile(invalidFile, { type: 'image' }))
|
||||
.rejects.toThrow('不支持的文件类型');
|
||||
});
|
||||
|
||||
it('should reject dangerous file extensions', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const dangerousFile = createMockFile({
|
||||
name: 'malware.exe',
|
||||
type: 'application/pdf',
|
||||
});
|
||||
|
||||
await expect(uploadFile(dangerousFile, { type: 'document' }))
|
||||
.rejects.toThrow('不允许上传此类型的文件');
|
||||
});
|
||||
|
||||
it('should reject files with mismatched signatures', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const fakeJpegBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||
const fakeFile = createMockFile({
|
||||
type: 'image/jpeg',
|
||||
arrayBuffer: jest.fn().mockResolvedValue(fakeJpegBuffer),
|
||||
});
|
||||
|
||||
await expect(uploadFile(fakeFile, { type: 'image' }))
|
||||
.rejects.toThrow('文件内容与声明类型不匹配');
|
||||
});
|
||||
|
||||
it('should create upload directory if it does not exist', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const validJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0x00]);
|
||||
const mockFile = createMockFile({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(validJpegBuffer),
|
||||
});
|
||||
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
mockedMkdir.mockResolvedValue(undefined as any);
|
||||
mockedWriteFile.mockResolvedValue();
|
||||
|
||||
await uploadFile(mockFile, { type: 'image' });
|
||||
|
||||
expect(mockedMkdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include userId in upload result', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const validJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0x00]);
|
||||
const mockFile = createMockFile({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(validJpegBuffer),
|
||||
});
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedWriteFile.mockResolvedValue();
|
||||
|
||||
const result = await uploadFile(mockFile, { type: 'image', userId: 'user-123' });
|
||||
|
||||
expect(result.uploadedBy).toBe('user-123');
|
||||
});
|
||||
|
||||
it('should sanitize file name', async () => {
|
||||
const { uploadFile } = await import('./upload');
|
||||
|
||||
const validJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0x00, 0x00]);
|
||||
const mockFile = createMockFile({
|
||||
name: 'Test<>File.JPG',
|
||||
arrayBuffer: jest.fn().mockResolvedValue(validJpegBuffer),
|
||||
});
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedWriteFile.mockResolvedValue();
|
||||
|
||||
const result = await uploadFile(mockFile, { type: 'image' });
|
||||
|
||||
expect(result.name).toBe('test__file.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFile', () => {
|
||||
it('should delete existing file successfully', async () => {
|
||||
const { deleteFile } = await import('./upload');
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedUnlink.mockResolvedValue();
|
||||
|
||||
const result = await deleteFile('/uploads/image/test.jpg');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockedUnlink).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false for non-existent file', async () => {
|
||||
const { deleteFile } = await import('./upload');
|
||||
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = await deleteFile('/uploads/image/nonexistent.jpg');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockedUnlink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false on deletion error', async () => {
|
||||
const { deleteFile } = await import('./upload');
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedUnlink.mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
const result = await deleteFile('/uploads/image/test.jpg');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileInfo', () => {
|
||||
it('should return file information for existing file', async () => {
|
||||
const { getFileInfo } = await import('./upload');
|
||||
|
||||
const mockStats = {
|
||||
size: 1024,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-02'),
|
||||
};
|
||||
|
||||
mockedStat.mockResolvedValue(mockStats as any);
|
||||
|
||||
const result = await getFileInfo('/path/to/file.jpg');
|
||||
|
||||
expect(result).toEqual({
|
||||
size: 1024,
|
||||
createdAt: mockStats.birthtime,
|
||||
modifiedAt: mockStats.mtime,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for non-existent file', async () => {
|
||||
const { getFileInfo } = await import('./upload');
|
||||
|
||||
mockedStat.mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const result = await getFileInfo('/path/to/nonexistent.jpg');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Tests', () => {
|
||||
it('should prevent path traversal attacks', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
const maliciousName = '../../../etc/passwd';
|
||||
const sanitized = sanitizeFileName(maliciousName);
|
||||
|
||||
expect(sanitized).not.toContain('/');
|
||||
expect(sanitized).not.toContain('..');
|
||||
});
|
||||
|
||||
it('should prevent double extension attacks', async () => {
|
||||
const { sanitizeFileName } = await import('./upload');
|
||||
const maliciousName = 'image.jpg.php';
|
||||
const sanitized = sanitizeFileName(maliciousName);
|
||||
|
||||
expect(sanitized).toBe('image.jpg.php');
|
||||
});
|
||||
|
||||
it('should validate file signatures to prevent MIME type spoofing', async () => {
|
||||
const { validateFileSignature } = await import('./upload');
|
||||
|
||||
const fakeJpegBuffer = Buffer.from([0x25, 0x50, 0x44, 0x46]);
|
||||
expect(validateFileSignature(fakeJpegBuffer, 'image/jpeg')).toBe(false);
|
||||
|
||||
const realJpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF]);
|
||||
expect(validateFileSignature(realJpegBuffer, 'image/jpeg')).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject all dangerous file extensions', async () => {
|
||||
const { isDangerousFile } = await import('./upload');
|
||||
const dangerousExtensions = ['.exe', '.bat', '.cmd', '.sh', '.php', '.jsp', '.asp', '.aspx', '.js'];
|
||||
|
||||
dangerousExtensions.forEach(ext => {
|
||||
expect(isDangerousFile(`malware${ext}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user