tenant-isolation.integration.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
  4. import { JWTUtil } from '@d8d/shared-utils';
  5. import { z } from '@hono/zod-openapi';
  6. import { createCrudRoutes } from '../../src/routes/generic-crud.routes';
  7. import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
  8. // 测试实体类
  9. @Entity()
  10. class TestEntity {
  11. @PrimaryGeneratedColumn()
  12. id!: number;
  13. @Column('varchar')
  14. name!: string;
  15. @Column('int')
  16. tenantId!: number;
  17. }
  18. // 定义测试实体的Schema
  19. const createTestSchema = z.object({
  20. name: z.string().min(1, '名称不能为空'),
  21. tenantId: z.number().optional()
  22. });
  23. const updateTestSchema = z.object({
  24. name: z.string().min(1, '名称不能为空').optional()
  25. });
  26. const getTestSchema = z.object({
  27. id: z.number(),
  28. name: z.string(),
  29. tenantId: z.number()
  30. });
  31. const listTestSchema = z.object({
  32. id: z.number(),
  33. name: z.string(),
  34. tenantId: z.number()
  35. });
  36. // 设置集成测试钩子
  37. setupIntegrationDatabaseHooksWithEntities([TestEntity])
  38. describe('共享CRUD租户隔离集成测试', () => {
  39. let client: any;
  40. let testToken1: string;
  41. let testToken2: string;
  42. let mockAuthMiddleware: any;
  43. beforeEach(async () => {
  44. // 获取数据源
  45. const dataSource = await IntegrationTestDatabase.getDataSource();
  46. // 生成测试用户的token
  47. testToken1 = JWTUtil.generateToken({
  48. id: 1,
  49. username: 'tenant1_user',
  50. roles: [{name:'user'}],
  51. tenantId: 1
  52. });
  53. testToken2 = JWTUtil.generateToken({
  54. id: 2,
  55. username: 'tenant2_user',
  56. roles: [{name:'user'}],
  57. tenantId: 2
  58. });
  59. // 创建模拟认证中间件
  60. mockAuthMiddleware = async (c: any, next: any) => {
  61. const authHeader = c.req.header('Authorization');
  62. if (authHeader && authHeader.startsWith('Bearer ')) {
  63. const token = authHeader.substring(7);
  64. try {
  65. const payload = JWTUtil.verifyToken(token);
  66. // 根据token确定租户ID
  67. let tenantId: number | undefined;
  68. if (token === testToken1) {
  69. tenantId = 1;
  70. } else if (token === testToken2) {
  71. tenantId = 2;
  72. }
  73. // 确保用户对象包含tenantId
  74. const userWithTenant = { ...payload, tenantId };
  75. c.set('user', userWithTenant);
  76. // 设置租户上下文
  77. c.set('tenantId', tenantId);
  78. } catch (error) {
  79. // token解析失败
  80. }
  81. } else {
  82. // 没有认证信息,返回401
  83. return c.json({ code: 401, message: '认证失败' }, 401);
  84. }
  85. await next();
  86. };
  87. // 创建测试路由 - 启用租户隔离
  88. const testRoutes = createCrudRoutes({
  89. entity: TestEntity,
  90. createSchema: createTestSchema,
  91. updateSchema: updateTestSchema,
  92. getSchema: getTestSchema,
  93. listSchema: listTestSchema,
  94. middleware: [mockAuthMiddleware],
  95. tenantOptions: {
  96. enabled: true,
  97. tenantIdField: 'tenantId',
  98. autoExtractFromContext: true
  99. }
  100. });
  101. client = testClient(testRoutes);
  102. });
  103. describe('GET / - 列表查询租户隔离', () => {
  104. it('应该只返回当前租户的数据', async () => {
  105. // 创建测试数据
  106. const dataSource = await IntegrationTestDatabase.getDataSource();
  107. const testRepository = dataSource.getRepository(TestEntity);
  108. // 为租户1创建数据
  109. const tenant1Data1 = testRepository.create({
  110. name: '租户1的数据1',
  111. tenantId: 1
  112. });
  113. await testRepository.save(tenant1Data1);
  114. const tenant1Data2 = testRepository.create({
  115. name: '租户1的数据2',
  116. tenantId: 1
  117. });
  118. await testRepository.save(tenant1Data2);
  119. // 为租户2创建数据
  120. const tenant2Data = testRepository.create({
  121. name: '租户2的数据',
  122. tenantId: 2
  123. });
  124. await testRepository.save(tenant2Data);
  125. // 租户1用户查询列表
  126. const response = await client.index.$get({
  127. query: {
  128. page: 1,
  129. pageSize: 10
  130. }
  131. }, {
  132. headers: {
  133. 'Authorization': `Bearer ${testToken1}`
  134. }
  135. });
  136. console.debug('租户隔离列表查询响应状态:', response.status);
  137. if (response.status !== 200) {
  138. const errorData = await response.json();
  139. console.debug('租户隔离列表查询错误信息:', errorData);
  140. }
  141. expect(response.status).toBe(200);
  142. if (response.status === 200) {
  143. const data = await response.json();
  144. expect(data).toHaveProperty('data');
  145. expect(Array.isArray(data.data)).toBe(true);
  146. expect(data.data).toHaveLength(2); // 应该只返回租户1的2条数据
  147. // 验证所有返回的数据都属于租户1
  148. data.data.forEach((item: any) => {
  149. expect(item.tenantId).toBe(1);
  150. });
  151. }
  152. });
  153. it('应该拒绝未认证用户的访问', async () => {
  154. const response = await client.index.$get({
  155. query: {
  156. page: 1,
  157. pageSize: 10
  158. }
  159. });
  160. expect(response.status).toBe(401);
  161. });
  162. });
  163. describe('POST / - 创建操作租户验证', () => {
  164. it('应该成功创建属于当前租户的数据', async () => {
  165. const createData = {
  166. name: '测试创建数据'
  167. };
  168. const response = await client.index.$post({
  169. json: createData
  170. }, {
  171. headers: {
  172. 'Authorization': `Bearer ${testToken1}`
  173. }
  174. });
  175. console.debug('租户隔离创建数据响应状态:', response.status);
  176. expect(response.status).toBe(201);
  177. if (response.status === 201) {
  178. const data = await response.json();
  179. expect(data).toHaveProperty('id');
  180. expect(data.name).toBe(createData.name);
  181. expect(data.tenantId).toBe(1); // 应该自动设置为租户1
  182. }
  183. });
  184. });
  185. describe('GET /:id - 获取详情租户验证', () => {
  186. it('应该成功获取属于当前租户的数据详情', async () => {
  187. // 先创建测试数据
  188. const dataSource = await IntegrationTestDatabase.getDataSource();
  189. const testRepository = dataSource.getRepository(TestEntity);
  190. const testData = testRepository.create({
  191. name: '测试数据详情',
  192. tenantId: 1
  193. });
  194. await testRepository.save(testData);
  195. const response = await client[':id'].$get({
  196. param: { id: testData.id }
  197. }, {
  198. headers: {
  199. 'Authorization': `Bearer ${testToken1}`
  200. }
  201. });
  202. console.debug('租户隔离获取详情响应状态:', response.status);
  203. if (response.status !== 200) {
  204. const errorData = await response.json();
  205. console.debug('租户隔离获取详情错误信息:', errorData);
  206. }
  207. expect(response.status).toBe(200);
  208. if (response.status === 200) {
  209. const data = await response.json();
  210. expect(data.id).toBe(testData.id);
  211. expect(data.name).toBe(testData.name);
  212. expect(data.tenantId).toBe(1);
  213. }
  214. });
  215. it('应该拒绝获取不属于当前租户的数据详情', async () => {
  216. // 先创建属于租户2的数据
  217. const dataSource = await IntegrationTestDatabase.getDataSource();
  218. const testRepository = dataSource.getRepository(TestEntity);
  219. const testData = testRepository.create({
  220. name: '租户2的数据',
  221. tenantId: 2
  222. });
  223. await testRepository.save(testData);
  224. // 租户1用户尝试获取租户2的数据
  225. const response = await client[':id'].$get({
  226. param: { id: testData.id }
  227. }, {
  228. headers: {
  229. 'Authorization': `Bearer ${testToken1}`
  230. }
  231. });
  232. console.debug('租户隔离获取无权详情响应状态:', response.status);
  233. expect(response.status).toBe(404); // 应该返回404而不是403
  234. });
  235. });
  236. describe('PUT /:id - 更新操作租户验证', () => {
  237. it('应该成功更新属于当前租户的数据', async () => {
  238. // 先创建测试数据
  239. const dataSource = await IntegrationTestDatabase.getDataSource();
  240. const testRepository = dataSource.getRepository(TestEntity);
  241. const testData = testRepository.create({
  242. name: '原始数据',
  243. tenantId: 1
  244. });
  245. await testRepository.save(testData);
  246. const updateData = {
  247. name: '更新后的数据'
  248. };
  249. const response = await client[':id'].$put({
  250. param: { id: testData.id },
  251. json: updateData
  252. }, {
  253. headers: {
  254. 'Authorization': `Bearer ${testToken1}`
  255. }
  256. });
  257. console.debug('租户隔离更新数据响应状态:', response.status);
  258. expect(response.status).toBe(200);
  259. if (response.status === 200) {
  260. const data = await response.json();
  261. expect(data.name).toBe(updateData.name);
  262. expect(data.tenantId).toBe(1);
  263. }
  264. });
  265. it('应该拒绝更新不属于当前租户的数据', async () => {
  266. // 先创建属于租户2的数据
  267. const dataSource = await IntegrationTestDatabase.getDataSource();
  268. const testRepository = dataSource.getRepository(TestEntity);
  269. const testData = testRepository.create({
  270. name: '租户2的数据',
  271. tenantId: 2
  272. });
  273. await testRepository.save(testData);
  274. const updateData = {
  275. name: '尝试更新的数据'
  276. };
  277. // 租户1用户尝试更新租户2的数据
  278. const response = await client[':id'].$put({
  279. param: { id: testData.id },
  280. json: updateData
  281. }, {
  282. headers: {
  283. 'Authorization': `Bearer ${testToken1}`
  284. }
  285. });
  286. console.debug('租户隔离更新无权数据响应状态:', response.status);
  287. expect(response.status).toBe(404); // 应该返回404而不是403
  288. });
  289. });
  290. describe('DELETE /:id - 删除操作租户验证', () => {
  291. it('应该成功删除属于当前租户的数据', async () => {
  292. // 先创建测试数据
  293. const dataSource = await IntegrationTestDatabase.getDataSource();
  294. const testRepository = dataSource.getRepository(TestEntity);
  295. const testData = testRepository.create({
  296. name: '待删除数据',
  297. tenantId: 1
  298. });
  299. await testRepository.save(testData);
  300. const response = await client[':id'].$delete({
  301. param: { id: testData.id }
  302. }, {
  303. headers: {
  304. 'Authorization': `Bearer ${testToken1}`
  305. }
  306. });
  307. console.debug('租户隔离删除数据响应状态:', response.status);
  308. expect(response.status).toBe(204);
  309. // 验证数据确实被删除
  310. const deletedData = await testRepository.findOne({
  311. where: { id: testData.id }
  312. });
  313. expect(deletedData).toBeNull();
  314. });
  315. it('应该拒绝删除不属于当前租户的数据', async () => {
  316. // 先创建属于租户2的数据
  317. const dataSource = await IntegrationTestDatabase.getDataSource();
  318. const testRepository = dataSource.getRepository(TestEntity);
  319. const testData = testRepository.create({
  320. name: '租户2的数据',
  321. tenantId: 2
  322. });
  323. await testRepository.save(testData);
  324. // 租户1用户尝试删除租户2的数据
  325. const response = await client[':id'].$delete({
  326. param: { id: testData.id }
  327. }, {
  328. headers: {
  329. 'Authorization': `Bearer ${testToken1}`
  330. }
  331. });
  332. console.debug('租户隔离删除无权数据响应状态:', response.status);
  333. expect(response.status).toBe(404); // 应该返回404而不是403
  334. // 验证数据没有被删除
  335. const existingData = await testRepository.findOne({
  336. where: { id: testData.id }
  337. });
  338. expect(existingData).not.toBeNull();
  339. });
  340. });
  341. describe('禁用租户隔离的情况', () => {
  342. it('当租户隔离禁用时应该允许跨租户访问', async () => {
  343. // 创建禁用租户隔离的路由
  344. const noTenantRoutes = createCrudRoutes({
  345. entity: TestEntity,
  346. createSchema: createTestSchema,
  347. updateSchema: updateTestSchema,
  348. getSchema: getTestSchema,
  349. listSchema: listTestSchema,
  350. middleware: [mockAuthMiddleware],
  351. tenantOptions: {
  352. enabled: false, // 禁用租户隔离
  353. tenantIdField: 'tenantId',
  354. autoExtractFromContext: true
  355. }
  356. });
  357. const noTenantClient = testClient(noTenantRoutes);
  358. // 创建属于租户2的数据
  359. const dataSource = await IntegrationTestDatabase.getDataSource();
  360. const testRepository = dataSource.getRepository(TestEntity);
  361. const testData = testRepository.create({
  362. name: '租户2的数据',
  363. tenantId: 2
  364. });
  365. await testRepository.save(testData);
  366. // 租户1用户应该能够访问租户2的数据(租户隔离已禁用)
  367. const response = await noTenantClient[':id'].$get({
  368. param: { id: testData.id }
  369. }, {
  370. headers: {
  371. 'Authorization': `Bearer ${testToken1}`
  372. }
  373. });
  374. console.debug('禁用租户隔离时的响应状态:', response.status);
  375. if (response.status !== 200) {
  376. try {
  377. const errorData = await response.json();
  378. console.debug('禁用租户隔离时的错误信息:', errorData);
  379. } catch (e) {
  380. const text = await response.text();
  381. console.debug('禁用租户隔离时的响应文本:', text);
  382. }
  383. }
  384. expect(response.status).toBe(200);
  385. if (response.status === 200) {
  386. const data = await response.json();
  387. expect(data.id).toBe(testData.id);
  388. expect(data.tenantId).toBe(2);
  389. }
  390. });
  391. it('当不传递tenantOptions配置时应该允许跨租户访问', async () => {
  392. // 创建不传递租户隔离配置的路由
  393. const noTenantRoutes = createCrudRoutes({
  394. entity: TestEntity,
  395. createSchema: createTestSchema,
  396. updateSchema: updateTestSchema,
  397. getSchema: getTestSchema,
  398. listSchema: listTestSchema,
  399. middleware: [mockAuthMiddleware]
  400. // 不传递 tenantOptions 配置
  401. });
  402. const noTenantClient = testClient(noTenantRoutes);
  403. // 创建属于租户2的数据
  404. const dataSource = await IntegrationTestDatabase.getDataSource();
  405. const testRepository = dataSource.getRepository(TestEntity);
  406. const testData = testRepository.create({
  407. name: '租户2的数据(无租户配置)',
  408. tenantId: 2
  409. });
  410. await testRepository.save(testData);
  411. // 租户1用户应该能够访问租户2的数据(没有租户隔离配置)
  412. console.debug('测试数据ID(无租户配置):', testData.id);
  413. const response = await noTenantClient[':id'].$get({
  414. param: { id: testData.id }
  415. }, {
  416. headers: {
  417. 'Authorization': `Bearer ${testToken1}`
  418. }
  419. });
  420. console.debug('无租户配置时的响应状态:', response.status);
  421. expect(response.status).toBe(200);
  422. if (response.status === 200) {
  423. const data = await response.json();
  424. expect(data.id).toBe(testData.id);
  425. expect(data.tenantId).toBe(2);
  426. }
  427. });
  428. });
  429. });