Parcourir la source

✅ feat(server): 从server-test分支恢复测试目录

- 恢复server包的完整测试目录结构
- 包含集成测试、单元测试和测试工具类
- 为重构后的server包集成测试提供参考

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname il y a 1 mois
Parent
commit
010a669ba6

+ 411 - 0
packages/server/tests/integration/auth.integration.test.ts

@@ -0,0 +1,411 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '../utils/integration-test-db';
+import { UserEntity } from '../../src/modules/users/user.entity';
+import { authRoutes } from '../../src/api';
+import { AuthService } from '../../src/modules/auth/auth.service';
+import { UserService } from '../../src/modules/users/user.service';
+import { DisabledStatus } from '../../src/share/types';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('认证API集成测试 (使用hono/testing)', () => {
+  let client: ReturnType<typeof testClient<typeof authRoutes>>['api']['v1'];
+  let authService: AuthService;
+  let userService: UserService;
+  let testToken: string;
+  let testUser: any;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(authRoutes).api.v1;
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 初始化服务
+    userService = new UserService(dataSource);
+    authService = new AuthService(userService);
+
+    // 创建测试用户前先删除可能存在的重复用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    await userRepository.delete({ username: 'testuser' });
+
+    testUser = await TestDataFactory.createTestUser(dataSource, {
+      username: 'testuser',
+      password: 'TestPassword123!',
+      email: 'testuser@example.com'
+    });
+
+    // 生成测试用户的token
+    testToken = authService.generateToken(testUser);
+  });
+
+  describe('登录端点测试 (POST /api/v1/auth/login)', () => {
+    it('应该使用正确凭据成功登录', async () => {
+      const loginData = {
+        username: 'testuser',
+        password: 'TestPassword123!'
+      };
+
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('token');
+        expect(responseData).toHaveProperty('user');
+        expect(responseData.user.username).toBe('testuser');
+        expect(responseData.user.email).toBe('testuser@example.com');
+        expect(typeof responseData.token).toBe('string');
+        expect(responseData.token.length).toBeGreaterThan(0);
+      }
+    });
+
+    it('应该拒绝错误密码的登录', async () => {
+      const loginData = {
+        username: 'testuser',
+        password: 'WrongPassword123!'
+      };
+
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+
+      // 认证失败应该返回401
+      expect(response.status).toBe(401);
+      if (response.status === 401){
+        const responseData = await response.json();
+        expect(responseData.message).toContain('用户名或密码错误');
+      }
+    });
+
+    it('应该拒绝不存在的用户登录', async () => {
+      const loginData = {
+        username: 'nonexistent_user',
+        password: 'TestPassword123!'
+      };
+
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+
+      // 认证失败应该返回401
+      expect(response.status).toBe(401);
+      if (response.status === 401){
+        const responseData = await response.json();
+        expect(responseData.message).toContain('用户名或密码错误');
+      }
+    });
+
+    it('应该拒绝禁用账户的登录', async () => {
+      // 创建禁用账户
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 先删除可能存在的重复用户
+      const userRepository = dataSource.getRepository(UserEntity);
+      await userRepository.delete({ username: 'disabled_user' });
+
+      await TestDataFactory.createTestUser(dataSource, {
+        username: 'disabled_user',
+        password: 'TestPassword123!',
+        email: 'disabled@example.com',
+        isDisabled: DisabledStatus.DISABLED
+      });
+
+      const loginData = {
+        username: 'disabled_user',
+        password: 'TestPassword123!'
+      };
+
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+
+      // 禁用账户应该返回401状态码
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('账户已禁用');
+      }
+    });
+  });
+
+  describe('令牌验证端点测试 (GET /api/v1/auth/sso-verify)', () => {
+    it('应该成功验证有效令牌', async () => {
+      const response = await client.auth['sso-verify'].$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        }
+      );
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseText = await response.text();
+        expect(responseText).toBe('OK');
+      }
+    });
+
+    it('应该拒绝无效令牌', async () => {
+      const response = await client.auth['sso-verify'].$get(
+        {},
+        {
+          headers: {
+            'Authorization': 'Bearer invalid.token.here'
+          }
+        }
+      );
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('令牌验证失败');
+      }
+    });
+
+    it('应该拒绝过期令牌', async () => {
+      // 创建立即过期的令牌
+      const expiredToken = authService.generateToken(testUser, '1ms');
+
+      // 等待令牌过期
+      await new Promise(resolve => setTimeout(resolve, 10));
+
+      const response = await client.auth['sso-verify'].$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${expiredToken}`
+          }
+        }
+      );
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('令牌验证失败');
+      }
+    });
+  });
+
+  describe('用户信息端点测试 (GET /api/v1/auth/me)', () => {
+    it('应该成功获取用户信息', async () => {
+      const response = await client.auth.me.$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        }
+      );
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('username');
+        expect(responseData).toHaveProperty('email');
+        expect(responseData.username).toBe('testuser');
+        expect(responseData.email).toBe('testuser@example.com');
+      }
+    });
+
+    it('应该拒绝无令牌的用户信息请求', async () => {
+      const response = await client.auth.me.$get();
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该拒绝无效令牌的用户信息请求', async () => {
+      const response = await client.auth.me.$get(
+        {},
+        {
+          headers: {
+            'Authorization': 'Bearer invalid.token.here'
+          }
+        }
+      );
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Invalid token');
+      }
+    });
+  });
+
+  describe('角色权限验证测试', () => {
+    it('应该为不同角色的用户生成包含正确角色信息的令牌', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 创建管理员角色
+      const adminRole = await TestDataFactory.createTestRole(dataSource, {
+        name: 'admin',
+        permissions: ['user:create', 'user:delete', 'user:update']
+      });
+
+      // 创建普通用户角色
+      const userRole = await TestDataFactory.createTestRole(dataSource, {
+        name: 'user',
+        permissions: ['user:read']
+      });
+
+      // 创建管理员用户
+      const adminUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'admin_user',
+        password: 'TestPassword123!',
+        email: 'admin@example.com'
+      });
+
+      // 创建普通用户
+      const regularUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'regular_user',
+        password: 'TestPassword123!',
+        email: 'regular@example.com'
+      });
+
+      // 分配角色
+      await userService.assignRoles(adminUser.id, [adminRole.id]);
+      await userService.assignRoles(regularUser.id, [userRole.id]);
+
+      // 重新加载用户以确保角色信息正确加载
+      const adminUserWithRoles = await userService.getUserById(adminUser.id);
+      const regularUserWithRoles = await userService.getUserById(regularUser.id);
+
+      // 生成令牌并验证角色信息
+      const adminToken = authService.generateToken(adminUserWithRoles!);
+      const regularToken = authService.generateToken(regularUserWithRoles!);
+
+      // 验证管理员令牌包含admin角色
+      const adminDecoded = authService.verifyToken(adminToken);
+      expect(adminDecoded.roles).toContain('admin');
+
+      // 验证普通用户令牌包含user角色
+      const regularDecoded = authService.verifyToken(regularToken);
+      expect(regularDecoded.roles).toContain('user');
+    });
+  });
+
+  describe('错误处理测试', () => {
+    it('应该正确处理认证失败错误', async () => {
+      const loginData = {
+        username: 'testuser',
+        password: 'WrongPassword'
+      };
+
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('code', 401);
+        expect(responseData).toHaveProperty('message');
+        expect(responseData.message).toContain('用户名或密码错误');
+      }
+    });
+
+    it('应该正确处理令牌过期错误', async () => {
+      // 模拟过期令牌
+      const expiredToken = 'expired.jwt.token.here';
+
+      const response = await client.auth['sso-verify'].$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${expiredToken}`
+          }
+        }
+      );
+
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('code', 401);
+        expect(responseData.message).toContain('令牌验证失败');
+      }
+    });
+
+    it('应该正确处理权限不足错误', async () => {
+      // 创建普通用户(无管理员权限)
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 先删除可能存在的重复用户
+      const userRepository = dataSource.getRepository(UserEntity);
+      await userRepository.delete({ username: 'regular_user' });
+
+      const regularUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'regular_user',
+        password: 'TestPassword123!',
+        email: 'regular@example.com'
+      });
+
+      const regularToken = authService.generateToken(regularUser);
+
+      // 尝试访问需要认证的端点(这里使用/me端点)
+      const response = await client.auth.me.$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${regularToken}`
+          }
+        }
+      );
+
+      // 普通用户应该能够访问自己的信息
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe('性能基准测试', () => {
+    it('登录操作响应时间应小于200ms', async () => {
+      const loginData = {
+        username: 'testuser',
+        password: 'TestPassword123!'
+      };
+
+      const startTime = Date.now();
+      const response = await client.auth.login.$post({
+        json: loginData
+      });
+      const endTime = Date.now();
+      const responseTime = endTime - startTime;
+
+      expect(response.status).toBe(200);
+      expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
+    });
+
+    it('令牌验证操作响应时间应小于200ms', async () => {
+      const startTime = Date.now();
+      const response = await client.auth['sso-verify'].$get(
+        {},
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        }
+      );
+      const endTime = Date.now();
+      const responseTime = endTime - startTime;
+
+      expect(response.status).toBe(200);
+      expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
+    });
+  });
+});

+ 229 - 0
packages/server/tests/integration/backup.integration.test.ts

@@ -0,0 +1,229 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { databaseBackup } from '../../src/utils/backup'
+import { databaseRestore } from '../../src/utils/restore'
+import path from 'path'
+
+// Mock pg-dump-restore for integration tests
+vi.mock('pg-dump-restore', async (importOriginal) => {
+  const actual = await importOriginal() as typeof import('pg-dump-restore')
+  return {
+    ...actual,
+    pgDump: vi.fn().mockResolvedValue({ size: 1024 }),
+    pgRestore: vi.fn().mockResolvedValue(undefined),
+  }
+})
+
+// Mock fs for integration tests
+vi.mock('fs', () => ({
+  promises: {
+    mkdir: vi.fn().mockResolvedValue(undefined),
+    chmod: vi.fn().mockResolvedValue(undefined),
+    readdir: vi.fn().mockResolvedValue([]),
+    stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() }),
+    access: vi.fn().mockResolvedValue(undefined),
+    unlink: vi.fn().mockResolvedValue(undefined),
+    writeFile: vi.fn().mockResolvedValue(undefined),
+    rm: vi.fn().mockResolvedValue(undefined),
+    utimes: vi.fn().mockResolvedValue(undefined),
+  }
+}))
+
+// Mock node-cron with importOriginal for proper partial mocking
+vi.mock('node-cron', async (importOriginal) => {
+  const actual = await importOriginal() as typeof import('node-cron')
+  return {
+    ...actual,
+    default: {
+      schedule: vi.fn().mockImplementation(() => ({
+        stop: vi.fn(),
+        nextDate: vi.fn().mockReturnValue(new Date()),
+      })),
+    },
+  }
+})
+
+// Mock logger
+vi.mock('../../src/utils/logger', () => ({
+  logger: {
+    db: vi.fn(),
+    error: vi.fn(),
+    api: vi.fn(),
+    middleware: vi.fn(),
+  },
+}))
+
+describe('Database Backup Integration', () => {
+  const testBackupDir = './test-backups'
+
+  beforeEach(async () => {
+    vi.clearAllMocks()
+
+    // 设置测试环境变量
+    process.env.BACKUP_DIR = testBackupDir
+    process.env.BACKUP_RETENTION_DAYS = '1'
+
+    // 重置所有mock函数
+    const { promises } = await import('fs')
+    const mockedPromises = vi.mocked(promises)
+
+    // 重置所有mock实现
+    mockedPromises.mkdir.mockResolvedValue(undefined)
+    mockedPromises.chmod.mockResolvedValue(undefined)
+    mockedPromises.readdir.mockResolvedValue([])
+    mockedPromises.stat.mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() } as any)
+    mockedPromises.access.mockResolvedValue(undefined)
+    mockedPromises.unlink.mockResolvedValue(undefined)
+    mockedPromises.writeFile.mockResolvedValue(undefined)
+    mockedPromises.rm.mockResolvedValue(undefined)
+    mockedPromises.utimes.mockResolvedValue(undefined)
+  })
+
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
+
+  describe('Backup Creation', () => {
+    it('应该成功创建备份文件', async () => {
+      const backupFile = await databaseBackup.createBackup()
+
+      console.debug('backupFile', backupFile)
+
+      expect(backupFile).toBeDefined()
+      expect(backupFile).toContain('.dump')
+
+      // 验证文件已创建
+      const exists = await databaseBackup.backupExists(backupFile)
+      expect(exists).toBe(true)
+
+      // 验证文件权限 - 由于mock环境,我们验证chmod被正确调用
+      const fs = await import('fs')
+      expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
+    })
+
+    it('应该设置正确的文件权限', async () => {
+      const backupFile = await databaseBackup.createBackup()
+
+      console.debug('backupFile', backupFile)
+
+      // 验证chmod被正确调用
+      const fs = await import('fs')
+      expect(vi.mocked(fs.promises.chmod)).toHaveBeenCalledWith(backupFile, 0o600)
+    })
+  })
+
+  describe('Backup Cleanup', () => {
+    it('应该清理旧的备份文件', async () => {
+      const fs = await import('fs')
+
+      // 设置readdir返回测试文件
+      vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'] as any)
+
+      // 设置stat返回不同的时间
+      const now = Date.now()
+      const oldFileTime = now - (8 * 24 * 60 * 60 * 1000) // 8天前(超过保留期)
+      const newFileTime = now - (6 * 24 * 60 * 60 * 1000) // 6天前(在保留期内)
+
+      vi.mocked(fs.promises.stat)
+        .mockResolvedValueOnce({ size: 1024, mtimeMs: oldFileTime, mtime: new Date(oldFileTime), mode: 0o600 } as any)
+        .mockResolvedValueOnce({ size: 1024, mtimeMs: newFileTime, mtime: new Date(newFileTime), mode: 0o600 } as any)
+
+      // 执行清理
+      await databaseBackup.cleanupOldBackups()
+
+      // 验证unlink被正确调用(只针对旧文件)
+      expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledTimes(1)
+      expect(vi.mocked(fs.promises.unlink)).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
+    })
+  })
+
+  describe('Backup Management', () => {
+    it('应该能够检查备份文件是否存在', async () => {
+      const backupFile = await databaseBackup.createBackup()
+
+      const exists = await databaseBackup.backupExists(backupFile)
+      expect(exists).toBe(true)
+
+      // 对于不存在的文件,mock应该返回rejected
+      const fs = await import('fs')
+      vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在'))
+
+      const notExists = await databaseBackup.backupExists('/nonexistent/path.dump')
+      expect(notExists).toBe(false)
+    })
+
+    it('应该能够获取备份文件信息', async () => {
+      const backupFile = await databaseBackup.createBackup()
+
+      const info = await databaseBackup.getBackupInfo(backupFile)
+
+      expect(info).toHaveProperty('size')
+      expect(info).toHaveProperty('mtime')
+      expect(info).toHaveProperty('formattedSize')
+      expect(info.formattedSize).toBe('1 KB') // mock stat返回1024字节
+    })
+  })
+
+  describe('Scheduled Backups', () => {
+    it('应该启动和停止定时备份', async () => {
+      const cron = await import('node-cron')
+
+      databaseBackup.startScheduledBackups()
+      expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
+
+      databaseBackup.stopScheduledBackups()
+      // 验证schedule方法被调用
+      expect(vi.mocked(cron.default.schedule)).toHaveBeenCalledTimes(1)
+
+      // 验证返回的mock实例的stop方法被调用
+      const mockCalls = vi.mocked(cron.default.schedule).mock.calls
+      const mockReturnValue = vi.mocked(cron.default.schedule).mock.results[0]?.value
+      if (mockReturnValue) {
+        expect(mockReturnValue.stop).toHaveBeenCalled()
+      } else {
+        // 如果mock没有正确返回实例,至少验证schedule被调用
+        expect(mockCalls.length).toBe(1)
+      }
+    })
+
+    it('应该返回备份状态', () => {
+      databaseBackup.startScheduledBackups()
+
+      const status = databaseBackup.getBackupStatus()
+
+      expect(status).toHaveProperty('scheduled')
+      expect(status).toHaveProperty('nextRun')
+      expect(status).toHaveProperty('lastRun')
+      expect(status.scheduled).toBe(true)
+    })
+  })
+
+  describe('Restore Integration', () => {
+    it('应该能够找到最新备份', async () => {
+      const fs = await import('fs')
+
+      // 设置readdir返回测试文件(字符串数组)
+      vi.mocked(fs.promises.readdir).mockResolvedValue([
+        'backup-2024-01-01T00-00-00Z.dump',
+        'backup-2024-01-03T00-00-00Z.dump',
+      ] as any)
+
+      const latestBackup = await databaseRestore.findLatestBackup()
+      expect(latestBackup).toBeDefined()
+      expect(latestBackup).toBe(path.join('./backups', 'backup-2024-01-03T00-00-00Z.dump'))
+    })
+
+    it('应该能够列出所有备份', async () => {
+      const fs = await import('fs')
+
+      // 设置readdir返回测试文件(字符串数组)
+      vi.mocked(fs.promises.readdir).mockResolvedValue([
+        'backup-1.dump',
+        'backup-2.dump',
+        'other-file.txt'
+      ] as any)
+
+      const backups = await databaseRestore.listBackups()
+      expect(backups).toEqual(['backup-1.dump', 'backup-2.dump'])
+    })
+  })
+})

+ 653 - 0
packages/server/tests/integration/files.integration.test.ts

@@ -0,0 +1,653 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { FileService } from '../../src/modules/files/file.service';
+import { authMiddleware } from '../../src/middleware/auth.middleware';
+import { fileApiRoutes } from '../../src/api';
+import { ConcreteCrudService } from '../../src/utils/concrete-crud.service';
+
+vi.mock('../../src/modules/files/file.service');
+vi.mock('../../src/middleware/auth.middleware');
+vi.mock('../../src/data-source');
+vi.mock('../../src/utils/concrete-crud.service');
+
+describe('File API Integration Tests', () => {
+  let client: ReturnType<typeof testClient<typeof fileApiRoutes>>['api']['v1'];
+  const user1 = {
+    id: 1,
+    username: 'testuser',
+    password: 'password123',
+    phone: null,
+    email: null,
+    nickname: null,
+    name: null,
+    avatarFileId: null,
+    avatarFile: null,
+    isDisabled: 0,
+    isDeleted: 0,
+    registrationSource: 'web',
+    roles: [],
+    createdAt: new Date(),
+    updatedAt: new Date()
+  };
+  const user1Response = {
+    ...user1,
+    createdAt: (user1.createdAt).toISOString(),
+    updatedAt: (user1.updatedAt).toISOString()
+  }
+
+  beforeEach(async () => {
+    vi.clearAllMocks();
+
+    // Mock auth middleware to bypass authentication
+    vi.mocked(authMiddleware).mockImplementation(async (c: any, next: any) => {
+      const authHeader = c.req.header('Authorization');
+      if (!authHeader) {
+        return c.json({ message: 'Authorization header missing' }, 401);
+      }
+      c.set('user', user1)
+      await next();
+    });
+
+
+
+    client = testClient(fileApiRoutes).api.v1;
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('POST /api/v1/files/upload-policy', () => {
+    it('should generate upload policy successfully', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        path: '/uploads/test.txt',
+        description: 'Test file',
+        uploadUserId: 1
+      };
+
+      const mockResponse = {
+        file: {
+          id: 1,
+          ...mockFileData,
+          path: '1/test-uuid-123-test.txt',
+          uploadTime: (new Date()).toISOString(),
+          createdAt: (new Date()).toISOString(),
+          updatedAt: (new Date()).toISOString(),
+          fullUrl: 'https://minio.example.com/d8dai/1/test-uuid-123-test.txt',
+          uploadUser: user1Response,
+          lastUpdated: null
+        },
+        uploadPolicy: {
+          'x-amz-algorithm': 'AWS4-HMAC-SHA256',
+          'x-amz-credential': 'test-credential',
+          'x-amz-date': '20250101T120000Z',
+          'x-amz-security-token': 'test-token',
+          policy: 'test-policy',
+          'x-amz-signature': 'test-signature',
+          host: 'https://minio.example.com',
+          key: '1/test-uuid-123-test.txt',
+          bucket: 'd8dai'
+        }
+      };
+
+
+      const mockCreateFile = vi.fn().mockResolvedValue(mockResponse);
+      vi.mocked(FileService).mockImplementation(() => ({
+        createFile: mockCreateFile
+      } as unknown as FileService));
+      const response = await client.files['upload-policy'].$post({
+        json: mockFileData
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      if (response.status !== 200) {
+        const error = await response.json();
+        console.debug('Error response:', JSON.stringify(error, null, 2));
+        console.debug('Response status:', response.status);
+      }
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual(mockResponse);
+      expect(mockCreateFile).toHaveBeenCalledWith({
+        ...mockFileData,
+        uploadTime: expect.any(Date),
+        uploadUserId: 1
+      });
+    });
+
+    it('should return 400 for invalid request data', async () => {
+      const invalidData = {
+        name: '', // Empty name
+        type: 'text/plain'
+      };
+
+      const response = await client.files['upload-policy'].$post({
+        json: invalidData as any
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+
+    it('should handle service errors gracefully', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        path: '/uploads/test.txt',
+        uploadUserId: 1
+      };
+
+      const mockCreateFile = vi.fn().mockRejectedValue(new Error('Service error'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        createFile: mockCreateFile
+      } as unknown as FileService));
+
+      const response = await client.files['upload-policy'].$post({
+        json: mockFileData as any
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(500);
+    });
+  });
+
+  describe('GET /api/v1/files/{id}/url', () => {
+    it('should generate file access URL successfully', async () => {
+      const mockUrl = 'https://minio.example.com/presigned-url';
+      const mockGetFileUrl = vi.fn().mockResolvedValue(mockUrl);
+      vi.mocked(FileService).mockImplementation(() => ({
+        getFileUrl: mockGetFileUrl
+      } as unknown as FileService));
+
+      const response = await client.files[':id']['url'].$get({
+        param: { id: 1 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({ url: mockUrl });
+    });
+
+    it('should return 404 when file not found', async () => {
+      const mockGetFileUrl = vi.fn().mockRejectedValue(new Error('文件不存在'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        getFileUrl: mockGetFileUrl
+      } as unknown as FileService));
+
+      const response = await client.files[':id']['url'].$get({
+        param: { id: 999 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('GET /api/v1/files/{id}/download', () => {
+    it('should generate file download URL successfully', async () => {
+      const mockDownloadInfo = {
+        url: 'https://minio.example.com/download-url',
+        filename: 'test.txt'
+      };
+      const mockGetFileDownloadUrl = vi.fn().mockResolvedValue(mockDownloadInfo);
+      vi.mocked(FileService).mockImplementation(() => ({
+        getFileDownloadUrl: mockGetFileDownloadUrl
+      } as unknown as FileService));
+
+      const response = await client.files[':id']['download'].$get({
+        param: { id: 1 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual(mockDownloadInfo);
+      expect(mockGetFileDownloadUrl).toHaveBeenCalledWith(1);
+    });
+
+    it('should return 404 when file not found for download', async () => {
+      const mockGetFileDownloadUrl = vi.fn().mockRejectedValue(new Error('文件不存在'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        getFileDownloadUrl: mockGetFileDownloadUrl
+      } as unknown as FileService));
+
+      const response = await client.files[':id']['download'].$get({
+        param: { id: 999 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('DELETE /api/v1/files/{id}', () => {
+    it('should delete file successfully', async () => {
+      const mockDeleteFile = vi.fn().mockResolvedValue(true);
+      vi.mocked(FileService).mockImplementation(() => ({
+        deleteFile: mockDeleteFile
+      } as unknown as FileService));
+
+      const response = await client.files[':id'].$delete({
+        param: { id: 1 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({ success: true, message: '文件删除成功' });
+      expect(mockDeleteFile).toHaveBeenCalledWith(1);
+    });
+
+    it('should return 404 when file not found for deletion', async () => {
+      const mockDeleteFile = vi.fn().mockRejectedValue(new Error('文件不存在'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        deleteFile: mockDeleteFile
+      } as unknown as FileService));
+
+      const response = await client.files[':id'].$delete({
+        param: { id: 999 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+
+    it('should handle deletion errors', async () => {
+      const mockDeleteFile = vi.fn().mockRejectedValue(new Error('删除失败'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        deleteFile: mockDeleteFile
+      } as unknown as FileService));
+
+      const response = await client.files[':id'].$delete({
+        param: { id: 1 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(500);
+    });
+  });
+
+  describe('POST /api/v1/files/multipart-policy', () => {
+    it('should generate multipart upload policy successfully', async () => {
+      const mockRequestData = {
+        fileKey: 'large-file.zip',
+        totalSize: 1024 * 1024 * 100, // 100MB
+        partSize: 1024 * 1024 * 20, // 20MB
+        name: 'large-file.zip',
+        type: 'application/zip',
+        uploadUserId: 1
+      };
+
+      const mockServiceResponse = {
+        file: {
+          id: 1,
+          name: 'large-file.zip',
+          type: 'application/zip',
+          size: 104857600,
+          uploadUserId: 1,
+          path: '1/test-uuid-123-large-file.zip',
+          description: null,
+          uploadTime: new Date(),
+          lastUpdated: null,
+          createdAt: new Date(),
+          updatedAt: new Date(),
+          fullUrl: Promise.resolve('https://minio.example.com/d8dai/1/test-uuid-123-large-file.zip')
+        },
+        uploadId: 'upload-123',
+        uploadUrls: ['url1', 'url2', 'url3', 'url4', 'url5'],
+        bucket: 'd8dai',
+        key: '1/test-uuid-123-large-file.zip'
+      };
+
+      const mockCreateMultipartUploadPolicy = vi.fn().mockResolvedValue(mockServiceResponse);
+      vi.mocked(FileService).mockImplementation(() => ({
+        createMultipartUploadPolicy: mockCreateMultipartUploadPolicy
+      } as unknown as FileService));
+
+      const response = await client.files['multipart-policy'].$post({
+        json: mockRequestData
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-uuid-123-large-file.zip',
+        host: 'http://undefined:undefined',
+        partUrls: ['url1', 'url2', 'url3', 'url4', 'url5']
+      });
+      expect(mockCreateMultipartUploadPolicy).toHaveBeenCalledWith(
+        {
+          fileKey: 'large-file.zip',
+          totalSize: 104857600,
+          partSize: 20971520,
+          name: 'large-file.zip',
+          type: 'application/zip',
+          uploadUserId: 1
+        },
+        5
+      );
+    });
+
+    it('should validate multipart policy request data', async () => {
+      const invalidData = {
+        name: 'test.zip'
+        // Missing required fields: fileKey, totalSize, partSize
+      };
+
+      const response = await client.files['multipart-policy'].$post({
+        json: invalidData as any
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('POST /api/v1/files/multipart-complete', () => {
+    it('should complete multipart upload successfully', async () => {
+      const mockCompleteData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.zip',
+        parts: [
+          { partNumber: 1, etag: 'etag1' },
+          { partNumber: 2, etag: 'etag2' }
+        ]
+      };
+
+      const mockResponse = {
+        fileId: 1,
+        url: 'https://minio.example.com/file.zip',
+        key: '1/test-file.zip',
+        size: 2048,
+        host: 'http://undefined:undefined',
+        bucket: 'd8dai'
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockResponse);
+      vi.mocked(FileService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as FileService));
+
+      const response = await client.files['multipart-complete'].$post({
+        json: mockCompleteData
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual(mockResponse);
+      expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(mockCompleteData);
+    });
+
+    it('should validate complete multipart request data', async () => {
+      const invalidData = {
+        uploadId: 'upload-123',
+        // Missing required fields: bucket, key, parts
+      };
+
+      const response = await client.files['multipart-complete'].$post({
+        json: invalidData as any
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+
+    it('should handle completion errors', async () => {
+      const completeData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.zip',
+        parts: [{ partNumber: 1, etag: 'etag1' }]
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
+      vi.mocked(FileService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as FileService));
+
+      const response = await client.files['multipart-complete'].$post({
+        json: completeData
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(500);
+    });
+  });
+
+  describe('CRUD Operations', () => {
+    it('should list files successfully', async () => {
+      const mockFiles = [
+        {
+          id: 1,
+          name: 'file1.txt',
+          type: 'text/plain',
+          size: 1024,
+          path: '/uploads/file1.txt',
+          fullUrl: 'https://minio.example.com/d8dai/uploads/file1.txt',
+          description: null,
+          uploadUserId: 1,
+          uploadUser: user1Response,
+          uploadTime: new Date(),
+          lastUpdated: null,
+          createdAt: new Date(),
+          updatedAt: new Date()
+        },
+        {
+          id: 2,
+          name: 'file2.txt',
+          type: 'text/plain',
+          size: 2048,
+          path: '/uploads/file2.txt',
+          fullUrl: 'https://minio.example.com/d8dai/uploads/file2.txt',
+          description: null,
+          uploadUserId: 1,
+          uploadUser: user1Response,
+          uploadTime: new Date(),
+          lastUpdated: null,
+          createdAt: new Date(),
+          updatedAt: new Date()
+        }
+      ];
+
+      // 设置ConcreteCrudService的mock返回数据
+      vi.mocked(ConcreteCrudService).mockImplementation(() => ({
+        getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length])
+      } as unknown as ConcreteCrudService<any>));
+
+      const response = await client.files.$get({
+        query: {}
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      if (response.status !== 200) {
+        const error = await response.json();
+        console.debug('Error response:', JSON.stringify(error, null, 2));
+        console.debug('Response status:', response.status);
+      }
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({
+        data: mockFiles.map(file => ({
+          ...file,
+          createdAt: file.createdAt.toISOString(),
+          updatedAt: file.updatedAt.toISOString(),
+          uploadTime: file.uploadTime.toISOString()
+        })),
+        pagination: {
+          current: 1,
+          pageSize: 10,
+          total: mockFiles.length
+        }
+      });
+    });
+
+    it('should get file by ID successfully', async () => {
+      const mockFile = {
+        id: 1,
+        name: 'file.txt',
+        type: 'text/plain',
+        size: 1024,
+        path: '/uploads/file.txt',
+        fullUrl: 'https://minio.example.com/d8dai/uploads/file.txt',
+        description: null,
+        uploadUserId: 1,
+        uploadUser: user1Response,
+        uploadTime: new Date(),
+        lastUpdated: null,
+        createdAt: new Date(),
+        updatedAt: new Date()
+      };
+
+      // 设置ConcreteCrudService的mock返回数据
+      vi.mocked(ConcreteCrudService).mockImplementation(() => ({
+        getById: vi.fn().mockResolvedValue(mockFile)
+      } as unknown as ConcreteCrudService<any>));
+
+      const response = await client.files[':id'].$get({
+        param: { id: 1 }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      if (response.status !== 200) {
+        const error = await response.json();
+        console.debug('Error response:', JSON.stringify(error, null, 2));
+        console.debug('Response status:', response.status);
+      }
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({
+        ...mockFile,
+        createdAt: mockFile.createdAt.toISOString(),
+        updatedAt: mockFile.updatedAt.toISOString(),
+        uploadTime: mockFile.uploadTime.toISOString()
+      });
+    });
+
+    it('should search files successfully', async () => {
+      const mockFiles = [
+        {
+          id: 1,
+          name: 'document.pdf',
+          type: 'application/pdf',
+          size: 1024,
+          path: '/uploads/document.pdf',
+          fullUrl: 'https://minio.example.com/d8dai/uploads/document.pdf',
+          description: null,
+          uploadUserId: 1,
+          uploadUser: user1Response,
+          uploadTime: new Date(),
+          lastUpdated: null,
+          createdAt: new Date(),
+          updatedAt: new Date()
+        }
+      ];
+
+      // 设置ConcreteCrudService的mock返回数据
+      vi.mocked(ConcreteCrudService).mockImplementation(() => ({
+        getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length])
+      } as unknown as ConcreteCrudService<any>));
+
+      const response = await client.files.$get({
+        query: { keyword: 'document' }
+      },
+      {
+        headers: {
+          'Authorization': 'Bearer test-token'
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const result = await response.json();
+      expect(result).toEqual({
+        data: mockFiles.map(file => ({
+          ...file,
+          createdAt: file.createdAt.toISOString(),
+          updatedAt: file.updatedAt.toISOString(),
+          uploadTime: file.uploadTime.toISOString()
+        })),
+        pagination: {
+          current: 1,
+          pageSize: 10,
+          total: mockFiles.length
+        }
+      });
+      expect(vi.mocked(ConcreteCrudService).mock.results[0].value.getList).toHaveBeenCalledWith(1, 10, 'document', ['name', 'type', 'description'], undefined, ['uploadUser'], { id: 'DESC' }, undefined);
+    });
+  });
+});

+ 302 - 0
packages/server/tests/integration/minio.integration.test.ts

@@ -0,0 +1,302 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { MinioService } from '../../src/modules/files/minio.service';
+import { Client } from 'minio';
+import { logger } from '../../src/utils/logger';
+
+// Mock dependencies
+vi.mock('minio');
+vi.mock('../../src/utils/logger');
+
+// Mock process.env using vi.stubEnv for proper isolation
+beforeEach(() => {
+  vi.stubEnv('MINIO_HOST', 'localhost');
+  vi.stubEnv('MINIO_PORT', '9000');
+  vi.stubEnv('MINIO_USE_SSL', 'false');
+  vi.stubEnv('MINIO_ACCESS_KEY', 'minioadmin');
+  vi.stubEnv('MINIO_SECRET_KEY', 'minioadmin');
+  vi.stubEnv('MINIO_BUCKET_NAME', 'test-bucket');
+});
+
+afterEach(() => {
+  vi.unstubAllEnvs();
+});
+
+describe('MinIO Integration Tests', () => {
+  let minioService: MinioService;
+  let mockClient: Client;
+
+  beforeEach(() => {
+    mockClient = new Client({} as any);
+    (Client as any).mockClear();
+    (Client as any).mockImplementation(() => mockClient);
+
+    // Create MinioService with mock client
+    minioService = new MinioService();
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('Bucket Operations', () => {
+    it('should ensure bucket exists and set policy', async () => {
+      // Mock bucket doesn't exist
+      mockClient.bucketExists = vi.fn().mockResolvedValue(false);
+      mockClient.makeBucket = vi.fn().mockResolvedValue(undefined);
+      mockClient.setBucketPolicy = vi.fn().mockResolvedValue(undefined);
+
+      const result = await minioService.ensureBucketExists();
+
+      expect(result).toBe(true);
+      expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.makeBucket).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.setBucketPolicy).toHaveBeenCalled();
+      expect(logger.db).toHaveBeenCalledWith('Created new bucket: test-bucket');
+    });
+
+    it('should handle existing bucket', async () => {
+      mockClient.bucketExists = vi.fn().mockResolvedValue(true);
+
+      const result = await minioService.ensureBucketExists();
+
+      expect(result).toBe(true);
+      expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.makeBucket).not.toHaveBeenCalled();
+      expect(mockClient.setBucketPolicy).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('File Operations', () => {
+    it('should upload and download file successfully', async () => {
+      const testContent = Buffer.from('Hello, MinIO!');
+      const mockUrl = 'http://localhost:9000/test-bucket/test.txt';
+
+      // Mock bucket operations
+      mockClient.bucketExists = vi.fn().mockResolvedValue(true);
+      mockClient.putObject = vi.fn().mockResolvedValue(undefined);
+      mockClient.statObject = vi.fn().mockResolvedValue({ size: testContent.length } as any);
+      mockClient.getObject = vi.fn().mockReturnValue({
+        on: (event: string, callback: Function) => {
+          if (event === 'data') callback(testContent);
+          if (event === 'end') callback();
+        }
+      } as any);
+
+      // Upload file
+      const uploadUrl = await minioService.createObject('test-bucket', 'test.txt', testContent, 'text/plain');
+      expect(uploadUrl).toBe(mockUrl);
+      expect(mockClient.putObject).toHaveBeenCalledWith(
+        'test-bucket',
+        'test.txt',
+        testContent,
+        testContent.length,
+        { 'Content-Type': 'text/plain' }
+      );
+
+      // Check file exists
+      const exists = await minioService.objectExists('test-bucket', 'test.txt');
+      expect(exists).toBe(true);
+      expect(mockClient.statObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
+    });
+
+    it('should handle file not found', async () => {
+      const notFoundError = new Error('Object not found');
+      notFoundError.message = 'not found';
+      mockClient.statObject = vi.fn().mockRejectedValue(notFoundError);
+
+      const exists = await minioService.objectExists('test-bucket', 'nonexistent.txt');
+      expect(exists).toBe(false);
+    });
+
+    it('should delete file successfully', async () => {
+      mockClient.removeObject = vi.fn().mockResolvedValue(undefined);
+
+      await minioService.deleteObject('test-bucket', 'test.txt');
+
+      expect(mockClient.removeObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
+      expect(logger.db).toHaveBeenCalledWith('Deleted object: test-bucket/test.txt');
+    });
+  });
+
+  describe('Presigned URL Operations', () => {
+    it('should generate presigned URLs correctly', async () => {
+      const mockPresignedUrl = 'https://minio.example.com/presigned-url';
+      mockClient.presignedGetObject = vi.fn().mockResolvedValue(mockPresignedUrl);
+
+      // Test regular presigned URL
+      const url = await minioService.getPresignedFileUrl('test-bucket', 'file.txt', 3600);
+      expect(url).toBe(mockPresignedUrl);
+      expect(mockClient.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'file.txt', 3600);
+
+      // Test download URL with content disposition
+      const downloadUrl = await minioService.getPresignedFileDownloadUrl(
+        'test-bucket',
+        'file.txt',
+        '测试文件.txt',
+        1800
+      );
+      expect(downloadUrl).toBe(mockPresignedUrl);
+      expect(mockClient.presignedGetObject).toHaveBeenCalledWith(
+        'test-bucket',
+        'file.txt',
+        1800,
+        {
+          'response-content-disposition': 'attachment; filename="%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt"',
+          'response-content-type': 'application/octet-stream'
+        }
+      );
+    });
+  });
+
+  describe('Multipart Upload Operations', () => {
+    it('should handle multipart upload workflow', async () => {
+      const mockUploadId = 'upload-123';
+      const mockPartUrls = ['url1', 'url2', 'url3'];
+      const mockStat = { size: 3072 };
+
+      // Mock multipart operations
+      mockClient.initiateNewMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
+      mockClient.presignedUrl = vi.fn()
+        .mockResolvedValueOnce('url1')
+        .mockResolvedValueOnce('url2')
+        .mockResolvedValueOnce('url3');
+      mockClient.completeMultipartUpload = vi.fn().mockResolvedValue(undefined);
+      mockClient.statObject = vi.fn().mockResolvedValue(mockStat as any);
+
+      // Create multipart upload
+      const uploadId = await minioService.createMultipartUpload('test-bucket', 'large-file.zip');
+      expect(uploadId).toBe(mockUploadId);
+      expect(mockClient.initiateNewMultipartUpload).toHaveBeenCalledWith(
+        'test-bucket',
+        'large-file.zip',
+        {}
+      );
+
+      // Generate part URLs
+      const partUrls = await minioService.generateMultipartUploadUrls(
+        'test-bucket',
+        'large-file.zip',
+        mockUploadId,
+        3
+      );
+      expect(partUrls).toEqual(mockPartUrls);
+      expect(mockClient.presignedUrl).toHaveBeenCalledTimes(3);
+
+      // Complete multipart upload
+      const parts = [
+        { ETag: 'etag1', PartNumber: 1 },
+        { ETag: 'etag2', PartNumber: 2 },
+        { ETag: 'etag3', PartNumber: 3 }
+      ];
+      const result = await minioService.completeMultipartUpload(
+        'test-bucket',
+        'large-file.zip',
+        mockUploadId,
+        parts
+      );
+      expect(result).toEqual({ size: 3072 });
+      expect(mockClient.completeMultipartUpload).toHaveBeenCalledWith(
+        'test-bucket',
+        'large-file.zip',
+        mockUploadId,
+        [{ part: 1, etag: 'etag1' }, { part: 2, etag: 'etag2' }, { part: 3, etag: 'etag3' }]
+      );
+    });
+  });
+
+  describe('Error Handling', () => {
+    it('should handle MinIO connection errors', async () => {
+      const connectionError = new Error('Connection refused');
+      mockClient.bucketExists = vi.fn().mockRejectedValue(connectionError);
+
+      await expect(minioService.ensureBucketExists()).rejects.toThrow(connectionError);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to ensure bucket exists: test-bucket',
+        connectionError
+      );
+    });
+
+    it('should handle file operation errors', async () => {
+      const operationError = new Error('Operation failed');
+
+      // 确保桶存在成功
+      mockClient.bucketExists = vi.fn().mockResolvedValue(true);
+      // 但文件操作失败
+      mockClient.putObject = vi.fn().mockRejectedValue(operationError);
+
+      await expect(minioService.createObject(
+        'test-bucket',
+        'test.txt',
+        Buffer.from('test'),
+        'text/plain'
+      )).rejects.toThrow(operationError);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to create object test-bucket/test.txt:',
+        operationError
+      );
+    });
+
+    it('should handle permission errors gracefully', async () => {
+      const permissionError = new Error('Permission denied');
+      mockClient.statObject = vi.fn().mockRejectedValue(permissionError);
+
+      await expect(minioService.objectExists('test-bucket', 'file.txt')).rejects.toThrow(permissionError);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Error checking existence of object test-bucket/file.txt:',
+        permissionError
+      );
+    });
+  });
+
+  describe('Configuration Validation', () => {
+    it('should validate MinIO configuration', () => {
+      expect(minioService.bucketName).toBe('test-bucket');
+
+      // Test URL generation with different configurations
+      const url = minioService.getFileUrl('test-bucket', 'file.txt');
+      expect(url).toBe('http://localhost:9000/test-bucket/file.txt');
+    });
+
+    it('should handle SSL configuration', async () => {
+      // Create new instance with SSL
+      vi.stubEnv('MINIO_USE_SSL', 'true');
+      vi.stubEnv('MINIO_PORT', '443');
+
+      const sslService = new MinioService();
+      const url = sslService.getFileUrl('test-bucket', 'file.txt');
+      expect(url).toBe('https://localhost:443/test-bucket/file.txt');
+    });
+  });
+
+  describe('Performance Testing', () => {
+    it('should handle concurrent operations', async () => {
+      mockClient.presignedGetObject = vi.fn().mockResolvedValue('https://minio.example.com/file');
+
+      // Test concurrent URL generation with smaller concurrency
+      const promises = Array(5).fill(0).map((_, i) =>
+        minioService.getPresignedFileUrl('test-bucket', `file${i}.txt`)
+      );
+
+      const results = await Promise.all(promises);
+      expect(results).toHaveLength(5);
+      expect(results.every(url => url === 'https://minio.example.com/file')).toBe(true);
+    });
+
+    it('should handle large file operations', async () => {
+      // Use smaller buffer size to avoid memory issues
+      const largeBuffer = Buffer.alloc(1 * 1024 * 1024); // 1MB instead of 10MB
+      mockClient.bucketExists = vi.fn().mockResolvedValue(true);
+      mockClient.putObject = vi.fn().mockResolvedValue({ etag: 'etag123', versionId: null });
+
+      await minioService.createObject('test-bucket', 'large-file.bin', largeBuffer, 'application/octet-stream');
+
+      expect(mockClient.putObject).toHaveBeenCalledWith(
+        'test-bucket',
+        'large-file.bin',
+        largeBuffer,
+        largeBuffer.length,
+        { 'Content-Type': 'application/octet-stream' }
+      );
+    });
+  });
+});

+ 430 - 0
packages/server/tests/integration/users.integration.test.ts

@@ -0,0 +1,430 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '../utils/integration-test-db';
+import { IntegrationTestAssertions } from '../utils/integration-test-utils';
+import { userRoutes } from '../../src/api';
+import { AuthService } from '../../src/modules/auth/auth.service';
+import { UserService } from '../../src/modules/users/user.service';
+
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('用户API集成测试 (使用hono/testing)', () => {
+  let client: ReturnType<typeof testClient<typeof userRoutes>>['api']['v1'];
+  let testToken: string;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(userRoutes).api.v1;
+
+    // 创建测试用户并生成token
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    const userService = new UserService(dataSource);
+    const authService = new AuthService(userService);
+
+    // 确保admin用户存在
+    const user = await authService.ensureAdminExists();
+
+    // 生成admin用户的token
+    testToken = authService.generateToken(user);
+
+    // 设置默认认证头 - 需要在每个请求中手动添加
+  });
+
+  describe('用户创建测试', () => {
+    it('应该成功创建用户', async () => {
+      const userData = {
+        username: 'testuser_create',
+        email: 'testcreate@example.com',
+        password: 'TestPassword123!',
+        name: 'Test User',
+        phone: '13800138000'
+      };
+
+      const response = await client.users.$post({
+        json: userData,
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 断言响应
+      expect(response.status).toBe(201);
+      if (response.status === 201) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('id');
+        expect(responseData.username).toBe(userData.username);
+        expect(responseData.email).toBe(userData.email);
+        expect(responseData.name).toBe(userData.name);
+
+        // 断言数据库中存在用户
+        await IntegrationTestAssertions.expectUserToExist(userData.username);
+      }
+    });
+
+    it('应该拒绝创建重复用户名的用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 先创建一个用户
+      await TestDataFactory.createTestUser(dataSource, {
+        username: 'duplicate_user'
+      });
+
+      // 尝试创建相同用户名的用户
+      const userData = {
+        username: 'duplicate_user',
+        email: 'different@example.com',
+        password: 'TestPassword123!',
+        name: 'Test User'
+      };
+
+      const response = await client.users.$post({
+        json: userData
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 应该返回错误
+      expect(response.status).toBe(500);
+      if (response.status === 500) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('duplicate key');
+      }
+    });
+
+    it('应该拒绝创建无效邮箱的用户', async () => {
+      const userData = {
+        username: 'testuser_invalid_email',
+        email: 'invalid-email',
+        password: 'TestPassword123!',
+        name: 'Test User'
+      };
+
+      const response = await client.users.$post({
+        json: userData
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 应该返回验证错误或服务器错误
+      // 根据实际实现,可能是400验证错误或500服务器错误
+      expect([400, 500]).toContain(response.status);
+      if (response.status === 400) {
+        const responseData = await response.json();
+        // 检查是否有code属性
+        if (responseData.code !== undefined) {
+          expect(responseData.code).toBe(400);
+        }
+        // 检查是否有message属性
+        if (responseData.message !== undefined) {
+          expect(typeof responseData.message).toBe('string');
+        }
+      } else if (response.status === 500) {
+        const responseData = await response.json();
+        expect(responseData.message).toBeDefined();
+      }
+    });
+  });
+
+  describe('用户读取测试', () => {
+    it('应该成功获取用户列表', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 创建几个测试用户
+      await TestDataFactory.createTestUser(dataSource, { username: 'user1' });
+      await TestDataFactory.createTestUser(dataSource, { username: 'user2' });
+
+      const response = await client.users.$get({
+        query: {}
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(Array.isArray(responseData.data)).toBe(true);
+        expect(responseData.data.length).toBeGreaterThanOrEqual(2);
+      }
+    });
+
+    it('应该成功获取单个用户详情', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'testuser_detail'
+      });
+
+      const response = await client.users[':id'].$get({
+        param: { id: testUser.id }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.id).toBe(testUser.id);
+        expect(responseData.username).toBe(testUser.username);
+        expect(responseData.email).toBe(testUser.email);
+      }
+    });
+
+    it('应该返回404当用户不存在时', async () => {
+      const response = await client.users[':id'].$get({
+        param: { id: 999999 }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+      if (response.status === 404) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('资源不存在');
+      }
+    });
+  });
+
+  describe('用户更新测试', () => {
+    it('应该成功更新用户信息', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'testuser_update'
+      });
+
+      const updateData = {
+        name: 'Updated Name',
+        email: 'updated@example.com'
+      };
+
+      const response = await client.users[':id'].$put({
+        param: { id: testUser.id },
+        json: updateData
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.name).toBe(updateData.name);
+        expect(responseData.email).toBe(updateData.email);
+      }
+
+      // 验证数据库中的更新
+      const getResponse = await client.users[':id'].$get({
+        param: { id: testUser.id }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+      if (getResponse.status === 200) {
+        expect(getResponse.status).toBe(200);
+        const getResponseData = await getResponse.json();
+        expect(getResponseData.name).toBe(updateData.name);
+      }else{
+        const getResponseData = await getResponse.json();
+        process.stderr.write('message:'+ getResponseData.message +"\n");
+      }
+    });
+
+    it('应该返回404当更新不存在的用户时', async () => {
+      const updateData = {
+        name: 'Updated Name',
+        email: 'updated@example.com'
+      };
+
+      const response = await client.users[':id'].$put({
+        param: { id: 999999 },
+        json: updateData
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+      if (response.status === 404) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('资源不存在');
+      }
+    });
+  });
+
+  describe('用户删除测试', () => {
+    it('应该成功删除用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'testuser_delete'
+      });
+
+      const response = await client.users[':id'].$delete({
+        param: { id: testUser.id }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 204);
+
+      // 验证用户已从数据库中删除
+      await IntegrationTestAssertions.expectUserNotToExist('testuser_delete');
+
+      // 验证再次获取用户返回404
+      const getResponse = await client.users[':id'].$get({
+        param: { id: testUser.id }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+      IntegrationTestAssertions.expectStatus(getResponse, 404);
+    });
+
+    it('应该返回404当删除不存在的用户时', async () => {
+      const response = await client.users[':id'].$delete({
+        param: { id: 999999 }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 404);
+      if (response.status === 404) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('资源不存在');
+      }
+    });
+  });
+
+  describe('用户搜索测试', () => {
+    it('应该能够按用户名搜索用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      await TestDataFactory.createTestUser(dataSource, { username: 'search_user_1', email: 'search1@example.com' });
+      await TestDataFactory.createTestUser(dataSource, { username: 'search_user_2', email: 'search2@example.com' });
+      await TestDataFactory.createTestUser(dataSource, { username: 'other_user', email: 'other@example.com' });
+
+      const response = await client.users.$get({
+        query: { keyword: 'search_user' }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(Array.isArray(responseData.data)).toBe(true);
+        expect(responseData.data.length).toBe(2);
+
+        // 验证搜索结果包含正确的用户
+        const usernames = responseData.data.map((user: any) => user.username);
+        expect(usernames).toContain('search_user_1');
+        expect(usernames).toContain('search_user_2');
+        expect(usernames).not.toContain('other_user');
+      }
+    });
+
+    it('应该能够按邮箱搜索用户', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      await TestDataFactory.createTestUser(dataSource, { username: 'user_email_1', email: 'test.email1@example.com' });
+      await TestDataFactory.createTestUser(dataSource, { username: 'user_email_2', email: 'test.email2@example.com' });
+
+      const response = await client.users.$get({
+        query: { keyword: 'test.email' }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.data.length).toBe(2);
+
+        const emails = responseData.data.map((user: any) => user.email);
+        expect(emails).toContain('test.email1@example.com');
+        expect(emails).toContain('test.email2@example.com');
+      }
+    });
+  });
+
+  describe('性能测试', () => {
+    it('用户列表查询响应时间应小于200ms', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 创建一些测试数据
+      for (let i = 0; i < 10; i++) {
+        await TestDataFactory.createTestUser(dataSource, {
+          username: `perf_user_${i}`,
+          email: `perf${i}@example.com`
+        });
+      }
+
+      const startTime = Date.now();
+      const response = await client.users.$get({
+        query: {}
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+      const endTime = Date.now();
+      const responseTime = endTime - startTime;
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
+    });
+  });
+});

+ 17 - 0
packages/server/tests/unit/example.test.ts

@@ -0,0 +1,17 @@
+import { describe, it, expect } from 'vitest'
+
+describe('示例单元测试', () => {
+  it('应该通过基本的数学运算测试', () => {
+    expect(1 + 1).toBe(2)
+  })
+
+  it('应该验证字符串操作', () => {
+    const str = 'hello'
+    expect(str.toUpperCase()).toBe('HELLO')
+  })
+
+  it('应该处理异步操作', async () => {
+    const result = await Promise.resolve(42)
+    expect(result).toBe(42)
+  })
+})

+ 424 - 0
packages/server/tests/unit/modules/file.service.test.ts

@@ -0,0 +1,424 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { DataSource } from 'typeorm';
+import { FileService } from '@/modules/files/file.service';
+import { File } from '@/modules/files/file.entity';
+import { MinioService } from '@/modules/files/minio.service';
+import { logger } from '@/utils/logger';
+
+// Mock dependencies
+vi.mock('@/modules/files/minio.service');
+vi.mock('@/utils/logger');
+vi.mock('uuid', () => ({
+  v4: () => 'test-uuid-123'
+}));
+
+describe('FileService', () => {
+  let mockDataSource: DataSource;
+
+  beforeEach(() => {
+    mockDataSource = {
+      getRepository: vi.fn(() => ({
+        findOneBy: vi.fn(),
+        save: vi.fn()
+      }))
+    } as unknown as DataSource;
+
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('createFile', () => {
+    it('should create file with upload policy successfully', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        uploadUserId: 1
+      };
+
+      const mockUploadPolicy = {
+        'x-amz-algorithm': 'test-algorithm',
+        'x-amz-credential': 'test-credential',
+        host: 'https://minio.example.com'
+      };
+
+      const mockSavedFile = {
+        id: 1,
+        ...mockFileData,
+        path: '1/test-uuid-123-test.txt',
+        uploadTime: new Date(),
+        createdAt: new Date(),
+        updatedAt: new Date()
+      };
+
+
+      const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy);
+      vi.mocked(MinioService).mockImplementation(() => ({
+        generateUploadPolicy: mockGenerateUploadPolicy
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      // Mock GenericCrudService methods
+      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File);
+
+      const result = await fileService.createFile(mockFileData);
+
+      expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt');
+      expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({
+        name: 'test.txt',
+        path: '1/test-uuid-123-test.txt',
+        uploadUserId: 1
+      }));
+      expect(result).toEqual({
+        file: mockSavedFile,
+        uploadPolicy: mockUploadPolicy
+      });
+    });
+
+    it('should handle errors during file creation', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        uploadUserId: 1
+      };
+
+      const mockGenerateUploadPolicy = vi.fn().mockRejectedValue(new Error('MinIO error'));
+      vi.mocked(MinioService).mockImplementation(() => ({
+        generateUploadPolicy: mockGenerateUploadPolicy
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.createFile(mockFileData)).rejects.toThrow('文件创建失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+
+  describe('deleteFile', () => {
+    it('should delete file successfully when file exists', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: 'test-file.txt'
+      } as File;
+
+      const mockObjectExists = vi.fn().mockResolvedValue(true);
+      const mockDeleteObject = vi.fn().mockResolvedValue(undefined);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        objectExists: mockObjectExists,
+        deleteObject: mockDeleteObject,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+      vi.spyOn(fileService, 'delete').mockResolvedValue(true);
+
+      const result = await fileService.deleteFile(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(fileService.delete).toHaveBeenCalledWith(1);
+      expect(result).toBe(true);
+    });
+
+    it('should delete database record even when MinIO file not found', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: 'test-file.txt'
+      } as File;
+
+      const mockObjectExists = vi.fn().mockResolvedValue(false);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        objectExists: mockObjectExists,
+        deleteObject: vi.fn(),
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+      vi.spyOn(fileService, 'delete').mockResolvedValue(true);
+
+      const result = await fileService.deleteFile(1);
+
+      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(fileService.delete).toHaveBeenCalledWith(1);
+      expect(result).toBe(true);
+      expect(logger.error).toHaveBeenCalled();
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('getFileUrl', () => {
+    it('should return file URL successfully', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt'
+      } as File;
+
+      const mockPresignedUrl = 'https://minio.example.com/presigned-url';
+
+      const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        getPresignedFileUrl: mockGetPresignedFileUrl,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+
+      const result = await fileService.getFileUrl(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(result).toBe(mockPresignedUrl);
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('getFileDownloadUrl', () => {
+    it('should return download URL with filename', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: '测试文件.txt'
+      } as File;
+
+      const mockPresignedUrl = 'https://minio.example.com/download-url';
+
+      const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+
+      const result = await fileService.getFileDownloadUrl(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-file.txt',
+        '测试文件.txt'
+      );
+      expect(result).toEqual({
+        url: mockPresignedUrl,
+        filename: '测试文件.txt'
+      });
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('createMultipartUploadPolicy', () => {
+    it('should create multipart upload policy successfully', async () => {
+      const mockFileData = {
+        name: 'large-file.zip',
+        type: 'application/zip',
+        uploadUserId: 1
+      };
+
+      const mockUploadId = 'upload-123';
+      const mockUploadUrls = ['url1', 'url2', 'url3'];
+      const mockSavedFile = {
+        id: 1,
+        ...mockFileData,
+        path: '1/test-uuid-123-large-file.zip',
+        uploadTime: new Date(),
+        createdAt: new Date(),
+        updatedAt: new Date()
+      } as File;
+
+      const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
+      const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        createMultipartUpload: mockCreateMultipartUpload,
+        generateMultipartUploadUrls: mockGenerateMultipartUploadUrls,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile);
+
+      const result = await fileService.createMultipartUploadPolicy(mockFileData, 3);
+
+      expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip');
+      expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-uuid-123-large-file.zip',
+        mockUploadId,
+        3
+      );
+      expect(result).toEqual({
+        file: mockSavedFile,
+        uploadId: mockUploadId,
+        uploadUrls: mockUploadUrls,
+        bucket: 'd8dai',
+        key: '1/test-uuid-123-large-file.zip'
+      });
+    });
+
+    it('should handle errors during multipart upload creation', async () => {
+      const mockFileData = {
+        name: 'large-file.zip',
+        uploadUserId: 1
+      };
+
+      const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error'));
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        createMultipartUpload: mockCreateMultipartUpload,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+
+  describe('completeMultipartUpload', () => {
+    it('should complete multipart upload successfully', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.txt',
+        parts: [
+          { partNumber: 1, etag: 'etag1' },
+          { partNumber: 2, etag: 'etag2' }
+        ]
+      };
+
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        size: 0,
+        updatedAt: new Date()
+      } as File;
+
+      const mockCompleteResult = { size: 2048 };
+      const mockFileUrl = 'https://minio.example.com/file.txt';
+
+      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult);
+      const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload,
+        getFileUrl: mockGetFileUrl
+      } as unknown as MinioService));
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(mockFile),
+        save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File)
+      };
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      const fileService = new FileService(mockDataSource);
+
+      const result = await fileService.completeMultipartUpload(uploadData);
+
+      expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-file.txt',
+        'upload-123',
+        [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }]
+      );
+      expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' });
+      expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
+        size: 2048
+      }));
+      expect(result).toEqual({
+        fileId: 1,
+        url: mockFileUrl,
+        key: '1/test-file.txt',
+        size: 2048
+      });
+    });
+
+    it('should throw error when file record not found', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/nonexistent.txt',
+        parts: [{ partNumber: 1, etag: 'etag1' }]
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 });
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as MinioService));
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(null)
+      };
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在');
+    });
+
+    it('should handle errors during completion', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.txt',
+        parts: [{ partNumber: 1, etag: 'etag1' }]
+      };
+
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        size: 0,
+        updatedAt: new Date()
+      } as File;
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(mockFile),
+        save: vi.fn()
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+});

+ 424 - 0
packages/server/tests/unit/modules/minio.service.test.ts

@@ -0,0 +1,424 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { MinioService } from '@/modules/files/minio.service';
+import { Client } from 'minio';
+import { logger } from '@/utils/logger';
+
+// Mock dependencies
+vi.mock('minio');
+vi.mock('@/utils/logger');
+
+// Mock process.env using vi.stubEnv for proper isolation
+beforeEach(() => {
+  vi.stubEnv('MINIO_HOST', 'localhost');
+  vi.stubEnv('MINIO_PORT', '9000');
+  vi.stubEnv('MINIO_USE_SSL', 'false');
+  vi.stubEnv('MINIO_ACCESS_KEY', 'minioadmin');
+  vi.stubEnv('MINIO_SECRET_KEY', 'minioadmin');
+  vi.stubEnv('MINIO_BUCKET_NAME', 'test-bucket');
+});
+
+afterEach(() => {
+  vi.unstubAllEnvs();
+});
+
+describe('MinioService', () => {
+  let minioService: MinioService;
+  let mockClient: Client;
+
+  beforeEach(() => {
+    mockClient = new Client({} as any);
+    (Client as any).mockClear();
+    (Client as any).mockImplementation(() => mockClient);
+
+    minioService = new MinioService();
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('constructor', () => {
+    it('should initialize with correct configuration', () => {
+      expect(Client).toHaveBeenCalledWith({
+        endPoint: 'localhost',
+        port: 9000,
+        useSSL: false,
+        accessKey: 'minioadmin',
+        secretKey: 'minioadmin'
+      });
+      expect(minioService.bucketName).toBe('test-bucket');
+    });
+  });
+
+  describe('setPublicReadPolicy', () => {
+    it('should set public read policy successfully', async () => {
+      const mockPolicy = JSON.stringify({
+        Version: '2012-10-17',
+        Statement: [
+          {
+            Effect: 'Allow',
+            Principal: { AWS: '*' },
+            Action: ['s3:GetObject'],
+            Resource: ['arn:aws:s3:::test-bucket/*']
+          },
+          {
+            Effect: 'Allow',
+            Principal: { AWS: '*' },
+            Action: ['s3:ListBucket'],
+            Resource: ['arn:aws:s3:::test-bucket']
+          }
+        ]
+      });
+
+      vi.mocked(mockClient.setBucketPolicy).mockResolvedValue(undefined);
+
+      await minioService.setPublicReadPolicy();
+
+      expect(mockClient.setBucketPolicy).toHaveBeenCalledWith('test-bucket', mockPolicy);
+      expect(logger.db).toHaveBeenCalledWith('Bucket policy set to public read for: test-bucket');
+    });
+
+    it('should handle errors when setting policy', async () => {
+      const error = new Error('Policy error');
+      vi.mocked(mockClient.setBucketPolicy).mockRejectedValue(error);
+
+      await expect(minioService.setPublicReadPolicy()).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith('Failed to set bucket policy for test-bucket:', error);
+    });
+  });
+
+  describe('ensureBucketExists', () => {
+    it('should create bucket if not exists', async () => {
+      vi.mocked(mockClient.bucketExists).mockResolvedValue(false);
+      vi.mocked(mockClient.makeBucket).mockResolvedValue(undefined);
+      vi.spyOn(minioService, 'setPublicReadPolicy').mockResolvedValue(undefined);
+
+      const result = await minioService.ensureBucketExists();
+
+      expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.makeBucket).toHaveBeenCalledWith('test-bucket');
+      expect(minioService.setPublicReadPolicy).toHaveBeenCalledWith('test-bucket');
+      expect(result).toBe(true);
+      expect(logger.db).toHaveBeenCalledWith('Created new bucket: test-bucket');
+    });
+
+    it('should return true if bucket already exists', async () => {
+      vi.mocked(mockClient.bucketExists).mockResolvedValue(true);
+
+      const result = await minioService.ensureBucketExists();
+
+      expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.makeBucket).not.toHaveBeenCalled();
+      expect(result).toBe(true);
+    });
+
+    it('should handle errors during bucket check', async () => {
+      const error = new Error('Bucket check failed');
+      vi.mocked(mockClient.bucketExists).mockRejectedValue(error);
+
+      await expect(minioService.ensureBucketExists()).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith('Failed to ensure bucket exists: test-bucket', error);
+    });
+  });
+
+  describe('generateUploadPolicy', () => {
+    it('should generate upload policy successfully', async () => {
+      const fileKey = 'test-file.txt';
+      const mockPolicy = {
+        setBucket: vi.fn(),
+        setKey: vi.fn(),
+        setExpires: vi.fn()
+      };
+      const mockFormData = {
+        'x-amz-algorithm': 'AWS4-HMAC-SHA256',
+        'x-amz-credential': 'credential',
+        'x-amz-date': '20250101T120000Z',
+        policy: 'policy-string',
+        'x-amz-signature': 'signature'
+      };
+
+      vi.spyOn(minioService, 'ensureBucketExists').mockResolvedValue(true);
+      vi.mocked(mockClient.newPostPolicy).mockReturnValue(mockPolicy as any);
+      vi.mocked(mockClient.presignedPostPolicy).mockResolvedValue({
+        postURL: 'https://minio.example.com',
+        formData: mockFormData
+      });
+
+      const result = await minioService.generateUploadPolicy(fileKey);
+
+      expect(minioService.ensureBucketExists).toHaveBeenCalled();
+      expect(mockClient.newPostPolicy).toHaveBeenCalled();
+      expect(mockPolicy.setBucket).toHaveBeenCalledWith('test-bucket');
+      expect(mockPolicy.setKey).toHaveBeenCalledWith(fileKey);
+      expect(mockPolicy.setExpires).toHaveBeenCalledWith(expect.any(Date));
+      expect(mockClient.presignedPostPolicy).toHaveBeenCalledWith(mockPolicy);
+      expect(result).toEqual({
+        'x-amz-algorithm': 'AWS4-HMAC-SHA256',
+        'x-amz-credential': 'credential',
+        'x-amz-date': '20250101T120000Z',
+        'x-amz-security-token': undefined,
+        policy: 'policy-string',
+        'x-amz-signature': 'signature',
+        host: 'https://minio.example.com',
+        key: fileKey,
+        bucket: 'test-bucket'
+      });
+    });
+  });
+
+  describe('getFileUrl', () => {
+    it('should generate correct file URL without SSL', () => {
+      const url = minioService.getFileUrl('test-bucket', 'file.txt');
+      expect(url).toBe('http://localhost:9000/test-bucket/file.txt');
+    });
+
+    it('should generate correct file URL with SSL', async () => {
+      // Create new instance with SSL by temporarily overriding env vars
+      vi.stubEnv('MINIO_USE_SSL', 'true');
+      vi.stubEnv('MINIO_PORT', '443');
+
+      const sslService = new MinioService();
+      const url = sslService.getFileUrl('test-bucket', 'file.txt');
+      expect(url).toBe('https://localhost:443/test-bucket/file.txt');
+    });
+  });
+
+  describe('getPresignedFileUrl', () => {
+    it('should generate presigned URL successfully', async () => {
+      const mockUrl = 'https://minio.example.com/presigned-url';
+      vi.mocked(mockClient.presignedGetObject).mockResolvedValue(mockUrl);
+
+      const result = await minioService.getPresignedFileUrl('test-bucket', 'file.txt', 3600);
+
+      expect(mockClient.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'file.txt', 3600);
+      expect(result).toBe(mockUrl);
+      expect(logger.db).toHaveBeenCalledWith(
+        'Generated presigned URL for test-bucket/file.txt, expires in 3600s'
+      );
+    });
+
+    it('should handle errors during URL generation', async () => {
+      const error = new Error('URL generation failed');
+      vi.mocked(mockClient.presignedGetObject).mockRejectedValue(error);
+
+      await expect(minioService.getPresignedFileUrl('test-bucket', 'file.txt')).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to generate presigned URL for test-bucket/file.txt:',
+        error
+      );
+    });
+  });
+
+  describe('getPresignedFileDownloadUrl', () => {
+    it('should generate download URL with content disposition', async () => {
+      const mockUrl = 'https://minio.example.com/download-url';
+      vi.mocked(mockClient.presignedGetObject).mockResolvedValue(mockUrl);
+
+      const result = await minioService.getPresignedFileDownloadUrl(
+        'test-bucket',
+        'file.txt',
+        '测试文件.txt',
+        1800
+      );
+
+      expect(mockClient.presignedGetObject).toHaveBeenCalledWith(
+        'test-bucket',
+        'file.txt',
+        1800,
+        {
+          'response-content-disposition': 'attachment; filename="%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt"',
+          'response-content-type': 'application/octet-stream'
+        }
+      );
+      expect(result).toBe(mockUrl);
+      expect(logger.db).toHaveBeenCalledWith(
+        'Generated presigned download URL for test-bucket/file.txt, filename: 测试文件.txt'
+      );
+    });
+  });
+
+  describe('createMultipartUpload', () => {
+    it('should create multipart upload successfully', async () => {
+      const mockUploadId = 'upload-123';
+      vi.mocked(mockClient.initiateNewMultipartUpload).mockResolvedValue(mockUploadId);
+
+      const result = await minioService.createMultipartUpload('test-bucket', 'large-file.zip');
+
+      expect(mockClient.initiateNewMultipartUpload).toHaveBeenCalledWith(
+        'test-bucket',
+        'large-file.zip',
+        {}
+      );
+      expect(result).toBe(mockUploadId);
+      expect(logger.db).toHaveBeenCalledWith(
+        'Created multipart upload for large-file.zip with ID: upload-123'
+      );
+    });
+
+    it('should handle errors during multipart upload creation', async () => {
+      const error = new Error('Upload creation failed');
+      vi.mocked(mockClient.initiateNewMultipartUpload).mockRejectedValue(error);
+
+      await expect(minioService.createMultipartUpload('test-bucket', 'file.zip')).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to create multipart upload for file.zip:',
+        error
+      );
+    });
+  });
+
+  describe('generateMultipartUploadUrls', () => {
+    it('should generate multipart upload URLs', async () => {
+      const mockUrls = ['url1', 'url2', 'url3'];
+      vi.mocked(mockClient.presignedUrl)
+        .mockResolvedValueOnce('url1')
+        .mockResolvedValueOnce('url2')
+        .mockResolvedValueOnce('url3');
+
+      const result = await minioService.generateMultipartUploadUrls(
+        'test-bucket',
+        'large-file.zip',
+        'upload-123',
+        3
+      );
+
+      expect(mockClient.presignedUrl).toHaveBeenCalledTimes(3);
+      expect(mockClient.presignedUrl).toHaveBeenNthCalledWith(
+        1,
+        'put',
+        'test-bucket',
+        'large-file.zip',
+        3600,
+        { uploadId: 'upload-123', partNumber: '1' }
+      );
+      expect(result).toEqual(mockUrls);
+    });
+  });
+
+  describe('completeMultipartUpload', () => {
+    it('should complete multipart upload successfully', async () => {
+      const parts = [
+        { ETag: 'etag1', PartNumber: 1 },
+        { ETag: 'etag2', PartNumber: 2 }
+      ];
+      const mockStat = { size: 2048 };
+
+      vi.mocked(mockClient.completeMultipartUpload).mockResolvedValue({ etag: 'etag123', versionId: null });
+      vi.mocked(mockClient.statObject).mockResolvedValue(mockStat as any);
+
+      const result = await minioService.completeMultipartUpload(
+        'test-bucket',
+        'large-file.zip',
+        'upload-123',
+        parts
+      );
+
+      expect(mockClient.completeMultipartUpload).toHaveBeenCalledWith(
+        'test-bucket',
+        'large-file.zip',
+        'upload-123',
+        [{ part: 1, etag: 'etag1' }, { part: 2, etag: 'etag2' }]
+      );
+      expect(mockClient.statObject).toHaveBeenCalledWith('test-bucket', 'large-file.zip');
+      expect(result).toEqual({ size: 2048 });
+      expect(logger.db).toHaveBeenCalledWith(
+        'Completed multipart upload for large-file.zip with ID: upload-123'
+      );
+    });
+
+    it('should handle errors during completion', async () => {
+      const error = new Error('Completion failed');
+      vi.mocked(mockClient.completeMultipartUpload).mockRejectedValue(error);
+
+      await expect(minioService.completeMultipartUpload(
+        'test-bucket',
+        'file.zip',
+        'upload-123',
+        [{ ETag: 'etag1', PartNumber: 1 }]
+      )).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to complete multipart upload for file.zip:',
+        error
+      );
+    });
+  });
+
+  describe('createObject', () => {
+    it('should create object successfully', async () => {
+      const fileContent = Buffer.from('test content');
+      const mockUrl = 'http://localhost:9000/test-bucket/file.txt';
+
+      vi.spyOn(minioService, 'ensureBucketExists').mockResolvedValue(true);
+      vi.mocked(mockClient.putObject).mockResolvedValue({ etag: 'etag123', versionId: null });
+      vi.spyOn(minioService, 'getFileUrl').mockReturnValue(mockUrl);
+
+      const result = await minioService.createObject(
+        'test-bucket',
+        'file.txt',
+        fileContent,
+        'text/plain'
+      );
+
+      expect(minioService.ensureBucketExists).toHaveBeenCalledWith('test-bucket');
+      expect(mockClient.putObject).toHaveBeenCalledWith(
+        'test-bucket',
+        'file.txt',
+        fileContent,
+        fileContent.length,
+        { 'Content-Type': 'text/plain' }
+      );
+      expect(result).toBe(mockUrl);
+      expect(logger.db).toHaveBeenCalledWith('Created object: test-bucket/file.txt');
+    });
+  });
+
+  describe('objectExists', () => {
+    it('should return true when object exists', async () => {
+      vi.mocked(mockClient.statObject).mockResolvedValue({} as any);
+
+      const result = await minioService.objectExists('test-bucket', 'file.txt');
+      expect(result).toBe(true);
+    });
+
+    it('should return false when object not found', async () => {
+      const error = new Error('Object not found');
+      vi.mocked(mockClient.statObject).mockRejectedValue(error);
+
+      const result = await minioService.objectExists('test-bucket', 'nonexistent.txt');
+      expect(result).toBe(false);
+    });
+
+    it('should rethrow other errors', async () => {
+      const error = new Error('Permission denied');
+      vi.mocked(mockClient.statObject).mockRejectedValue(error);
+
+      await expect(minioService.objectExists('test-bucket', 'file.txt')).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Error checking existence of object test-bucket/file.txt:',
+        error
+      );
+    });
+  });
+
+  describe('deleteObject', () => {
+    it('should delete object successfully', async () => {
+      vi.mocked(mockClient.removeObject).mockResolvedValue(undefined);
+
+      await minioService.deleteObject('test-bucket', 'file.txt');
+
+      expect(mockClient.removeObject).toHaveBeenCalledWith('test-bucket', 'file.txt');
+      expect(logger.db).toHaveBeenCalledWith('Deleted object: test-bucket/file.txt');
+    });
+
+    it('should handle errors during deletion', async () => {
+      const error = new Error('Deletion failed');
+      vi.mocked(mockClient.removeObject).mockRejectedValue(error);
+
+      await expect(minioService.deleteObject('test-bucket', 'file.txt')).rejects.toThrow(error);
+      expect(logger.error).toHaveBeenCalledWith(
+        'Failed to delete object test-bucket/file.txt:',
+        error
+      );
+    });
+  });
+});

+ 144 - 0
packages/server/tests/unit/modules/user.service.test.ts

@@ -0,0 +1,144 @@
+import { UserService } from '@/modules/users/user.service';
+import { UserEntity as User } from '@/modules/users/user.entity';
+import * as bcrypt from 'bcrypt';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+
+// Mock TypeORM 数据源和仓库
+vi.mock('typeorm', async (importOriginal) => {
+  const actual = await importOriginal() as any
+  return {
+    ...actual,
+    DataSource: vi.fn().mockImplementation(() => ({
+      getRepository: vi.fn()
+    })),
+    Repository: vi.fn()
+  }
+});
+
+// Mock bcrypt
+vi.mock('bcrypt', () => ({
+  hash: vi.fn().mockResolvedValue('hashed_password'),
+  compare: vi.fn().mockResolvedValue(true)
+}));
+
+describe('UserService', () => {
+  let userService: UserService;
+  let mockDataSource: any;
+  let mockUserRepository: any;
+  let mockRoleRepository: any;
+
+  beforeEach(() => {
+    // 创建模拟的仓库实例
+    mockUserRepository = {
+      create: vi.fn(),
+      save: vi.fn(),
+      findOne: vi.fn(),
+      update: vi.fn(),
+      delete: vi.fn(),
+      createQueryBuilder: vi.fn(),
+      find: vi.fn(),
+      findByIds: vi.fn()
+    } as any;
+
+    mockRoleRepository = {
+      findByIds: vi.fn()
+    } as any;
+
+    // 创建模拟的数据源
+    mockDataSource = {
+      getRepository: vi.fn()
+    } as any;
+
+    // 设置数据源返回模拟的仓库
+    mockDataSource.getRepository
+      .mockReturnValueOnce(mockUserRepository)
+      .mockReturnValueOnce(mockRoleRepository);
+
+    userService = new UserService(mockDataSource);
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('createUser', () => {
+    it('应该成功创建用户并哈希密码', async () => {
+      const userData = {
+        username: 'testuser',
+        password: 'password123',
+        email: 'test@example.com'
+      };
+
+      const mockUser = { id: 1, ...userData, password: 'hashed_password' } as User;
+
+      mockUserRepository.create.mockReturnValue(mockUser);
+      mockUserRepository.save.mockResolvedValue(mockUser);
+
+      const result = await userService.createUser(userData);
+
+      expect(bcrypt.hash).toHaveBeenCalledWith('password123', 10);
+      expect(mockUserRepository.create).toHaveBeenCalledWith({
+        ...userData,
+        password: 'hashed_password'
+      });
+      expect(mockUserRepository.save).toHaveBeenCalledWith(mockUser);
+      expect(result).toEqual(mockUser);
+    });
+
+    it('应该在创建用户失败时抛出错误', async () => {
+      const userData = { username: 'testuser', password: 'password123' };
+      const error = new Error('Database error');
+
+      mockUserRepository.create.mockImplementation(() => {
+        throw error;
+      });
+
+      await expect(userService.createUser(userData)).rejects.toThrow('Failed to create user');
+    });
+  });
+
+  describe('getUserById', () => {
+    it('应该通过ID成功获取用户', async () => {
+      const mockUser = { id: 1, username: 'testuser' } as User;
+      mockUserRepository.findOne.mockResolvedValue(mockUser);
+
+      const result = await userService.getUserById(1);
+
+      expect(mockUserRepository.findOne).toHaveBeenCalledWith({
+        where: { id: 1 },
+        relations: ['roles', 'avatarFile']
+      });
+      expect(result).toEqual(mockUser);
+    });
+
+    it('应该在用户不存在时返回null', async () => {
+      mockUserRepository.findOne.mockResolvedValue(null);
+
+      const result = await userService.getUserById(999);
+
+      expect(result).toBeNull();
+    });
+  });
+
+  // getUsersWithPagination 方法已移除,使用通用CRUD服务替代
+
+  describe('verifyPassword', () => {
+    it('应该验证密码正确', async () => {
+      const user = { password: 'hashed_password' } as User;
+
+      const result = await userService.verifyPassword(user, 'password123');
+
+      expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashed_password');
+      expect(result).toBe(true);
+    });
+
+    it('应该验证密码错误', async () => {
+      (vi.mocked(bcrypt.compare) as any).mockResolvedValueOnce(false);
+      const user = { password: 'hashed_password' } as User;
+
+      const result = await userService.verifyPassword(user, 'wrong_password');
+
+      expect(result).toBe(false);
+    });
+  });
+});

+ 201 - 0
packages/server/tests/unit/utils/backup.test.ts

@@ -0,0 +1,201 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { DatabaseBackup } from '@/utils/backup'
+import path from 'path'
+
+// Mock pg-dump-restore
+vi.mock('pg-dump-restore', () => ({
+  pgDump: vi.fn().mockResolvedValue(undefined),
+  pgRestore: vi.fn().mockResolvedValue(undefined),
+}))
+
+
+// Mock fs for tests
+vi.mock('fs', () => ({
+  promises: {
+    mkdir: vi.fn().mockResolvedValue(undefined),
+    chmod: vi.fn().mockResolvedValue(undefined),
+    readdir: vi.fn().mockResolvedValue([]),
+    stat: vi.fn().mockResolvedValue({ size: 1024, mtimeMs: Date.now(), mode: 0o600, mtime: new Date() }),
+    access: vi.fn().mockResolvedValue(undefined),
+    unlink: vi.fn().mockResolvedValue(undefined),
+    rm: vi.fn().mockResolvedValue(undefined),
+    writeFile: vi.fn().mockResolvedValue(undefined),
+    utimes: vi.fn().mockResolvedValue(undefined),
+  }
+}))
+
+// Mock logger
+vi.mock('@/utils/logger', () => ({
+  logger: {
+    db: vi.fn(),
+    error: vi.fn(),
+    api: vi.fn(),
+    middleware: vi.fn(),
+  },
+}))
+
+describe('DatabaseBackup', () => {
+  let backup: DatabaseBackup
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    backup = DatabaseBackup.getInstance()
+  })
+
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
+
+  describe('getInstance', () => {
+    it('应该返回单例实例', () => {
+      const instance1 = DatabaseBackup.getInstance()
+      const instance2 = DatabaseBackup.getInstance()
+      expect(instance1).toBe(instance2)
+    })
+  })
+
+  describe('ensureBackupDir', () => {
+    it('应该创建备份目录并设置权限', async () => {
+      const fs = await import('fs')
+
+      await backup.ensureBackupDir()
+
+      expect(fs.promises.mkdir).toHaveBeenCalledWith('./backups', { recursive: true })
+      expect(fs.promises.chmod).toHaveBeenCalledWith('./backups', 0o700)
+    })
+
+    it('应该在创建目录失败时抛出错误', async () => {
+      const fs = await import('fs')
+      const { logger } = await import('@/utils/logger')
+
+      vi.mocked(fs.promises.mkdir).mockRejectedValueOnce(new Error('创建目录失败'))
+
+      await expect(backup.ensureBackupDir()).rejects.toThrow('创建目录失败')
+      expect(logger.error).toHaveBeenCalled()
+    })
+  })
+
+  describe('getDbConfig', () => {
+    it('应该返回正确的数据库配置', () => {
+      process.env.DB_HOST = 'test-host'
+      process.env.DB_PORT = '5433'
+      process.env.DB_DATABASE = 'test-db'
+      process.env.DB_USERNAME = 'test-user'
+      process.env.DB_PASSWORD = 'test-password'
+
+      const config = (backup as any).getDbConfig()
+
+      expect(config).toEqual({
+        host: 'test-host',
+        port: 5433,
+        database: 'test-db',
+        username: 'test-user',
+        password: 'test-password',
+      })
+    })
+
+    it('应该使用默认值当环境变量未设置时', () => {
+      delete process.env.DB_HOST
+      delete process.env.DB_PORT
+      delete process.env.DB_DATABASE
+      delete process.env.DB_USERNAME
+      delete process.env.DB_PASSWORD
+
+      const config = (backup as any).getDbConfig()
+
+      expect(config).toEqual({
+        host: 'localhost',
+        port: 5432,
+        database: 'postgres',
+        username: 'postgres',
+        password: '',
+      })
+    })
+  })
+
+  describe('formatFileSize', () => {
+    it('应该正确格式化文件大小', () => {
+      const formatFileSize = (backup as any).formatFileSize
+
+      expect(formatFileSize(0)).toBe('0 B')
+      expect(formatFileSize(1024)).toBe('1 KB')
+      expect(formatFileSize(1048576)).toBe('1 MB')
+      expect(formatFileSize(1073741824)).toBe('1 GB')
+    })
+  })
+
+  describe('backupExists', () => {
+    it('应该返回true当备份文件存在时', async () => {
+      const fs = await import('fs')
+
+      const exists = await backup.backupExists('/path/to/backup.dump')
+
+      expect(exists).toBe(true)
+      expect(fs.promises.access).toHaveBeenCalledWith('/path/to/backup.dump')
+    })
+
+    it('应该返回false当备份文件不存在时', async () => {
+      const fs = await import('fs')
+      vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在'))
+
+      const exists = await backup.backupExists('/path/to/backup.dump')
+
+      expect(exists).toBe(false)
+    })
+  })
+
+  describe('cleanupOldBackups', () => {
+    it('应该清理7天前的旧备份', async () => {
+      const fs = await import('fs')
+      const { logger } = await import('@/utils/logger')
+
+      const now = Date.now()
+      const oldFileTime = now - (8 * 24 * 60 * 60 * 1000) // 8天前
+      const newFileTime = now - (6 * 24 * 60 * 60 * 1000) // 6天前
+
+      vi.mocked(fs.promises.readdir).mockResolvedValue(['backup-old.dump', 'backup-new.dump'] as any)
+      vi.mocked(fs.promises.stat)
+        .mockResolvedValueOnce({ mtimeMs: oldFileTime } as any)
+        .mockResolvedValueOnce({ mtimeMs: newFileTime } as any)
+
+      await backup.cleanupOldBackups()
+
+      expect(fs.promises.unlink).toHaveBeenCalledTimes(1)
+      expect(fs.promises.unlink).toHaveBeenCalledWith(path.join('./backups', 'backup-old.dump'))
+      expect(logger.db).toHaveBeenCalledWith('删除旧备份文件: backup-old.dump')
+    })
+
+    it('应该在清理失败时记录错误但不抛出', async () => {
+      const fs = await import('fs')
+      const { logger } = await import('@/utils/logger')
+
+      vi.mocked(fs.promises.readdir).mockRejectedValueOnce(new Error('读取目录失败'))
+
+      await expect(backup.cleanupOldBackups()).resolves.not.toThrow()
+      expect(logger.error).toHaveBeenCalled()
+    })
+  })
+
+  describe('startScheduledBackups', () => {
+    it('应该启动定时备份任务', async () => {
+      const { logger } = await import('@/utils/logger')
+
+      backup.startScheduledBackups()
+
+      // expect(cron.default.schedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function))
+      expect(logger.db).toHaveBeenCalledWith('备份调度已启动: 0 2 * * *')
+    })
+  })
+
+  describe('stopScheduledBackups', () => {
+    it('应该停止定时备份任务', async () => {
+      const { logger } = await import('@/utils/logger')
+
+      // 先启动再停止
+      backup.startScheduledBackups()
+      backup.stopScheduledBackups()
+
+      expect(logger.db).toHaveBeenCalledWith('备份调度已停止')
+    })
+  })
+})

+ 213 - 0
packages/server/tests/unit/utils/restore.test.ts

@@ -0,0 +1,213 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { DatabaseRestore } from '@/utils/restore'
+import path from 'path'
+
+// Mock pg-dump-restore
+vi.mock('pg-dump-restore', () => ({
+  pgRestore: vi.fn().mockResolvedValue(undefined),
+}))
+
+// Mock fs with importOriginal for partial mocking
+vi.mock('fs', async (importOriginal) => {
+  const actual = await importOriginal() as typeof import('fs')
+  return {
+    ...actual,
+    promises: {
+      ...actual.promises,
+      readdir: vi.fn().mockResolvedValue([]),
+      access: vi.fn().mockResolvedValue(undefined),
+      stat: vi.fn().mockResolvedValue({ size: 1024, mtime: new Date() }),
+    },
+  }
+})
+
+// Mock logger
+vi.mock('@/utils/logger', () => ({
+  logger: {
+    db: vi.fn(),
+    error: vi.fn(),
+    api: vi.fn(),
+    middleware: vi.fn(),
+  },
+}))
+
+describe('DatabaseRestore', () => {
+  let restore: DatabaseRestore
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    restore = new DatabaseRestore()
+  })
+
+  afterEach(() => {
+    vi.restoreAllMocks()
+  })
+
+  describe('getDbConfig', () => {
+    it('应该返回正确的数据库配置', () => {
+      process.env.DB_HOST = 'test-host'
+      process.env.DB_PORT = '5433'
+      process.env.DB_DATABASE = 'test-db'
+      process.env.DB_USERNAME = 'test-user'
+      process.env.DB_PASSWORD = 'test-password'
+
+      const config = (restore as any).getDbConfig()
+
+      expect(config).toEqual({
+        host: 'test-host',
+        port: 5433,
+        database: 'test-db',
+        username: 'test-user',
+        password: 'test-password',
+      })
+    })
+
+    it('应该使用默认值当环境变量未设置时', () => {
+      delete process.env.DB_HOST
+      delete process.env.DB_PORT
+      delete process.env.DB_DATABASE
+      delete process.env.DB_USERNAME
+      delete process.env.DB_PASSWORD
+
+      const config = (restore as any).getDbConfig()
+
+      expect(config).toEqual({
+        host: 'localhost',
+        port: 5432,
+        database: 'postgres',
+        username: 'postgres',
+        password: '',
+      })
+    })
+  })
+
+  describe('findLatestBackup', () => {
+    it('应该返回最新的备份文件', async () => {
+      const fs = await import('fs')
+
+      vi.mocked(fs.promises.readdir).mockResolvedValue([
+        'backup-2024-01-01T00-00-00Z.dump',
+        'backup-2024-01-03T00-00-00Z.dump',
+        'backup-2024-01-02T00-00-00Z.dump',
+      ] as any)
+
+      const latest = await restore.findLatestBackup()
+
+      expect(latest).toBe(path.join('./backups', 'backup-2024-01-03T00-00-00Z.dump'))
+    })
+
+    it('应该返回null当没有备份文件时', async () => {
+      const fs = await import('fs')
+
+      vi.mocked(fs.promises.readdir).mockResolvedValue([
+        'some-other-file.txt'
+      ] as any)
+
+      const latest = await restore.findLatestBackup()
+
+      expect(latest).toBeNull()
+    })
+
+    it('应该在读取目录失败时返回null', async () => {
+      const fs = await import('fs')
+      const { logger } = await import('@/utils/logger')
+
+      vi.mocked(fs.promises.readdir).mockRejectedValueOnce(new Error('读取目录失败'))
+
+      const latest = await restore.findLatestBackup()
+
+      expect(latest).toBeNull()
+      expect(logger.error).toHaveBeenCalled()
+    })
+  })
+
+  describe('listBackups', () => {
+    it('应该返回所有备份文件列表', async () => {
+      const fs = await import('fs')
+
+      vi.mocked(fs.promises.readdir).mockResolvedValue([
+        'backup-2024-01-01.dump',
+        'some-other-file.txt',
+        'backup-2024-01-02.dump',
+      ] as any)
+
+      const backups = await restore.listBackups()
+
+      expect(backups).toEqual([
+        'backup-2024-01-01.dump',
+        'backup-2024-01-02.dump',
+      ])
+    })
+
+    it('应该在读取目录失败时返回空数组', async () => {
+      const fs = await import('fs')
+      const { logger } = await import('@/utils/logger')
+
+      vi.mocked(fs.promises.readdir).mockRejectedValueOnce(new Error('读取目录失败'))
+
+      const backups = await restore.listBackups()
+
+      expect(backups).toEqual([])
+      expect(logger.error).toHaveBeenCalled()
+    })
+  })
+
+  describe('backupExists', () => {
+    it('应该返回true当备份文件存在时', async () => {
+      const fs = await import('fs')
+
+      const exists = await restore.backupExists('/path/to/backup.dump')
+
+      expect(exists).toBe(true)
+      expect(fs.promises.access).toHaveBeenCalledWith('/path/to/backup.dump')
+    })
+
+    it('应该返回false当备份文件不存在时', async () => {
+      const fs = await import('fs')
+      vi.mocked(fs.promises.access).mockRejectedValueOnce(new Error('文件不存在'))
+
+      const exists = await restore.backupExists('/path/to/backup.dump')
+
+      expect(exists).toBe(false)
+    })
+  })
+
+  describe('getBackupInfo', () => {
+    it('应该返回备份文件信息', async () => {
+      const fs = await import('fs')
+      const testDate = new Date()
+
+      vi.mocked(fs.promises.stat).mockResolvedValueOnce({
+        size: 1048576,
+        mtime: testDate,
+      } as any)
+
+      const info = await restore.getBackupInfo('/path/to/backup.dump')
+
+      expect(info).toEqual({
+        size: 1048576,
+        mtime: testDate,
+        formattedSize: '1 MB',
+      })
+    })
+
+    it('应该在获取信息失败时抛出错误', async () => {
+      const fs = await import('fs')
+
+      vi.mocked(fs.promises.stat).mockRejectedValueOnce(new Error('获取文件信息失败'))
+
+      await expect(restore.getBackupInfo('/path/to/backup.dump')).rejects.toThrow('获取备份信息失败')
+    })
+  })
+
+  describe('formatFileSize', () => {
+    it('应该正确格式化文件大小', () => {
+      const formatFileSize = (restore as any).formatFileSize
+
+      expect(formatFileSize(0)).toBe('0 B')
+      expect(formatFileSize(1024)).toBe('1 KB')
+      expect(formatFileSize(1048576)).toBe('1 MB')
+      expect(formatFileSize(1073741824)).toBe('1 GB')
+    })
+  })
+})

+ 99 - 0
packages/server/tests/utils/integration-test-db.ts

@@ -0,0 +1,99 @@
+import { DataSource } from 'typeorm';
+import { beforeEach, afterEach } from 'vitest';
+import { UserEntity } from '../../src/modules/users/user.entity';
+import { Role } from '../../src/modules/users/role.entity';
+import { AppDataSource } from '../../src/data-source';
+
+/**
+ * 集成测试数据库工具类 - 使用真实PostgreSQL数据库
+ */
+export class IntegrationTestDatabase {
+  /**
+   * 清理集成测试数据库
+   */
+  static async cleanup(): Promise<void> {
+    if (AppDataSource.isInitialized) {
+      await AppDataSource.destroy();
+    }
+  }
+
+  /**
+   * 获取当前数据源
+   */
+  static async getDataSource(): Promise<DataSource> {
+    if(!AppDataSource.isInitialized) {
+      await AppDataSource.initialize();
+    }
+    return AppDataSource
+  }
+}
+
+/**
+ * 测试数据工厂类
+ */
+export class TestDataFactory {
+  /**
+   * 创建测试用户数据
+   */
+  static createUserData(overrides: Partial<UserEntity> = {}): Partial<UserEntity> {
+    const timestamp = Date.now();
+    return {
+      username: `testuser_${timestamp}`,
+      password: 'TestPassword123!',
+      email: `test_${timestamp}@example.com`,
+      phone: `138${timestamp.toString().slice(-8)}`,
+      nickname: `Test User ${timestamp}`,
+      name: `Test Name ${timestamp}`,
+      isDisabled: 0,
+      isDeleted: 0,
+      ...overrides
+    };
+  }
+
+  /**
+   * 创建测试角色数据
+   */
+  static createRoleData(overrides: Partial<Role> = {}): Partial<Role> {
+    const timestamp = Date.now();
+    return {
+      name: `test_role_${timestamp}`,
+      description: `Test role description ${timestamp}`,
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试用户
+   */
+  static async createTestUser(dataSource: DataSource, overrides: Partial<UserEntity> = {}): Promise<UserEntity> {
+    const userData = this.createUserData(overrides);
+    const userRepository = dataSource.getRepository(UserEntity);
+
+    const user = userRepository.create(userData);
+    return await userRepository.save(user);
+  }
+
+  /**
+   * 在数据库中创建测试角色
+   */
+  static async createTestRole(dataSource: DataSource, overrides: Partial<Role> = {}): Promise<Role> {
+    const roleData = this.createRoleData(overrides);
+    const roleRepository = dataSource.getRepository(Role);
+
+    const role = roleRepository.create(roleData);
+    return await roleRepository.save(role);
+  }
+}
+
+/**
+ * 集成测试数据库生命周期钩子
+ */
+export function setupIntegrationDatabaseHooks() {
+  beforeEach(async () => {
+    await IntegrationTestDatabase.getDataSource();
+  });
+
+  afterEach(async () => {
+    await IntegrationTestDatabase.cleanup();
+  });
+}

+ 72 - 0
packages/server/tests/utils/integration-test-utils.ts

@@ -0,0 +1,72 @@
+import { IntegrationTestDatabase } from './integration-test-db';
+import { UserEntity } from '../../src/modules/users/user.entity';
+
+/**
+ * 集成测试断言工具
+ */
+export class IntegrationTestAssertions {
+  /**
+   * 断言响应状态码
+   */
+  static expectStatus(response: { status: number }, expectedStatus: number): void {
+    if (response.status !== expectedStatus) {
+      throw new Error(`Expected status ${expectedStatus}, but got ${response.status}`);
+    }
+  }
+
+  /**
+   * 断言响应包含特定字段
+   */
+  static expectResponseToHave(response: { data: any }, expectedFields: Record<string, any>): void {
+    for (const [key, value] of Object.entries(expectedFields)) {
+      if (response.data[key] !== value) {
+        throw new Error(`Expected field ${key} to be ${value}, but got ${response.data[key]}`);
+      }
+    }
+  }
+
+  /**
+   * 断言响应包含特定结构
+   */
+  static expectResponseStructure(response: { data: any }, structure: Record<string, any>): void {
+    for (const key of Object.keys(structure)) {
+      if (!(key in response.data)) {
+        throw new Error(`Expected response to have key: ${key}`);
+      }
+    }
+  }
+
+  /**
+   * 断言用户存在于数据库中
+   */
+  static async expectUserToExist(username: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const userRepository = dataSource.getRepository(UserEntity);
+    const user = await userRepository.findOne({ where: { username } });
+
+    if (!user) {
+      throw new Error(`Expected user ${username} to exist in database`);
+    }
+  }
+
+  /**
+   * 断言用户不存在于数据库中
+   */
+  static async expectUserNotToExist(username: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const userRepository = dataSource.getRepository(UserEntity);
+    const user = await userRepository.findOne({ where: { username } });
+
+    if (user) {
+      throw new Error(`Expected user ${username} not to exist in database`);
+    }
+  }
+}

+ 21 - 0
packages/server/tests/utils/setup.ts

@@ -0,0 +1,21 @@
+import { beforeAll, afterAll } from 'vitest'
+
+/**
+ * 全局测试设置文件
+ * 这个文件在运行任何测试之前执行
+ */
+
+// 全局测试设置
+beforeAll(async () => {
+  // 设置全局测试环境变量
+  process.env.NODE_ENV = 'test'
+
+  // 可以在这里初始化全局测试资源
+  console.log('测试环境初始化完成')
+})
+
+// 全局测试清理
+afterAll(async () => {
+  // 清理全局测试资源
+  console.log('测试环境清理完成')
+})