Ver código fonte

✅ test(file): 添加文件模块集成测试

- 创建文件路由API集成测试,覆盖创建、读取、更新、删除等核心功能
- 实现测试数据工厂类,提供测试文件创建工具方法
- 开发集成测试断言工具,封装常用测试验证逻辑
- 配置vitest测试环境,关闭并行测试避免数据库连接冲突
- 测试包含认证授权、数据验证、边界条件等多种场景
yourname 4 semanas atrás
pai
commit
ce86150d4e

+ 549 - 0
packages/file-module/tests/integration/file.routes.integration.test.ts

@@ -0,0 +1,549 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooksWithEntities
+} from '@d8d/shared-test-util';
+import {
+  IntegrationTestAssertions
+} from '../utils/integration-test-utils';
+import fileRoutes from '../../src/routes';
+import { File } from '../../src/entities';
+import { UserEntity } from '@d8d/user-module';
+import { TestDataFactory } from '../utils/integration-test-db';
+import { AuthService } from '@d8d/auth-module';
+import { UserService } from '@d8d/user-module';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([File, UserEntity])
+
+describe('文件路由API集成测试 (使用hono/testing)', () => {
+  let client: ReturnType<typeof testClient<typeof fileRoutes>>;
+  let authService: AuthService;
+  let userService: UserService;
+  let testToken: string;
+  let testUser: any;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(fileRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) throw new Error('Database not initialized');
+
+    // 初始化服务
+    userService = new UserService(dataSource);
+    authService = new AuthService(userService);
+
+    // 创建测试用户并生成token
+    testUser = await TestDataFactory.createTestUser(dataSource, {
+      username: 'testuser_file',
+      password: 'TestPassword123!',
+      email: 'testuser_file@example.com'
+    });
+
+    // 生成测试用户的token
+    testToken = authService.generateToken(testUser);
+  });
+
+  describe('文件创建路由测试', () => {
+    it('应该拒绝无认证令牌的文件创建请求', async () => {
+      const fileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        path: '/uploads/test.txt',
+        description: 'Test file'
+      };
+
+      const response = await client['upload-policy'].$post({
+        json: fileData
+      });
+
+      // 应该返回401状态码,因为缺少认证
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该拒绝无效认证令牌的文件创建请求', async () => {
+      const fileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        path: '/uploads/test.txt',
+        description: 'Test file'
+      };
+
+      const response = await client['upload-policy'].$post({
+        json: fileData
+      }, {
+        headers: {
+          'Authorization': 'Bearer invalid.token.here'
+        }
+      });
+
+      // 应该返回401状态码,因为令牌无效
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Invalid token');
+      }
+    });
+
+    it('应该成功创建文件上传策略(使用有效认证令牌)', async () => {
+      const fileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        path: '/uploads/test.txt',
+        description: 'Test file'
+      };
+
+      const response = await client['upload-policy'].$post({
+        json: fileData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 断言响应
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('file');
+        expect(responseData).toHaveProperty('uploadPolicy');
+        expect(responseData.file.name).toBe(fileData.name);
+        expect(responseData.file.type).toBe(fileData.type);
+        expect(responseData.file.size).toBe(fileData.size);
+        expect(responseData.file.uploadUserId).toBe(testUser.id);
+
+        // 断言数据库中存在文件记录
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        if (!dataSource) throw new Error('Database not initialized');
+
+        const fileRepository = dataSource.getRepository(File);
+        const savedFile = await fileRepository.findOne({
+          where: { name: fileData.name }
+        });
+        expect(savedFile).toBeTruthy();
+        expect(savedFile?.uploadUserId).toBe(testUser.id);
+      }
+    });
+
+    it('应该拒绝创建无效文件数据的请求', async () => {
+      const invalidFileData = {
+        name: '', // 空文件名
+        type: 'text/plain'
+      };
+
+      const response = await client['upload-policy'].$post({
+        json: invalidFileData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 应该返回验证错误
+      expect([400, 500]).toContain(response.status);
+    });
+  });
+
+  describe('文件读取路由测试', () => {
+    it('应该成功获取文件列表', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      // 创建几个测试文件
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'file1.txt',
+        uploadUserId: testUser.id
+      });
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'file2.txt',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client.index.$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 testFile = await TestDataFactory.createTestFile(dataSource, {
+        name: 'testfile_detail',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client[':id'].$get({
+        param: { id: testFile.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.id).toBe(testFile.id);
+        expect(responseData.name).toBe(testFile.name);
+        expect(responseData.type).toBe(testFile.type);
+      }
+    });
+
+    it('应该返回404当文件不存在时', async () => {
+      const response = await client[':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('文件URL生成路由测试', () => {
+    it('应该成功生成文件访问URL', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testFile = await TestDataFactory.createTestFile(dataSource, {
+        name: 'testfile_url',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client[':id']['url'].$get({
+        param: { id: testFile.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('url');
+        expect(typeof responseData.url).toBe('string');
+        expect(responseData.url.length).toBeGreaterThan(0);
+      }
+    });
+
+    it('应该返回404当为不存在的文件生成URL时', async () => {
+      const response = await client[':id']['url'].$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('应该成功生成文件下载URL', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testFile = await TestDataFactory.createTestFile(dataSource, {
+        name: 'testfile_download.txt',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client[':id']['download'].$get({
+        param: { id: testFile.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData).toHaveProperty('url');
+        expect(responseData).toHaveProperty('filename');
+        expect(typeof responseData.url).toBe('string');
+        expect(responseData.filename).toBe(testFile.name);
+      }
+    });
+
+    it('应该返回404当为不存在的文件生成下载URL时', async () => {
+      const response = await client[':id']['download'].$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 testFile = await TestDataFactory.createTestFile(dataSource, {
+        name: 'testfile_delete_no_auth',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client[':id'].$delete({
+        param: { id: testFile.id }
+      });
+
+      // 应该返回401状态码,因为缺少认证
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该成功删除文件(使用有效认证令牌)', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testFile = await TestDataFactory.createTestFile(dataSource, {
+        name: 'testfile_delete',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client[':id'].$delete({
+        param: { id: testFile.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      // 验证文件已从数据库中删除
+      const fileRepository = dataSource.getRepository(File);
+      const deletedFile = await fileRepository.findOne({
+        where: { id: testFile.id }
+      });
+      expect(deletedFile).toBeNull();
+
+      // 验证再次获取文件返回404
+      const getResponse = await client[':id'].$get({
+        param: { id: testFile.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+      IntegrationTestAssertions.expectStatus(getResponse, 404);
+    });
+
+    it('应该返回404当删除不存在的文件时', async () => {
+      const response = await client[':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.createTestFile(dataSource, {
+        name: 'search_file_1.txt',
+        uploadUserId: testUser.id
+      });
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'search_file_2.txt',
+        uploadUserId: testUser.id
+      });
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'other_file.txt',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client.index.$get({
+        query: { keyword: 'search_file' }
+      }, {
+        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 filenames = responseData.data.map((file: any) => file.name);
+        expect(filenames).toContain('search_file_1.txt');
+        expect(filenames).toContain('search_file_2.txt');
+        expect(filenames).not.toContain('other_file.txt');
+      }
+    });
+
+    it('应该能够按文件类型搜索文件', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'image1.jpg',
+        type: 'image/jpeg',
+        uploadUserId: testUser.id
+      });
+      await TestDataFactory.createTestFile(dataSource, {
+        name: 'image2.png',
+        type: 'image/png',
+        uploadUserId: testUser.id
+      });
+
+      const response = await client.index.$get({
+        query: { keyword: 'image' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.data.length).toBe(2);
+
+        const types = responseData.data.map((file: any) => file.type);
+        expect(types).toContain('image/jpeg');
+        expect(types).toContain('image/png');
+      }
+    });
+  });
+
+  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.createTestFile(dataSource, {
+          name: `perf_file_${i}.txt`,
+          uploadUserId: testUser.id
+        });
+      }
+
+      const startTime = Date.now();
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+      const endTime = Date.now();
+      const responseTime = endTime - startTime;
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
+    });
+  });
+
+  describe('认证令牌测试', () => {
+    it('应该拒绝过期令牌的文件请求', async () => {
+      // 创建立即过期的令牌
+      const expiredToken = authService.generateToken(testUser, '1ms');
+
+      // 等待令牌过期
+      await new Promise(resolve => setTimeout(resolve, 10));
+
+      const response = await client['upload-policy'].$post({
+        json: {
+          name: 'test_expired_token.txt',
+          type: 'text/plain',
+          size: 1024
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${expiredToken}`
+        }
+      });
+
+      // 应该返回401状态码,因为令牌过期
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Invalid token');
+      }
+    });
+
+    it('应该拒绝格式错误的认证头', async () => {
+      const response = await client['upload-policy'].$post({
+        json: {
+          name: 'test_bad_auth_header.txt',
+          type: 'text/plain',
+          size: 1024
+        }
+      }, {
+        headers: {
+          'Authorization': 'Basic invalid_format'
+        }
+      });
+
+      // 应该返回401状态码,因为认证头格式错误
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+  });
+});

+ 35 - 0
packages/file-module/tests/utils/integration-test-db.ts

@@ -0,0 +1,35 @@
+import { DataSource } from 'typeorm';
+import { File } from '../../src/entities';
+
+/**
+ * 测试数据工厂类
+ */
+export class TestDataFactory {
+  /**
+   * 创建测试文件数据
+   */
+  static createFileData(overrides: Partial<File> = {}): Partial<File> {
+    const timestamp = Date.now();
+    return {
+      name: `testfile_${timestamp}.txt`,
+      type: 'text/plain',
+      size: 1024,
+      path: `/uploads/testfile_${timestamp}.txt`,
+      description: `Test file ${timestamp}`,
+      uploadUserId: 1,
+      uploadTime: new Date(),
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试文件
+   */
+  static async createTestFile(dataSource: DataSource, overrides: Partial<File> = {}): Promise<File> {
+    const fileData = this.createFileData(overrides);
+    const fileRepository = dataSource.getRepository(File);
+
+    const file = fileRepository.create(fileData);
+    return await fileRepository.save(file);
+  }
+}

+ 106 - 0
packages/file-module/tests/utils/integration-test-utils.ts

@@ -0,0 +1,106 @@
+import { IntegrationTestDatabase } from '@d8d/shared-test-util';
+import { File } from '../../src/entities';
+
+/**
+ * 集成测试断言工具
+ */
+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 expectFileToExist(name: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const fileRepository = dataSource.getRepository(File);
+    const file = await fileRepository.findOne({ where: { name } });
+
+    if (!file) {
+      throw new Error(`Expected file ${name} to exist in database`);
+    }
+  }
+
+  /**
+   * 断言文件不存在于数据库中
+   */
+  static async expectFileNotToExist(name: string): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const fileRepository = dataSource.getRepository(File);
+    const file = await fileRepository.findOne({ where: { name } });
+
+    if (file) {
+      throw new Error(`Expected file ${name} not to exist in database`);
+    }
+  }
+
+  /**
+   * 断言文件存在于数据库中(通过ID)
+   */
+  static async expectFileToExistById(id: number): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const fileRepository = dataSource.getRepository(File);
+    const file = await fileRepository.findOne({ where: { id } });
+
+    if (!file) {
+      throw new Error(`Expected file with ID ${id} to exist in database`);
+    }
+  }
+
+  /**
+   * 断言文件不存在于数据库中(通过ID)
+   */
+  static async expectFileNotToExistById(id: number): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) {
+      throw new Error('Database not initialized');
+    }
+
+    const fileRepository = dataSource.getRepository(File);
+    const file = await fileRepository.findOne({ where: { id } });
+
+    if (file) {
+      throw new Error(`Expected file with ID ${id} not to exist in database`);
+    }
+  }
+}

+ 21 - 0
packages/file-module/vitest.config.ts

@@ -0,0 +1,21 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.test.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'node_modules/',
+        'dist/',
+        'tests/',
+        '**/*.d.ts'
+      ]
+    },
+    // 关闭并行测试以避免数据库连接冲突
+    fileParallelism: false
+  }
+});