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 TestUser { @PrimaryGeneratedColumn() id!: number; @Column('varchar') username!: string; @Column('varchar') password!: string; @Column('varchar') nickname!: string; @Column('varchar') registrationSource!: string; } // 测试实体类 @Entity() class TestEntity { @PrimaryGeneratedColumn() id!: number; @Column('varchar') name!: string; @Column('int') userId!: number; @Column('int', { nullable: true }) createdBy?: number; @Column('int', { nullable: true }) updatedBy?: number; } // 测试带状态字段的实体类 @Entity() class TestEntityWithStatus { @PrimaryGeneratedColumn() id!: number; @Column('varchar') name!: string; @Column('int') status!: number; // 1=可用,0=不可用 @Column('int') userId!: number; } // 定义测试实体的Schema const createTestSchema = z.object({ name: z.string().min(1, '名称不能为空'), userId: z.number().optional() }); const updateTestSchema = z.object({ name: z.string().min(1, '名称不能为空').optional() }); const getTestSchema = z.object({ id: z.number(), name: z.string(), userId: z.number(), createdBy: z.number().nullable().optional(), updatedBy: z.number().nullable().optional() }); const listTestSchema = z.object({ id: z.number(), name: z.string(), userId: z.number(), createdBy: z.number().nullable().optional(), updatedBy: z.number().nullable().optional() }); // 设置集成测试钩子 setupIntegrationDatabaseHooksWithEntities([TestUser, TestEntity, TestEntityWithStatus]) describe('共享CRUD数据权限控制集成测试', () => { let client: any; let testToken1: string; let testToken2: string; let testUser1: TestUser; let testUser2: TestUser; let mockAuthMiddleware: any; beforeEach(async () => { // 获取数据源 const dataSource = await IntegrationTestDatabase.getDataSource(); // 创建测试用户1 const userRepository = dataSource.getRepository(TestUser); testUser1 = userRepository.create({ username: `test_user_1_${Date.now()}`, password: 'test_password', nickname: '测试用户1', registrationSource: 'web' }); await userRepository.save(testUser1); // 创建测试用户2 testUser2 = userRepository.create({ username: `test_user_2_${Date.now()}`, password: 'test_password', nickname: '测试用户2', registrationSource: 'web' }); await userRepository.save(testUser2); // 生成测试用户的token testToken1 = JWTUtil.generateToken({ id: testUser1.id, username: testUser1.username, roles: [{name:'user'}] }); testToken2 = JWTUtil.generateToken({ id: testUser2.id, username: testUser2.username, roles: [{name:'user'}] }); // 创建模拟认证中间件 mockAuthMiddleware = async (c: any, next: any) => { const authHeader = c.req.header('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7); try { // 简单模拟用户解析 if (token === testToken1) { c.set('user', { id: testUser1.id, username: testUser1.username }); } else if (token === testToken2) { c.set('user', { id: testUser2.id, username: testUser2.username }); } } 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], dataPermission: { enabled: true, userIdField: 'userId' } }); client = testClient(testRoutes); }); describe('GET / - 列表查询权限过滤', () => { it('应该只返回当前用户的数据', async () => { // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); // 为用户1创建数据 const user1Data1 = testRepository.create({ name: '用户1的数据1', userId: testUser1.id }); await testRepository.save(user1Data1); const user1Data2 = testRepository.create({ name: '用户1的数据2', userId: testUser1.id }); await testRepository.save(user1Data2); // 为用户2创建数据 const user2Data = testRepository.create({ name: '用户2的数据', userId: testUser2.id }); await testRepository.save(user2Data); // 用户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.userId).toBe(testUser1.id); }); } }); 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: '测试创建数据', userId: testUser1.id // 用户ID与当前用户匹配 }; 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.userId).toBe(testUser1.id); } }); it('应该拒绝创建不属于当前用户的数据', async () => { const createData = { name: '测试创建数据', userId: testUser2.id // 用户ID与当前用户不匹配 }; const response = await client.index.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); console.debug('创建无权数据响应状态:', response.status); expect(response.status).toBe(403); // 权限验证失败返回403 Forbidden if (response.status === 403) { const data = await response.json(); expect(data.message).toContain('无权'); } }); }); describe('GET /:id - 获取详情权限验证', () => { it('应该成功获取属于当前用户的数据详情', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '测试数据详情', userId: testUser1.id }); 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.userId).toBe(testUser1.id); } }); it('应该拒绝获取不属于当前用户的数据详情', async () => { // 先创建属于用户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '用户2的数据', userId: testUser2.id }); 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); // GET操作中,权限错误返回404而不是403 }); it('应该处理不存在的资源', async () => { const response = await client[':id'].$get({ param: { id: 999999 } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(response.status).toBe(404); }); }); describe('PUT /:id - 更新操作权限验证', () => { it('应该成功更新属于当前用户的数据', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '原始数据', userId: testUser1.id }); 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.userId).toBe(testUser1.id); } }); it('应该拒绝更新不属于当前用户的数据', async () => { // 先创建属于用户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '用户2的数据', userId: testUser2.id }); 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(403); // 权限验证失败返回403 Forbidden if (response.status === 403) { const data = await response.json(); expect(data.message).toContain('无权'); } }); }); describe('DELETE /:id - 删除操作权限验证', () => { it('应该成功删除属于当前用户的数据', async () => { // 先创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '待删除数据', userId: testUser1.id }); 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的数据', userId: testUser2.id }); 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(403); // 权限验证失败返回403 Forbidden if (response.status === 403) { const data = await response.json(); expect(data.message).toContain('无权'); } // 验证数据没有被删除 const existingData = await testRepository.findOne({ where: { id: testData.id } }); expect(existingData).not.toBeNull(); }); }); describe('禁用数据权限控制的情况', () => { it('当数据权限控制禁用时应该允许跨用户访问', async () => { // 创建禁用数据权限控制的路由 const noPermissionRoutes = createCrudRoutes({ entity: TestEntity, createSchema: createTestSchema, updateSchema: updateTestSchema, getSchema: getTestSchema, listSchema: listTestSchema, middleware: [mockAuthMiddleware], dataPermission: { enabled: false, // 禁用权限控制 userIdField: 'userId' } }); const noPermissionClient = testClient(noPermissionRoutes); // 创建属于用户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '用户2的数据', userId: testUser2.id }); await testRepository.save(testData); // 用户1应该能够访问用户2的数据(权限控制已禁用) const response = await noPermissionClient[':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.userId).toBe(testUser2.id); } }); it('当不传递dataPermission配置时应该允许跨用户访问', async () => { // 创建不传递数据权限控制的路由 const noPermissionRoutes = createCrudRoutes({ entity: TestEntity, createSchema: createTestSchema, updateSchema: updateTestSchema, getSchema: getTestSchema, listSchema: listTestSchema, middleware: [mockAuthMiddleware] // 不传递 dataPermission 配置 }); const noPermissionClient = testClient(noPermissionRoutes); // 创建属于用户2的数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntity); const testData = testRepository.create({ name: '用户2的数据(无权限配置)', userId: testUser2.id }); await testRepository.save(testData); // 用户1应该能够访问用户2的数据(没有权限控制配置) console.debug('测试数据ID(无权限配置):', testData.id); const response = await noPermissionClient[':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.userId).toBe(testUser2.id); } }); }); describe('默认过滤条件配置', () => { it('应该支持listFilters和detailFilters分别配置', async () => { // 定义Schema const createTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空'), status: z.number().int().min(0).max(1), userId: z.number().optional() }); const updateTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空').optional(), status: z.number().int().min(0).max(1).optional() }); const getTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); const listTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); // 创建带有listFilters和detailFilters的路由 const filteredRoutes = createCrudRoutes({ entity: TestEntityWithStatus, createSchema: createTestSchemaWithStatus, updateSchema: updateTestSchemaWithStatus, getSchema: getTestSchemaWithStatus, listSchema: listTestSchemaWithStatus, middleware: [mockAuthMiddleware], // 列表查询:只返回状态为1的数据 listFilters: { status: 1 }, // 详情查询:没有过滤,可以访问任何状态的数据 detailFilters: undefined }); const filteredClient = testClient(filteredRoutes); // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntityWithStatus); // 创建可用状态的数据 const availableData = testRepository.create({ name: '可用数据', status: 1, userId: testUser1.id }); await testRepository.save(availableData); // 创建不可用状态的数据 const unavailableData = testRepository.create({ name: '不可用数据', status: 0, userId: testUser1.id }); await testRepository.save(unavailableData); // 测试列表查询:应该只返回可用状态的数据 const listResponse = await filteredClient.index.$get({ query: { page: 1, pageSize: 10 } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(listResponse.status).toBe(200); const listData = await listResponse.json(); // 类型检查:确保是成功响应 if ('data' in listData) { expect(listData.data).toHaveLength(1); // 只返回可用状态的数据 expect(listData.data[0].id).toBe(availableData.id); expect(listData.data[0].status).toBe(1); } else { throw new Error('列表查询失败: ' + JSON.stringify(listData)); } // 测试详情查询:可以访问不可用状态的数据(detailFilters为空) const detailResponse = await filteredClient[':id'].$get({ param: { id: unavailableData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(detailResponse.status).toBe(200); const detailData = await detailResponse.json(); // 类型检查:确保是成功响应 if ('id' in detailData) { expect(detailData.id).toBe(unavailableData.id); expect(detailData.status).toBe(0); } else { throw new Error('详情查询失败: ' + JSON.stringify(detailData)); } }); it('应该支持向后兼容的defaultFilters', async () => { // 定义Schema const createTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空'), status: z.number().int().min(0).max(1), userId: z.number().optional() }); const updateTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空').optional(), status: z.number().int().min(0).max(1).optional() }); const getTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); const listTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); // 创建只使用defaultFilters的路由(向后兼容) const defaultFilteredRoutes = createCrudRoutes({ entity: TestEntityWithStatus, createSchema: createTestSchemaWithStatus, updateSchema: updateTestSchemaWithStatus, getSchema: getTestSchemaWithStatus, listSchema: listTestSchemaWithStatus, middleware: [mockAuthMiddleware], // 只使用defaultFilters(旧方式) defaultFilters: { status: 1 } }); const defaultFilteredClient = testClient(defaultFilteredRoutes); // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntityWithStatus); // 创建可用状态的数据 const availableData = testRepository.create({ name: '可用数据(defaultFilters)', status: 1, userId: testUser1.id }); await testRepository.save(availableData); // 创建不可用状态的数据 const unavailableData = testRepository.create({ name: '不可用数据(defaultFilters)', status: 0, userId: testUser1.id }); await testRepository.save(unavailableData); // 测试列表查询:应该只返回可用状态的数据 const listResponse = await defaultFilteredClient.index.$get({ query: { page: 1, pageSize: 10 } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(listResponse.status).toBe(200); const listData = await listResponse.json(); // 类型检查:确保是成功响应 if ('data' in listData) { expect(listData.data).toHaveLength(1); // 只返回可用状态的数据 expect(listData.data[0].id).toBe(availableData.id); expect(listData.data[0].status).toBe(1); } else { throw new Error('列表查询失败: ' + JSON.stringify(listData)); } // 测试详情查询:不应该过滤不可用状态的数据(defaultFilters只应用于列表查询,不应用于详情查询) const detailResponse = await defaultFilteredClient[':id'].$get({ param: { id: unavailableData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(detailResponse.status).toBe(200); // 可以访问不可用状态的数据(defaultFilters不应用于详情查询) const detailData = await detailResponse.json(); // 类型检查:确保是成功响应 if ('id' in detailData) { expect(detailData.id).toBe(unavailableData.id); expect(detailData.status).toBe(0); } else { throw new Error('详情查询失败: ' + JSON.stringify(detailData)); } }); it('应该支持listFilters和detailFilters的优先级高于defaultFilters', async () => { // 定义Schema const createTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空'), status: z.number().int().min(0).max(1), userId: z.number().optional() }); const updateTestSchemaWithStatus = z.object({ name: z.string().min(1, '名称不能为空').optional(), status: z.number().int().min(0).max(1).optional() }); const getTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); const listTestSchemaWithStatus = z.object({ id: z.number(), name: z.string(), status: z.number(), userId: z.number() }); // 创建同时有defaultFilters、listFilters和detailFilters的路由 const mixedFilteredRoutes = createCrudRoutes({ entity: TestEntityWithStatus, createSchema: createTestSchemaWithStatus, updateSchema: updateTestSchemaWithStatus, getSchema: getTestSchemaWithStatus, listSchema: listTestSchemaWithStatus, middleware: [mockAuthMiddleware], // defaultFilters(旧方式,应该被忽略) defaultFilters: { status: 0 }, // 默认过滤状态为0的数据 // listFilters(新方式,优先级更高) listFilters: { status: 1 }, // 列表过滤状态为1的数据 // detailFilters(新方式,优先级更高) detailFilters: undefined // 详情查询不过滤 }); const mixedFilteredClient = testClient(mixedFilteredRoutes); // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const testRepository = dataSource.getRepository(TestEntityWithStatus); // 创建可用状态的数据 const availableData = testRepository.create({ name: '可用数据(混合过滤)', status: 1, userId: testUser1.id }); await testRepository.save(availableData); // 创建不可用状态的数据 const unavailableData = testRepository.create({ name: '不可用数据(混合过滤)', status: 0, userId: testUser1.id }); await testRepository.save(unavailableData); // 测试列表查询:应该使用listFilters(status: 1),而不是defaultFilters(status: 0) const listResponse = await mixedFilteredClient.index.$get({ query: { page: 1, pageSize: 10 } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(listResponse.status).toBe(200); const listData = await listResponse.json(); // 类型检查:确保是成功响应 if ('data' in listData) { expect(listData.data).toHaveLength(1); // 只返回可用状态的数据(status: 1) expect(listData.data[0].id).toBe(availableData.id); expect(listData.data[0].status).toBe(1); } else { throw new Error('列表查询失败: ' + JSON.stringify(listData)); } // 测试详情查询:应该使用detailFilters(空),可以访问不可用状态的数据 const detailResponse = await mixedFilteredClient[':id'].$get({ param: { id: unavailableData.id } }, { headers: { 'Authorization': `Bearer ${testToken1}` } }); expect(detailResponse.status).toBe(200); // 可以访问不可用状态的数据 const detailData = await detailResponse.json(); // 类型检查:确保是成功响应 if ('id' in detailData) { expect(detailData.id).toBe(unavailableData.id); expect(detailData.status).toBe(0); } else { throw new Error('详情查询失败: ' + JSON.stringify(detailData)); } }); }); });