Prechádzať zdrojové kódy

📦 build(ci): 重构测试工作流为monorepo架构

- 更新CI工作流路径以支持monorepo结构【component-tests.yml】
- 更新CI工作流路径以支持monorepo结构【e2e-tests.yml】
- 更新CI工作流路径以支持monorepo结构【integration-tests.yml】
- 添加独立的服务器测试工作流【server-tests.yml】
- 更新项目文档以反映新的测试架构【source-tree.md】
- 更新测试策略文档以匹配monorepo结构【testing-strategy.md】
- 清理web包中重复的服务器测试文件【package.json】
- 删除重复的服务器集成测试文件【auth.integration.test.ts】
- 删除重复的服务器集成测试文件【backup.integration.test.ts】
- 删除重复的服务器集成测试文件【files.integration.test.ts】
- 删除重复的服务器集成测试文件【minio.integration.test.ts】
- 删除重复的服务器集成测试文件【users.integration.test.ts】
- 删除重复的服务器单元测试文件【file.service.test.ts】
- 删除重复的服务器单元测试文件【minio.service.test.ts】
- 删除重复的服务器单元测试文件【user.service.test.ts】
- 删除重复的服务器单元测试文件【backup.test.ts】
- 删除重复的服务器单元测试文件【restore.test.ts】
- 删除重复的测试工具文件【integration-test-db.ts】
- 删除重复的测试工具文件【integration-test-utils.ts】

♻️ refactor(tests): 清理重复测试文件并重构测试架构

- 将服务器测试完全迁移到packages/server目录
- 清理web目录中的重复测试文件
- 重构CI工作流以支持独立的服务器和web测试
- 更新文档以反映新的测试架构
yourname 4 týždňov pred
rodič
commit
6146db9c58

+ 12 - 8
.github/workflows/component-tests.yml

@@ -4,16 +4,16 @@ on:
   push:
     branches: [ main, develop ]
     paths:
-      - 'src/client/**'
-      - 'src/client/__integration_tests__/**'
-      - 'vitest.config.components.ts'
+      - 'web/src/client/**'
+      - 'web/tests/unit/**'
+      - 'web/vitest.config.components.ts'
       - '.github/workflows/component-tests.yml'
   pull_request:
     branches: [ main ]
     paths:
-      - 'src/client/**'
-      - 'src/client/__integration_tests__/**'
-      - 'vitest.config.components.ts'
+      - 'web/src/client/**'
+      - 'web/tests/unit/**'
+      - 'web/vitest.config.components.ts'
       - '.github/workflows/component-tests.yml'
   workflow_dispatch:
 
@@ -41,7 +41,9 @@ jobs:
       run: pnpm install --frozen-lockfile
 
     - name: Run component tests
-      run: pnpm test:components
+      run: |
+        cd web
+        pnpm test:components
 
     - name: Upload test results
       if: always()
@@ -55,7 +57,9 @@ jobs:
 
     - name: Generate coverage report
       if: success()
-      run: pnpm test:components:coverage
+      run: |
+        cd web
+        pnpm test:components:coverage
 
     - name: Upload coverage to Codecov
       if: success()

+ 12 - 1
.github/workflows/e2e-tests.yml

@@ -3,8 +3,16 @@ name: E2E Tests
 on:
   push:
     branches: [ main, develop ]
+    paths:
+      - 'web/tests/e2e/**'
+      - 'web/src/**'
+      - '.github/workflows/e2e-tests.yml'
   pull_request:
     branches: [ main ]
+    paths:
+      - 'web/tests/e2e/**'
+      - 'web/src/**'
+      - '.github/workflows/e2e-tests.yml'
   workflow_dispatch:
 
 jobs:
@@ -63,6 +71,7 @@ jobs:
     - name: Run E2E tests
       run: |
         export NODE_ENV=test
+        cd web
         pnpm test:e2e --project=chromium
       env:
         E2E_BASE_URL: http://localhost:8080
@@ -91,7 +100,9 @@ jobs:
 
     - name: Analyze test results
       if: always()
-      run: pnpm test:analyze
+      run: |
+        cd web
+        pnpm test:analyze
 
     - name: Notify on failure
       if: failure()

+ 3 - 6
.github/workflows/integration-tests.yml

@@ -4,16 +4,12 @@ on:
   push:
     branches: [ main, develop ]
     paths:
-      - 'src/server/api/**'
-      - 'src/server/__test_utils__/**'
-      - 'src/client/__integration_tests__/**'
+      - 'web/tests/integration/**'
       - '.github/workflows/integration-tests.yml'
   pull_request:
     branches: [ main ]
     paths:
-      - 'src/server/api/**'
-      - 'src/server/__test_utils__/**'
-      - 'src/client/__integration_tests__/**'
+      - 'web/tests/integration/**'
       - '.github/workflows/integration-tests.yml'
   workflow_dispatch:
 
@@ -70,6 +66,7 @@ jobs:
     - name: Run integration tests
       run: |
         export NODE_ENV=test
+        cd web
         pnpm test:integration
 
     - name: Upload test results

+ 125 - 0
.github/workflows/server-tests.yml

@@ -0,0 +1,125 @@
+name: Server Tests
+
+on:
+  push:
+    branches: [ main, develop ]
+    paths:
+      - 'packages/server/**'
+      - '.github/workflows/server-tests.yml'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - 'packages/server/**'
+      - '.github/workflows/server-tests.yml'
+  workflow_dispatch:
+
+jobs:
+  server-tests:
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+
+    services:
+      postgres:
+        image: postgres:17
+        env:
+          POSTGRES_DB: test_d8dai
+          POSTGRES_USER: postgres
+          POSTGRES_PASSWORD: test_password
+        options: >-
+          --health-cmd="pg_isready -U postgres"
+          --health-interval=10s
+          --health-timeout=5s
+          --health-retries=3
+        ports:
+          - 5432:5432
+
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v4
+
+    - name: Setup Node.js
+      uses: actions/setup-node@v4
+      with:
+        node-version: '20'
+        cache: 'pnpm'
+
+    - name: Install pnpm
+      uses: pnpm/action-setup@v2
+      with:
+        version: 8
+
+    - name: Install dependencies
+      run: pnpm install --frozen-lockfile
+
+    - name: Setup test environment
+      run: |
+        cp .env.test.example .env.test
+        echo "TEST_DATABASE_URL=postgresql://postgres:test_password@localhost:5432/test_d8dai" >> .env.test
+        echo "NODE_ENV=test" >> .env.test
+        echo "JWT_SECRET=test_jwt_secret_1234567890" >> .env.test
+
+    - name: Run database migrations
+      run: |
+        export NODE_ENV=test
+        cd packages/server
+        pnpm db:migrate
+
+    - name: Run server unit tests
+      run: |
+        export NODE_ENV=test
+        cd packages/server
+        pnpm test:unit
+
+    - name: Run server integration tests
+      run: |
+        export NODE_ENV=test
+        cd packages/server
+        pnpm test:integration
+
+    - name: Generate server test coverage
+      run: |
+        export NODE_ENV=test
+        cd packages/server
+        pnpm test:coverage
+
+    - name: Upload test results
+      if: always()
+      uses: actions/upload-artifact@v4
+      with:
+        name: server-test-results
+        path: |
+          packages/server/coverage/
+          packages/server/test-results/
+        retention-days: 7
+
+    - name: Upload coverage to Codecov
+      if: success()
+      uses: codecov/codecov-action@v3
+      with:
+        file: ./packages/server/coverage/coverage-final.json
+        flags: server-tests
+
+    - name: Generate test summary
+      if: always()
+      uses: test-summary/action@v2
+      with:
+        paths: packages/server/test-results/junit.xml
+
+    - name: Notify on failure
+      if: failure()
+      uses: 8398a7/action-slack@v3
+      with:
+        status: ${{ job.status }}
+        channel: '#ci-notifications'
+        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
+      env:
+        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
+
+    - name: Send test results to GitHub
+      if: always()
+      uses: dorny/test-reporter@v1
+      with:
+        name: Server Tests
+        path: packages/server/test-results/junit.xml
+        reporter: jest-junit
+        fail-on-error: false

+ 29 - 4
docs/architecture/source-tree.md

@@ -3,6 +3,7 @@
 ## 版本信息
 | 版本 | 日期 | 描述 | 作者 |
 |------|------|------|------|
+| 3.1 | 2025-11-09 | 更新测试结构,清理重复测试文件 | James |
 | 3.0 | 2025-10-22 | 更新为 monorepo 结构,添加 packages/server | Winston |
 
 ## 实际项目结构
@@ -109,6 +110,21 @@ d8d-mini-starter/
 │       │   │   └── restore.ts                   # 数据库恢复
 │       │   ├── data-source.ts                   # 数据库连接
 │       │   └── index.ts                         # 服务器入口
+│       ├── tests/              # 服务器测试
+│       │   ├── integration/    # 集成测试
+│       │   │   ├── auth.integration.test.ts      # 认证集成测试
+│       │   │   ├── backup.integration.test.ts    # 备份集成测试
+│       │   │   ├── files.integration.test.ts     # 文件集成测试
+│       │   │   ├── minio.integration.test.ts     # MinIO集成测试
+│       │   │   └── users.integration.test.ts     # 用户集成测试
+│       │   └── unit/           # 单元测试
+│       │       ├── modules/    # 业务模块测试
+│       │       │   ├── file.service.test.ts      # 文件服务测试
+│       │       │   ├── minio.service.test.ts     # MinIO服务测试
+│       │       │   └── user.service.test.ts      # 用户服务测试
+│       │       └── utils/      # 工具函数测试
+│       │           ├── backup.test.ts            # 备份工具测试
+│       │           └── restore.test.ts           # 恢复工具测试
 │       └── package.json
 ├── web/                        # Web应用 (Hono + React SSR)
 │   ├── src/
@@ -148,8 +164,13 @@ d8d-mini-starter/
 │   │   └── share/              # 共享代码
 │   ├── tests/
 │   │   ├── e2e/                # E2E测试 (Playwright)
-│   │   ├── integration/        # 集成测试
+│   │   ├── integration/        # Web集成测试
+│   │   │   └── client/         # 客户端集成测试
 │   │   └── unit/               # 单元测试
+│   │       └── client/         # 客户端单元测试
+│   │           ├── pages/      # 页面组件测试
+│   │           │   └── Users.test.tsx           # 用户页面测试
+│   │           └── debug.test.tsx               # 调试测试
 │   └── package.json
 ├── docs/                       # 项目文档
 │   └── architecture/           # 架构文档
@@ -173,9 +194,13 @@ d8d-mini-starter/
 - **数据库**: 使用PostgreSQL + TypeORM
 - **存储**: 使用MinIO进行文件存储
 - **测试结构**:
-  - 单元测试位于`tests/unit/`
-  - 集成测试位于`tests/integration/`
-  - E2E测试位于`tests/e2e/` (Playwright)
+  - **packages/server**: 独立的服务器测试
+    - 单元测试位于`packages/server/tests/unit/`
+    - 集成测试位于`packages/server/tests/integration/`
+  - **web**: Web应用测试
+    - 单元测试位于`web/tests/unit/`
+    - 集成测试位于`web/tests/integration/`
+    - E2E测试位于`web/tests/e2e/` (Playwright)
 - **开发环境**: 多八多云端开发容器,包含Node.js 20.19.2、PostgreSQL 17、Redis 7、MinIO
 - **构建工具**: 使用Vite + pnpm,支持SSR构建
 - **架构优势**:

+ 64 - 20
docs/architecture/testing-strategy.md

@@ -3,20 +3,30 @@
 ## 版本信息
 | 版本 | 日期 | 描述 | 作者 |
 |------|------|------|------|
+| 2.7 | 2025-11-09 | 更新为monorepo测试架构,清理重复测试文件 | James |
 | 2.6 | 2025-10-15 | 完成遗留测试文件迁移到统一的tests目录结构 | Winston |
 | 2.5 | 2025-10-14 | 更新测试文件位置到统一的tests目录结构 | Claude |
 | 2.4 | 2025-09-20 | 更新测试策略与主架构文档版本一致 | Winston |
 
 ## 概述
 
-本文档定义了D8D Starter项目的完整测试策略,基于现有的测试基础设施和最佳实践。测试策略遵循测试金字塔模型,确保代码质量、功能稳定性和系统可靠性。
+本文档定义了D8D Starter项目的完整测试策略,基于monorepo架构和现有的测试基础设施。测试策略遵循测试金字塔模型,确保代码质量、功能稳定性和系统可靠性。
+
+### 测试架构更新 (v2.7)
+
+项目已重构为monorepo结构,测试架构相应调整为:
+- **packages/server**: 独立的API服务器包,包含单元测试和集成测试
+- **web**: Web应用,包含组件测试、集成测试和E2E测试
+- **CI/CD**: 独立的工作流分别处理server和web的测试
 
 ## 测试金字塔策略
 
 ### 单元测试 (Unit Tests)
 - **范围**: 单个函数、类或组件
 - **目标**: 验证独立单元的correctness
-- **位置**: `tests/unit/**/*.test.{ts,tsx}`
+- **位置**:
+  - `packages/server/tests/unit/**/*.test.ts` (服务器单元测试)
+  - `web/tests/unit/**/*.test.{ts,tsx}` (Web组件单元测试)
 - **框架**: Vitest
 - **覆盖率目标**: ≥ 80%
 - **执行频率**: 每次代码变更
@@ -24,7 +34,9 @@
 ### 集成测试 (Integration Tests)
 - **范围**: 多个组件/服务协作
 - **目标**: 验证模块间集成和交互
-- **位置**: `tests/integration/**/*.test.{ts,tsx}`
+- **位置**:
+  - `packages/server/tests/integration/**/*.test.ts` (服务器集成测试)
+  - `web/tests/integration/**/*.test.{ts,tsx}` (Web集成测试)
 - **框架**: Vitest + Testing Library + hono/testing
 - **覆盖率目标**: ≥ 60%
 - **执行频率**: 每次API变更
@@ -32,7 +44,7 @@
 ### E2E测试 (End-to-End Tests)
 - **范围**: 完整用户流程
 - **目标**: 验证端到端业务流程
-- **位置**: `tests/e2e/**/*.test.{ts,tsx}`
+- **位置**: `web/tests/e2e/**/*.test.{ts,tsx}`
 - **框架**: Playwright
 - **覆盖率目标**: 关键用户流程100%
 - **执行频率**: 每日或每次重大变更
@@ -72,31 +84,42 @@ export default defineConfig({
 
 ### CI/CD环境
 ```yaml
-# GitHub Actions 测试配置
+# GitHub Actions 测试配置 (Monorepo架构)
 name: Test Pipeline
 
 jobs:
-  unit-tests:
+  server-tests:
     runs-on: ubuntu-latest
+    services:
+      postgres:
+        image: postgres:17
+        env:
+          POSTGRES_PASSWORD: test_password
+          POSTGRES_DB: test_d8dai
     steps:
-      - run: npm run test:api
-      - run: npm run test:components
+      - run: cd packages/server && pnpm test:unit
+      - run: cd packages/server && pnpm test:integration
 
-  integration-tests:
+  web-integration-tests:
     runs-on: ubuntu-latest
     services:
       postgres:
-        image: postgres:15
+        image: postgres:17
         env:
-          POSTGRES_PASSWORD: postgres
-          POSTGRES_DB: test_db
+          POSTGRES_PASSWORD: test_password
+          POSTGRES_DB: test_d8dai
     steps:
-      - run: npm run test:integration
+      - run: cd web && pnpm test:integration
 
-  e2e-tests:
+  web-component-tests:
     runs-on: ubuntu-latest
     steps:
-      - run: npm run test:e2e:chromium
+      - run: cd web && pnpm test:components
+
+  web-e2e-tests:
+    runs-on: ubuntu-latest
+    steps:
+      - run: cd web && pnpm test:e2e:chromium
 ```
 
 ## 测试覆盖率标准
@@ -147,21 +170,41 @@ const inactiveUser = createTestUser({ active: false });
 ## 测试执行流程
 
 ### 本地开发测试
+
+#### packages/server
+```bash
+# 运行所有测试
+pnpm test
+
+# 运行单元测试
+pnpm test:unit
+
+# 运行集成测试
+pnpm test:integration
+
+# 生成覆盖率报告
+pnpm test:coverage
+```
+
+#### web
 ```bash
 # 运行所有测试
-npm test
+pnpm test
 
 # 运行单元测试
-npm run test:unit
+pnpm test:unit
 
 # 运行集成测试
-npm run test:integration
+pnpm test:integration
+
+# 运行组件测试
+pnpm test:components
 
 # 运行E2E测试
-npm run test:e2e:chromium
+pnpm test:e2e:chromium
 
 # 生成覆盖率报告
-npm run test:coverage
+pnpm test:coverage
 ```
 
 ### CI/CD流水线测试
@@ -283,6 +326,7 @@ describe('UserService', () => {
 ### 更新日志
 | 日期 | 版本 | 描述 |
 |------|------|------|
+| 2025-11-09 | 2.7 | 更新为monorepo测试架构,清理重复测试文件 |
 | 2025-10-15 | 2.6 | 完成遗留测试文件迁移到统一的tests目录结构 |
 | 2025-10-14 | 2.5 | 重构测试文件结构,统一到tests目录 |
 | 2025-09-20 | 2.4 | 更新版本与主架构文档一致 |

+ 0 - 1
web/package.json

@@ -19,7 +19,6 @@
     "test:api:coverage": "vitest run --coverage --project=node",
     "test:components:coverage": "vitest run --coverage --project=happy-dom",
     "test:unit": "vitest run tests/unit",
-    "test:integration:server": "vitest run tests/integration/server",
     "test:integration:client": "vitest run tests/integration/client",
     "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts",
     "test:e2e:ui": "playwright test --config=tests/e2e/playwright.config.ts --ui",

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

@@ -1,411 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { testClient } from 'hono/testing';
-import {
-  IntegrationTestDatabase,
-  setupIntegrationDatabaseHooks,
-  TestDataFactory
-} from '~/utils/server/integration-test-db';
-import { UserEntity } from '@d8d/server/modules/users/user.entity';
-import { authRoutes } from '@d8d/server/api';
-import { AuthService } from '@d8d/server/modules/auth/auth.service';
-import { UserService } from '@d8d/server/modules/users/user.service';
-import { DisabledStatus } from '@d8d/server/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
-    });
-  });
-});

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

@@ -1,229 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import { databaseBackup } from '@d8d/server/utils/backup'
-import { databaseRestore } from '@d8d/server/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('@d8d/server/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'])
-    })
-  })
-})

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

@@ -1,653 +0,0 @@
-import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
-import { testClient } from 'hono/testing';
-import { FileService } from '@d8d/server/modules/files/file.service';
-import { authMiddleware } from '@d8d/server/middleware/auth.middleware';
-import { fileApiRoutes } from '@d8d/server/api';
-import { ConcreteCrudService } from '@d8d/server/utils/concrete-crud.service';
-
-vi.mock('@d8d/server/modules/files/file.service');
-vi.mock('@d8d/server/middleware/auth.middleware');
-vi.mock('@d8d/server/data-source');
-vi.mock('@d8d/server/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);
-    });
-  });
-});

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

@@ -1,302 +0,0 @@
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import { MinioService } from '@d8d/server/modules/files/minio.service';
-import { Client } from 'minio';
-import { logger } from '@d8d/server/utils/logger';
-
-// Mock dependencies
-vi.mock('minio');
-vi.mock('@d8d/server/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' }
-      );
-    });
-  });
-});

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

@@ -1,430 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { testClient } from 'hono/testing';
-import {
-  IntegrationTestDatabase,
-  setupIntegrationDatabaseHooks,
-  TestDataFactory
-} from '~/utils/server/integration-test-db';
-import { IntegrationTestAssertions } from '~/utils/server/integration-test-utils';
-import { userRoutes } from '@d8d/server/api';
-import { AuthService } from '@d8d/server/modules/auth/auth.service';
-import { UserService } from '@d8d/server/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
-    });
-  });
-});

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

@@ -1,424 +0,0 @@
-import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
-import { DataSource } from 'typeorm';
-import { FileService } from '@d8d/server/modules/files/file.service';
-import { File } from '@d8d/server/modules/files/file.entity';
-import { MinioService } from '@d8d/server/modules/files/minio.service';
-import { logger } from '@d8d/server/utils/logger';
-
-// Mock dependencies
-vi.mock('@d8d/server/modules/files/minio.service');
-vi.mock('@d8d/server/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();
-    });
-  });
-});

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

@@ -1,424 +0,0 @@
-import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
-import { MinioService } from '@d8d/server/modules/files/minio.service';
-import { Client } from 'minio';
-import { logger } from '@d8d/server/utils/logger';
-
-// Mock dependencies
-vi.mock('minio');
-vi.mock('@d8d/server/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
-      );
-    });
-  });
-});

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

@@ -1,144 +0,0 @@
-import { UserService } from '@d8d/server/modules/users/user.service';
-import { UserEntity as User } from '@d8d/server/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);
-    });
-  });
-});

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

@@ -1,201 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import { DatabaseBackup } from '@d8d/server/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('@d8d/server/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('@d8d/server/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('@d8d/server/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('@d8d/server/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('@d8d/server/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('@d8d/server/utils/logger')
-
-      // 先启动再停止
-      backup.startScheduledBackups()
-      backup.stopScheduledBackups()
-
-      expect(logger.db).toHaveBeenCalledWith('备份调度已停止')
-    })
-  })
-})

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

@@ -1,213 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
-import { DatabaseRestore } from '@d8d/server/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('@d8d/server/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('@d8d/server/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('@d8d/server/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')
-    })
-  })
-})

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

@@ -1,99 +0,0 @@
-import { DataSource } from 'typeorm';
-import { beforeEach, afterEach } from 'vitest';
-import { UserEntity } from '@d8d/server/modules/users/user.entity';
-import { Role } from '@d8d/server/modules/users/role.entity';
-import { AppDataSource } from '@d8d/server/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();
-  });
-}

+ 0 - 75
web/tests/utils/server/integration-test-utils.ts

@@ -1,75 +0,0 @@
-
-import { IntegrationTestDatabase } from './integration-test-db';
-import { UserEntity } from '@d8d/server/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`);
-    }
-  }
-}