import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { testClient } from 'hono/testing'; import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util'; import { JWTUtil } from '@d8d/shared-utils'; import { z } from '@hono/zod-openapi'; import { createCrudRoutes } from '../../src/routes/generic-crud.routes'; import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; // 测试实体类 @Entity() class TestEntity { @PrimaryGeneratedColumn() id!: number; @Column('varchar') name!: string; @Column('int') tenantId!: number; } // 定义测试实体的Schema const createTestSchema = z.object({ name: z.string().min(1, '名称不能为空'), tenantId: z.number().optional() }); const updateTestSchema = z.object({ name: z.string().min(1, '名称不能为空').optional() }); const getTestSchema = z.object({ id: z.number(), name: z.string(), tenantId: z.number() }); const listTestSchema = z.object({ id: z.number(), name: z.string(), tenantId: z.number() }); // 设置集成测试钩子 setupIntegrationDatabaseHooksWithEntities([TestEntity]) describe('共享CRUD租户隔离集成测试', () => { let client: any; let testToken1: string; let testToken2: string; let mockAuthMiddleware: any; beforeEach(async () => { // 获取数据源 const dataSource = await IntegrationTestDatabase.getDataSource(); // 生成测试用户的token testToken1 = JWTUtil.generateToken({ id: 1, username: 'tenant1_user', roles: [{name:'user'}], tenantId: 1 }); testToken2 = JWTUtil.generateToken({ id: 2, username: 'tenant2_user', roles: [{name:'user'}], tenantId: 2 }); // 创建模拟认证中间件 mockAuthMiddleware = async (c: any, next: any) => { const authHeader = c.req.header('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7); try { const payload = JWTUtil.verifyToken(token); // 根据token确定租户ID let tenantId: number | undefined; if (token === testToken1) { tenantId = 1; } else if (token === testToken2) { tenantId = 2; } // 确保用户对象包含tenantId const userWithTenant = { ...payload, tenantId }; c.set('user', userWithTenant); // 设置租户上下文 c.set('tenantId', tenantId); } catch (error) { // token解析失败 } } else { // 没有认证信息,返回401 return c.json({ code: 401, message: '认证失败' }, 401); } await next(); }; // 创建测试路由 - 启用租户隔离 const testRoutes = createCrudRoutes({ entity: TestEntity, createSchema: createTestSchema, updateSchema: updateTestSchema, getSchema: getTestSchema, listSchema: listTestSchema, middleware: [mockAuthMiddleware], tenantOptions: { enabled: true, tenantIdField: 'tenantId', autoExtractFromContext: true } }); client = testClient(testRoutes); }); describe('GET / - 列表查询租户隔离', () => { it('应该只返回当前租户的数据', async () => { // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); // 为租户1创建数据 const tenant1Data1 = testRepository.create({ name: '租户1的数据1', tenantId: 1 }); await testRepository.save(tenant1Data1); const tenant1Data2 = testRepository.create({ name: '租户1的数据2', tenantId: 1 }); await testRepository.save(tenant1Data2); // 为租户2创建数据 const tenant2Data = testRepository.create({ name: '租户2的数据', tenantId: 2 }); await testRepository.save(tenant2Data); // 租户1用户查询列表 const response = await client.index.$get({ query: { page: 1, pageSize: 10 } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离列表查询响应状态:', response.status); if (response.status !== 200) { const errorData = await response.json(); console.debug('租户隔离列表查询错误信息:', errorData); } expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data).toHaveProperty('data'); expect(Array.isArray(data.data)).toBe(true); expect(data.data).toHaveLength(2); // 应该只返回租户1的2条数据 // 验证所有返回的数据都属于租户1 data.data.forEach((item: any) => { expect(item.tenantId).toBe(1); }); } }); it('应该拒绝未认证用户的访问', async () => { const response = await client.index.$get({ query: { page: 1, pageSize: 10 } }); expect(response.status).toBe(401); }); }); describe('POST / - 创建操作租户验证', () => { it('应该成功创建属于当前租户的数据', async () => { const createData = { name: '测试创建数据' }; const response = await client.index.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离创建数据响应状态:', response.status); expect(response.status).toBe(201); if (response.status === 201) { const data = await response.json(); expect(data).toHaveProperty('id'); expect(data.name).toBe(createData.name); expect(data.tenantId).toBe(1); // 应该自动设置为租户1 } }); }); describe('GET /:id - 获取详情租户验证', () => { it('应该成功获取属于当前租户的数据详情', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '测试数据详情', tenantId: 1 }); await testRepository.save(testData); const response = await client[':id'].$get({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离获取详情响应状态:', response.status); if (response.status !== 200) { const errorData = await response.json(); console.debug('租户隔离获取详情错误信息:', errorData); } expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.id).toBe(testData.id); expect(data.name).toBe(testData.name); expect(data.tenantId).toBe(1); } }); it('应该拒绝获取不属于当前租户的数据详情', async () => { // 先创建属于租户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '租户2的数据', tenantId: 2 }); await testRepository.save(testData); // 租户1用户尝试获取租户2的数据 const response = await client[':id'].$get({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离获取无权详情响应状态:', response.status); expect(response.status).toBe(404); // 应该返回404而不是403 }); }); describe('PUT /:id - 更新操作租户验证', () => { it('应该成功更新属于当前租户的数据', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '原始数据', tenantId: 1 }); await testRepository.save(testData); const updateData = { name: '更新后的数据' }; const response = await client[':id'].$put({ param: { id: testData.id }, json: updateData }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离更新数据响应状态:', response.status); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.name).toBe(updateData.name); expect(data.tenantId).toBe(1); } }); it('应该拒绝更新不属于当前租户的数据', async () => { // 先创建属于租户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '租户2的数据', tenantId: 2 }); await testRepository.save(testData); const updateData = { name: '尝试更新的数据' }; // 租户1用户尝试更新租户2的数据 const response = await client[':id'].$put({ param: { id: testData.id }, json: updateData }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离更新无权数据响应状态:', response.status); expect(response.status).toBe(404); // 应该返回404而不是403 }); }); describe('DELETE /:id - 删除操作租户验证', () => { it('应该成功删除属于当前租户的数据', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '待删除数据', tenantId: 1 }); await testRepository.save(testData); const response = await client[':id'].$delete({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离删除数据响应状态:', response.status); expect(response.status).toBe(204); // 验证数据确实被删除 const deletedData = await testRepository.findOne({ where: { id: testData.id } }); expect(deletedData).toBeNull(); }); it('应该拒绝删除不属于当前租户的数据', async () => { // 先创建属于租户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '租户2的数据', tenantId: 2 }); await testRepository.save(testData); // 租户1用户尝试删除租户2的数据 const response = await client[':id'].$delete({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('租户隔离删除无权数据响应状态:', response.status); expect(response.status).toBe(404); // 应该返回404而不是403 // 验证数据没有被删除 const existingData = await testRepository.findOne({ where: { id: testData.id } }); expect(existingData).not.toBeNull(); }); }); describe('禁用租户隔离的情况', () => { it('当租户隔离禁用时应该允许跨租户访问', async () => { // 创建禁用租户隔离的路由 const noTenantRoutes = createCrudRoutes({ entity: TestEntity, createSchema: createTestSchema, updateSchema: updateTestSchema, getSchema: getTestSchema, listSchema: listTestSchema, middleware: [mockAuthMiddleware], tenantOptions: { enabled: false, // 禁用租户隔离 tenantIdField: 'tenantId', autoExtractFromContext: true } }); const noTenantClient = testClient(noTenantRoutes); // 创建属于租户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '租户2的数据', tenantId: 2 }); await testRepository.save(testData); // 租户1用户应该能够访问租户2的数据(租户隔离已禁用) const response = await noTenantClient[':id'].$get({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('禁用租户隔离时的响应状态:', response.status); if (response.status !== 200) { try { const errorData = await response.json(); console.debug('禁用租户隔离时的错误信息:', errorData); } catch (e) { const text = await response.text(); console.debug('禁用租户隔离时的响应文本:', text); } } expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.id).toBe(testData.id); expect(data.tenantId).toBe(2); } }); it('当不传递tenantOptions配置时应该允许跨租户访问', async () => { // 创建不传递租户隔离配置的路由 const noTenantRoutes = createCrudRoutes({ entity: TestEntity, createSchema: createTestSchema, updateSchema: updateTestSchema, getSchema: getTestSchema, listSchema: listTestSchema, middleware: [mockAuthMiddleware] // 不传递 tenantOptions 配置 }); const noTenantClient = testClient(noTenantRoutes); // 创建属于租户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '租户2的数据(无租户配置)', tenantId: 2 }); await testRepository.save(testData); // 租户1用户应该能够访问租户2的数据(没有租户隔离配置) console.debug('测试数据ID(无租户配置):', testData.id); const response = await noTenantClient[':id'].$get({ param: { id: testData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('无租户配置时的响应状态:', response.status); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.id).toBe(testData.id); expect(data.tenantId).toBe(2); } }); }); });