|
|
@@ -8,11 +8,11 @@ import {
|
|
|
IntegrationTestAssertions
|
|
|
} from '../utils/integration-test-utils';
|
|
|
import fileRoutes from '../../src/routes';
|
|
|
-import { File } from '../../src/entities';
|
|
|
-import { UserEntity, Role } from '@d8d/user-module';
|
|
|
+import { FileMt } from '../../src/entities';
|
|
|
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
|
|
|
import { TestDataFactory } from '../utils/integration-test-db';
|
|
|
-import { AuthService } from '@d8d/auth-module';
|
|
|
-import { UserService } from '@d8d/user-module';
|
|
|
+import { AuthService } from '@d8d/auth-module-mt';
|
|
|
+import { UserServiceMt } from '@d8d/user-module-mt';
|
|
|
import { MinioService } from '../../src/services/minio.service';
|
|
|
|
|
|
// Mock MinIO service to avoid real connections in tests
|
|
|
@@ -44,12 +44,12 @@ vi.mock('../../src/services/minio.service', () => {
|
|
|
});
|
|
|
|
|
|
// 设置集成测试钩子
|
|
|
-setupIntegrationDatabaseHooksWithEntities([File, UserEntity, Role])
|
|
|
+setupIntegrationDatabaseHooksWithEntities([FileMt, UserEntityMt, RoleMt])
|
|
|
|
|
|
describe('文件路由API集成测试 (使用hono/testing)', () => {
|
|
|
let client: ReturnType<typeof testClient<typeof fileRoutes>>;
|
|
|
let authService: AuthService;
|
|
|
- let userService: UserService;
|
|
|
+ let userService: UserServiceMt;
|
|
|
let testToken: string;
|
|
|
let testUser: any;
|
|
|
|
|
|
@@ -62,7 +62,7 @@ describe('文件路由API集成测试 (使用hono/testing)', () => {
|
|
|
if (!dataSource) throw new Error('Database not initialized');
|
|
|
|
|
|
// 初始化服务
|
|
|
- userService = new UserService(dataSource);
|
|
|
+ userService = new UserServiceMt(dataSource);
|
|
|
authService = new AuthService(userService);
|
|
|
|
|
|
// 创建测试用户并生成token
|
|
|
@@ -159,7 +159,7 @@ describe('文件路由API集成测试 (使用hono/testing)', () => {
|
|
|
const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
if (!dataSource) throw new Error('Database not initialized');
|
|
|
|
|
|
- const fileRepository = dataSource.getRepository(File);
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
const savedFile = await fileRepository.findOne({
|
|
|
where: { name: fileData.name }
|
|
|
});
|
|
|
@@ -394,7 +394,7 @@ describe('文件路由API集成测试 (使用hono/testing)', () => {
|
|
|
IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
|
|
|
// 验证文件已从数据库中删除
|
|
|
- const fileRepository = dataSource.getRepository(File);
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
const deletedFile = await fileRepository.findOne({
|
|
|
where: { id: testFile.id }
|
|
|
});
|
|
|
@@ -583,4 +583,294 @@ describe('文件路由API集成测试 (使用hono/testing)', () => {
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+ describe('多租户数据隔离测试', () => {
|
|
|
+ let tenant1User: any;
|
|
|
+ let tenant2User: any;
|
|
|
+ let tenant1Token: string;
|
|
|
+ let tenant2Token: string;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 创建租户1的用户
|
|
|
+ tenant1User = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'tenant1_user',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'tenant1@example.com',
|
|
|
+ tenantId: 1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建租户2的用户
|
|
|
+ tenant2User = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'tenant2_user',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'tenant2@example.com',
|
|
|
+ tenantId: 2
|
|
|
+ });
|
|
|
+
|
|
|
+ // 生成租户用户的token
|
|
|
+ tenant1Token = authService.generateToken(tenant1User);
|
|
|
+ tenant2Token = authService.generateToken(tenant2User);
|
|
|
+
|
|
|
+ // 清理文件数据
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ await fileRepository.delete({ tenantId: 1 });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('文件创建租户隔离', () => {
|
|
|
+ it('应该为租户1创建文件并设置正确的租户ID', async () => {
|
|
|
+ const fileData = {
|
|
|
+ name: 'tenant1_file.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 1024,
|
|
|
+ path: 'test/path',
|
|
|
+ description: '租户1的文件'
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client['upload-policy'].$post({
|
|
|
+ json: fileData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant1Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.file.name).toBe('tenant1_file.pdf');
|
|
|
+ expect(responseData.file.uploadUserId).toBe(tenant1User.id);
|
|
|
+
|
|
|
+ // 验证数据库中的租户ID
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ const savedFile = await fileRepository.findOne({
|
|
|
+ where: { name: fileData.name }
|
|
|
+ });
|
|
|
+ expect(savedFile?.tenantId).toBe(1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该为租户2创建文件并设置正确的租户ID', async () => {
|
|
|
+ const fileData = {
|
|
|
+ name: 'tenant2_file.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 2048,
|
|
|
+ path: 'test/path',
|
|
|
+ description: '租户2的文件'
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client['upload-policy'].$post({
|
|
|
+ json: fileData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant2Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.file.name).toBe('tenant2_file.pdf');
|
|
|
+ expect(responseData.file.uploadUserId).toBe(tenant2User.id);
|
|
|
+
|
|
|
+ // 验证数据库中的租户ID
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ const savedFile = await fileRepository.findOne({
|
|
|
+ where: { name: fileData.name }
|
|
|
+ });
|
|
|
+ expect(savedFile?.tenantId).toBe(2);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('文件查询租户隔离', () => {
|
|
|
+ beforeEach(async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+
|
|
|
+ // 创建租户1的文件
|
|
|
+ await fileRepository.save([
|
|
|
+ fileRepository.create({
|
|
|
+ name: 'tenant1_file1.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 1024,
|
|
|
+ path: 'tenant1/path1',
|
|
|
+ uploadUserId: tenant1User.id,
|
|
|
+ tenantId: 1,
|
|
|
+ uploadTime: new Date()
|
|
|
+ }),
|
|
|
+ fileRepository.create({
|
|
|
+ name: 'tenant1_file2.jpg',
|
|
|
+ type: 'image/jpeg',
|
|
|
+ size: 2048,
|
|
|
+ path: 'tenant1/path2',
|
|
|
+ uploadUserId: tenant1User.id,
|
|
|
+ tenantId: 1,
|
|
|
+ uploadTime: new Date()
|
|
|
+ })
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 创建租户2的文件
|
|
|
+ await fileRepository.save([
|
|
|
+ fileRepository.create({
|
|
|
+ name: 'tenant2_file1.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 3072,
|
|
|
+ path: 'tenant2/path1',
|
|
|
+ uploadUserId: tenant2User.id,
|
|
|
+ tenantId: 2,
|
|
|
+ uploadTime: new Date()
|
|
|
+ })
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该只返回租户1的文件列表', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant1Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(Array.isArray(responseData.data)).toBe(true);
|
|
|
+ expect(responseData.data).toHaveLength(2);
|
|
|
+ expect(responseData.data.every((file: any) => file.tenantId === 1)).toBe(true);
|
|
|
+ expect(responseData.data.some((file: any) => file.name === 'tenant1_file1.pdf')).toBe(true);
|
|
|
+ expect(responseData.data.some((file: any) => file.name === 'tenant1_file2.jpg')).toBe(true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该只返回租户2的文件列表', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant2Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(Array.isArray(responseData.data)).toBe(true);
|
|
|
+ expect(responseData.data).toHaveLength(1);
|
|
|
+ expect(responseData.data[0].tenantId).toBe(2);
|
|
|
+ expect(responseData.data[0].name).toBe('tenant2_file1.pdf');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('租户1不应该访问租户2的文件', async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ const tenant2File = await fileRepository.findOneBy({ tenantId: 2, name: 'tenant2_file1.pdf' });
|
|
|
+
|
|
|
+ if (tenant2File) {
|
|
|
+ const response = await client[':id'].$get({
|
|
|
+ param: { id: tenant2File.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant1Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 应该返回404,因为租户1不能访问租户2的文件
|
|
|
+ expect(response.status).toBe(404);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('文件删除租户隔离', () => {
|
|
|
+ let tenant1File: FileMt;
|
|
|
+ let tenant2File: FileMt;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+
|
|
|
+ // 创建租户1的文件
|
|
|
+ tenant1File = fileRepository.create({
|
|
|
+ name: 'tenant1_delete_test.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 1024,
|
|
|
+ path: 'tenant1/delete_test',
|
|
|
+ uploadUserId: tenant1User.id,
|
|
|
+ tenantId: 1,
|
|
|
+ uploadTime: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建租户2的文件
|
|
|
+ tenant2File = fileRepository.create({
|
|
|
+ name: 'tenant2_delete_test.pdf',
|
|
|
+ type: 'application/pdf',
|
|
|
+ size: 2048,
|
|
|
+ path: 'tenant2/delete_test',
|
|
|
+ uploadUserId: tenant2User.id,
|
|
|
+ tenantId: 2,
|
|
|
+ uploadTime: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ await fileRepository.save([tenant1File, tenant2File]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该允许租户1删除自己的文件', async () => {
|
|
|
+ const response = await client[':id'].$delete({
|
|
|
+ param: { id: tenant1File.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant1Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+
|
|
|
+ // 验证文件已被删除
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ const deletedFile = await fileRepository.findOneBy({ id: tenant1File.id });
|
|
|
+ expect(deletedFile).toBeNull();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('不应该允许租户2删除租户1的文件', async () => {
|
|
|
+ const response = await client[':id'].$delete({
|
|
|
+ param: { id: tenant1File.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tenant2Token}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 调试输出
|
|
|
+ console.debug(`租户2删除租户1文件响应状态: ${response.status}`);
|
|
|
+ if (response.status !== 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ console.debug(`响应数据:`, responseData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应该返回404或403,因为租户2不能删除租户1的文件
|
|
|
+ expect([404, 403]).toContain(response.status);
|
|
|
+
|
|
|
+ // 验证文件仍然存在
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+ const fileRepository = dataSource.getRepository(FileMt);
|
|
|
+ const existingFile = await fileRepository.findOneBy({ id: tenant1File.id });
|
|
|
+ expect(existingFile).toBeDefined();
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|