Преглед изворни кода

✅ test(crud): add data permission integration tests

- 添加共享CRUD数据权限控制集成测试
- 测试列表查询、创建、获取详情、更新和删除操作的权限过滤
- 验证数据权限控制启用/禁用时的不同行为

📦 build(deps): add new dependencies

- 添加@d8d/shared-test-util依赖用于测试工具支持
- 添加hono依赖用于测试客户端功能
yourname пре 1 месец
родитељ
комит
dc8865081f

+ 2 - 0
packages/shared-crud/package.json

@@ -32,7 +32,9 @@
     "@asteasolutions/zod-to-openapi": "^8.1.0",
     "@d8d/shared-types": "workspace:*",
     "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-test-util": "workspace:*",
     "@hono/zod-openapi": "1.0.2",
+    "hono": "^4.8.5",
     "typeorm": "^0.3.20",
     "zod": "^4.1.12"
   },

+ 477 - 0
packages/shared-crud/tests/integration/data-permission.integration.test.ts

@@ -0,0 +1,477 @@
+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()
+  username!: string;
+
+  @Column()
+  password!: string;
+
+  @Column()
+  nickname!: string;
+
+  @Column()
+  registrationSource!: string;
+}
+
+// 测试实体类
+class TestEntity {
+  id!: number;
+  name!: string;
+  userId!: number;
+  createdBy?: number;
+  updatedBy?: 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().optional(),
+  updatedBy: z.number().optional()
+});
+
+const listTestSchema = z.object({
+  id: z.number(),
+  name: z.string(),
+  userId: z.number(),
+  createdBy: z.number().optional(),
+  updatedBy: z.number().optional()
+});
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([TestUser])
+
+describe('共享CRUD数据权限控制集成测试', () => {
+  let client: any;
+  let testToken1: string;
+  let testToken2: string;
+  let testUser1: TestUser;
+  let testUser2: TestUser;
+
+  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'}]
+    });
+
+    // 创建测试路由 - 启用数据权限控制
+    const testRoutes = createCrudRoutes({
+      entity: TestEntity,
+      createSchema: createTestSchema,
+      updateSchema: updateTestSchema,
+      getSchema: getTestSchema,
+      listSchema: listTestSchema,
+      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: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('列表查询响应状态:', response.status);
+      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: {}
+      });
+      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(500); // 权限验证失败会抛出错误
+
+      if (response.status === 500) {
+        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);
+      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); // 权限验证失败返回404
+    });
+
+    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(500); // 权限验证失败会抛出错误
+
+      if (response.status === 500) {
+        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(500); // 权限验证失败会抛出错误
+
+      if (response.status === 500) {
+        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,
+        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);
+      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);
+      }
+    });
+  });
+});

+ 6 - 0
pnpm-lock.yaml

@@ -640,6 +640,9 @@ importers:
       '@asteasolutions/zod-to-openapi':
         specifier: ^8.1.0
         version: 8.1.0(zod@4.1.12)
+      '@d8d/shared-test-util':
+        specifier: workspace:*
+        version: link:../shared-test-util
       '@d8d/shared-types':
         specifier: workspace:*
         version: link:../shared-types
@@ -649,6 +652,9 @@ importers:
       '@hono/zod-openapi':
         specifier: 1.0.2
         version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
       typeorm:
         specifier: ^0.3.20
         version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)