user-routes.integration.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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 { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
  6. import { FileMt } from '@d8d/file-module-mt';
  7. import { userMerchantRoutes } from '../../src/routes/user-routes.mt';
  8. import { MerchantMt } from '../../src/entities/merchant.mt.entity';
  9. import { MerchantTestUtils } from '../utils/test-utils';
  10. // 设置集成测试钩子
  11. setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, MerchantMt, FileMt])
  12. describe('用户商户管理API集成测试', () => {
  13. let client: ReturnType<typeof testClient<typeof userMerchantRoutes>>;
  14. let userToken: string;
  15. let otherUserToken: string;
  16. let testUser: UserEntityMt;
  17. let otherUser: UserEntityMt;
  18. beforeEach(async () => {
  19. // 创建测试客户端
  20. client = testClient(userMerchantRoutes);
  21. // 创建测试用户
  22. testUser = await MerchantTestUtils.createTestUser({ tenantId: 1 });
  23. otherUser = await MerchantTestUtils.createTestUser({
  24. username: `other_user_${Date.now()}`,
  25. nickname: '其他用户',
  26. tenantId: 1
  27. });
  28. // 生成测试用户的token
  29. userToken = JWTUtil.generateToken({
  30. id: testUser.id,
  31. username: testUser.username,
  32. roles: [{name:'user'}],
  33. tenantId: 1
  34. });
  35. // 生成其他用户的token
  36. otherUserToken = JWTUtil.generateToken({
  37. id: otherUser.id,
  38. username: otherUser.username,
  39. roles: [{name:'user'}],
  40. tenantId: 1
  41. });
  42. });
  43. describe('GET /merchants', () => {
  44. it('应该返回当前用户的商户列表', async () => {
  45. // 使用测试工具创建商户数据
  46. const { userMerchants, otherUserMerchants } = await MerchantTestUtils.createTestMerchantDataSet(
  47. testUser.id,
  48. otherUser.id,
  49. 1
  50. );
  51. const response = await client.index.$get({
  52. query: {}
  53. }, {
  54. headers: {
  55. 'Authorization': `Bearer ${userToken}`
  56. }
  57. });
  58. console.debug('用户商户列表响应状态:', response.status);
  59. expect(response.status).toBe(200);
  60. if (response.status === 200) {
  61. const data = await response.json();
  62. if (data && 'data' in data) {
  63. expect(Array.isArray(data.data)).toBe(true);
  64. // 应该只返回当前用户的商户
  65. data.data.forEach((merchant: any) => {
  66. expect(merchant.createdBy).toBe(testUser.id);
  67. expect(merchant.tenantId).toBe(1);
  68. });
  69. }
  70. }
  71. });
  72. it('应该拒绝未认证用户的访问', async () => {
  73. const response = await client.index.$get({
  74. query: {}
  75. });
  76. expect(response.status).toBe(401);
  77. });
  78. });
  79. describe('POST /merchants', () => {
  80. it('应该成功创建商户并自动使用当前用户ID', async () => {
  81. const createData = {
  82. name: '新商户',
  83. username: `new_${Date.now()}`,
  84. password: 'password123',
  85. phone: '13800138000',
  86. realname: '张三',
  87. state: 1,
  88. tenantId: 1
  89. };
  90. const response = await client.index.$post({
  91. json: createData
  92. }, {
  93. headers: {
  94. 'Authorization': `Bearer ${userToken}`
  95. }
  96. });
  97. console.debug('用户创建商户响应状态:', response.status);
  98. expect(response.status).toBe(201);
  99. if (response.status === 201) {
  100. const data = await response.json();
  101. console.debug('用户创建商户响应数据:', JSON.stringify(data, null, 2));
  102. expect(data).toHaveProperty('id');
  103. expect(data.createdBy).toBe(testUser.id); // 自动使用当前用户ID
  104. expect(data.tenantId).toBe(1); // 自动使用租户ID
  105. expect(data.name).toBe(createData.name);
  106. expect(data.username).toBe(createData.username);
  107. expect(data.phone).toBe(createData.phone);
  108. expect(data.realname).toBe(createData.realname);
  109. }
  110. });
  111. it('应该验证创建商户的必填字段', async () => {
  112. const invalidData = {
  113. // 缺少必填字段
  114. name: '',
  115. username: '',
  116. password: ''
  117. };
  118. const response = await client.index.$post({
  119. json: invalidData
  120. }, {
  121. headers: {
  122. 'Authorization': `Bearer ${userToken}`
  123. }
  124. });
  125. expect(response.status).toBe(400);
  126. });
  127. });
  128. describe('GET /merchants/:id', () => {
  129. it('应该返回当前用户的商户详情', async () => {
  130. // 使用测试工具创建商户
  131. const testMerchant = await MerchantTestUtils.createTestMerchant(testUser.id, 1);
  132. const response = await client[':id'].$get({
  133. param: { id: testMerchant.id }
  134. }, {
  135. headers: {
  136. 'Authorization': `Bearer ${userToken}`
  137. }
  138. });
  139. console.debug('用户商户详情响应状态:', response.status);
  140. expect(response.status).toBe(200);
  141. if (response.status === 200) {
  142. const data = await response.json();
  143. expect(data.id).toBe(testMerchant.id);
  144. expect(data.createdBy).toBe(testUser.id);
  145. expect(data.tenantId).toBe(1);
  146. expect(data.name).toBe(testMerchant.name);
  147. expect(data.username).toBe(testMerchant.username);
  148. expect(data.phone).toBe(testMerchant.phone);
  149. expect(data.realname).toBe(testMerchant.realname);
  150. }
  151. });
  152. it('应该拒绝访问其他用户的商户', async () => {
  153. // 为其他用户创建商户
  154. const otherUserMerchant = await MerchantTestUtils.createTestMerchant(otherUser.id, 1);
  155. // 当前用户尝试访问其他用户的商户
  156. const response = await client[':id'].$get({
  157. param: { id: otherUserMerchant.id }
  158. }, {
  159. headers: {
  160. 'Authorization': `Bearer ${userToken}`
  161. }
  162. });
  163. console.debug('用户访问其他用户商户响应状态:', response.status);
  164. expect(response.status).toBe(404); // 应该返回404,而不是403
  165. });
  166. it('应该处理不存在的商户', async () => {
  167. const response = await client[':id'].$get({
  168. param: { id: 999999 }
  169. }, {
  170. headers: {
  171. 'Authorization': `Bearer ${userToken}`
  172. }
  173. });
  174. expect(response.status).toBe(404);
  175. });
  176. });
  177. describe('PUT /merchants/:id', () => {
  178. it('应该成功更新当前用户的商户', async () => {
  179. // 使用测试工具创建商户
  180. const testMerchant = await MerchantTestUtils.createTestMerchant(testUser.id, 1);
  181. const updateData = {
  182. name: '更新后的商户',
  183. phone: '13900139000',
  184. realname: '更新后的姓名',
  185. state: 2
  186. };
  187. const response = await client[':id'].$put({
  188. param: { id: testMerchant.id },
  189. json: updateData
  190. }, {
  191. headers: {
  192. 'Authorization': `Bearer ${userToken}`
  193. }
  194. });
  195. console.debug('用户更新商户响应状态:', response.status);
  196. expect(response.status).toBe(200);
  197. if (response.status === 200) {
  198. const data = await response.json();
  199. expect(data.name).toBe(updateData.name);
  200. expect(data.phone).toBe(updateData.phone);
  201. expect(data.realname).toBe(updateData.realname);
  202. expect(data.state).toBe(updateData.state);
  203. }
  204. });
  205. it('应该拒绝更新其他用户的商户', async () => {
  206. // 为其他用户创建商户
  207. const otherUserMerchant = await MerchantTestUtils.createTestMerchant(otherUser.id, 1);
  208. const updateData = {
  209. name: '尝试更新的商户',
  210. phone: '13900139001',
  211. realname: '尝试更新的姓名'
  212. };
  213. // 当前用户尝试更新其他用户的商户
  214. const response = await client[':id'].$put({
  215. param: { id: otherUserMerchant.id },
  216. json: updateData
  217. }, {
  218. headers: {
  219. 'Authorization': `Bearer ${userToken}`
  220. }
  221. });
  222. console.debug('用户更新其他用户商户响应状态:', response.status);
  223. expect(response.status).toBe(403); // 数据权限控制返回403
  224. });
  225. });
  226. describe('DELETE /merchants/:id', () => {
  227. it('应该成功删除当前用户的商户', async () => {
  228. // 使用测试工具创建商户
  229. const testMerchant = await MerchantTestUtils.createTestMerchant(testUser.id, 1);
  230. const response = await client[':id'].$delete({
  231. param: { id: testMerchant.id }
  232. }, {
  233. headers: {
  234. 'Authorization': `Bearer ${userToken}`
  235. }
  236. });
  237. console.debug('用户删除商户响应状态:', response.status);
  238. expect(response.status).toBe(204);
  239. // 验证商户确实被删除
  240. const dataSource = await IntegrationTestDatabase.getDataSource();
  241. const merchantRepository = dataSource.getRepository(MerchantMt);
  242. const deletedMerchant = await merchantRepository.findOne({
  243. where: { id: testMerchant.id }
  244. });
  245. expect(deletedMerchant).toBeNull();
  246. });
  247. it('应该拒绝删除其他用户的商户', async () => {
  248. // 为其他用户创建商户
  249. const otherUserMerchant = await MerchantTestUtils.createTestMerchant(otherUser.id, 1);
  250. // 当前用户尝试删除其他用户的商户
  251. const response = await client[':id'].$delete({
  252. param: { id: otherUserMerchant.id }
  253. }, {
  254. headers: {
  255. 'Authorization': `Bearer ${userToken}`
  256. }
  257. });
  258. console.debug('用户删除其他用户商户响应状态:', response.status);
  259. expect(response.status).toBe(403); // 数据权限控制返回403
  260. });
  261. });
  262. describe('数据权限验证', () => {
  263. it('用户应该只能访问和操作自己的数据', async () => {
  264. // 使用测试工具创建商户数据集
  265. const { userMerchants, otherUserMerchants } = await MerchantTestUtils.createTestMerchantDataSet(
  266. testUser.id,
  267. otherUser.id,
  268. 1
  269. );
  270. // 当前用户应该只能看到自己的商户
  271. const listResponse = await client.index.$get({
  272. query: {}
  273. }, {
  274. headers: {
  275. 'Authorization': `Bearer ${userToken}`
  276. }
  277. });
  278. expect(listResponse.status).toBe(200);
  279. const listData = await listResponse.json();
  280. if (listData && 'data' in listData) {
  281. expect(Array.isArray(listData.data)).toBe(true);
  282. // 应该只包含当前用户的商户
  283. listData.data.forEach((merchant: any) => {
  284. expect(merchant.createdBy).toBe(testUser.id);
  285. });
  286. }
  287. // 当前用户应该无法访问其他用户的商户详情
  288. const getResponse = await client[':id'].$get({
  289. param: { id: otherUserMerchants[0].id }
  290. }, {
  291. headers: {
  292. 'Authorization': `Bearer ${userToken}`
  293. }
  294. });
  295. expect(getResponse.status).toBe(404);
  296. // 当前用户应该无法更新其他用户的商户
  297. const updateResponse = await client[':id'].$put({
  298. param: { id: otherUserMerchants[0].id },
  299. json: { name: '尝试更新' }
  300. }, {
  301. headers: {
  302. 'Authorization': `Bearer ${userToken}`
  303. }
  304. });
  305. expect(updateResponse.status).toBe(403);
  306. // 当前用户应该无法删除其他用户的商户
  307. const deleteResponse = await client[':id'].$delete({
  308. param: { id: otherUserMerchants[0].id }
  309. }, {
  310. headers: {
  311. 'Authorization': `Bearer ${userToken}`
  312. }
  313. });
  314. expect(deleteResponse.status).toBe(403);
  315. });
  316. });
  317. describe('商户状态管理测试', () => {
  318. it('应该支持商户状态管理', async () => {
  319. // 创建启用状态的商户
  320. const createData = {
  321. name: '状态测试商户',
  322. username: `stm_${Date.now()}`,
  323. password: 'password123',
  324. phone: '13800138006',
  325. realname: '状态测试',
  326. state: 1, // 启用
  327. tenantId: 1
  328. };
  329. const createResponse = await client.index.$post({
  330. json: createData
  331. }, {
  332. headers: {
  333. 'Authorization': `Bearer ${userToken}`
  334. }
  335. });
  336. expect(createResponse.status).toBe(201);
  337. const createdMerchant = await createResponse.json();
  338. // 检查响应是否为错误对象
  339. if ('code' in createdMerchant && 'message' in createdMerchant) {
  340. throw new Error(`创建商户失败: ${createdMerchant.message}`);
  341. }
  342. expect(createdMerchant.state).toBe(1);
  343. // 更新为禁用状态
  344. const updateResponse = await client[':id'].$put({
  345. param: { id: createdMerchant.id },
  346. json: { state: 2 } // 禁用
  347. }, {
  348. headers: {
  349. 'Authorization': `Bearer ${userToken}`
  350. }
  351. });
  352. expect(updateResponse.status).toBe(200);
  353. const updatedMerchant = await updateResponse.json();
  354. // 检查响应是否为错误对象
  355. if ('code' in updatedMerchant && 'message' in updatedMerchant) {
  356. throw new Error(`更新商户失败: ${updatedMerchant.message}`);
  357. }
  358. expect(updatedMerchant.state).toBe(2);
  359. });
  360. });
  361. describe('商户登录统计功能测试', () => {
  362. it('应该支持商户登录统计字段', async () => {
  363. // 创建商户
  364. const createData = {
  365. name: '登录统计商户',
  366. username: `lsm_${Date.now()}`,
  367. password: 'password123',
  368. phone: '13800138007',
  369. realname: '登录统计',
  370. state: 1,
  371. tenantId: 1
  372. };
  373. const createResponse = await client.index.$post({
  374. json: createData
  375. }, {
  376. headers: {
  377. 'Authorization': `Bearer ${userToken}`
  378. }
  379. });
  380. expect(createResponse.status).toBe(201);
  381. const createdMerchant = await createResponse.json();
  382. // 检查响应是否为错误对象
  383. if ('code' in createdMerchant && 'message' in createdMerchant) {
  384. throw new Error(`创建商户失败: ${createdMerchant.message}`);
  385. }
  386. // 验证登录统计字段存在
  387. expect(createdMerchant).toHaveProperty('loginNum');
  388. expect(createdMerchant).toHaveProperty('loginTime');
  389. expect(createdMerchant).toHaveProperty('loginIp');
  390. expect(createdMerchant).toHaveProperty('lastLoginTime');
  391. expect(createdMerchant).toHaveProperty('lastLoginIp');
  392. // 初始值应该为0或null
  393. expect(createdMerchant.loginNum).toBe(0);
  394. expect(createdMerchant.loginTime).toBe(0);
  395. expect(createdMerchant.lastLoginTime).toBe(0);
  396. });
  397. });
  398. describe('租户隔离测试', () => {
  399. let tenant2UserToken: string;
  400. let tenant2User: UserEntityMt;
  401. beforeEach(async () => {
  402. // 创建租户2的用户
  403. tenant2User = await MerchantTestUtils.createTestUser({
  404. username: `tenant2_user_${Date.now()}`,
  405. nickname: '租户2用户',
  406. tenantId: 2
  407. });
  408. // 生成租户2用户的token
  409. tenant2UserToken = JWTUtil.generateToken({
  410. id: tenant2User.id,
  411. username: tenant2User.username,
  412. roles: [{name:'user'}],
  413. tenantId: 2
  414. });
  415. });
  416. it('应该隔离不同租户的商户数据', async () => {
  417. // 为两个租户创建商户
  418. const tenant1Merchant = await MerchantTestUtils.createTestMerchant(testUser.id, 1);
  419. const tenant2Merchant = await MerchantTestUtils.createTestMerchant(tenant2User.id, 2);
  420. // 租户1用户应该只能看到租户1的商户
  421. const tenant1Response = await client.index.$get({
  422. query: {}
  423. }, {
  424. headers: {
  425. 'Authorization': `Bearer ${userToken}`
  426. }
  427. });
  428. expect(tenant1Response.status).toBe(200);
  429. const tenant1Data = await tenant1Response.json();
  430. if (tenant1Data && 'data' in tenant1Data) {
  431. expect(Array.isArray(tenant1Data.data)).toBe(true);
  432. // 应该只包含租户1的商户
  433. tenant1Data.data.forEach((merchant: any) => {
  434. expect(merchant.tenantId).toBe(1);
  435. });
  436. }
  437. // 租户2用户应该只能看到租户2的商户
  438. const tenant2Response = await client.index.$get({
  439. query: {}
  440. }, {
  441. headers: {
  442. 'Authorization': `Bearer ${tenant2UserToken}`
  443. }
  444. });
  445. expect(tenant2Response.status).toBe(200);
  446. const tenant2Data = await tenant2Response.json();
  447. if (tenant2Data && 'data' in tenant2Data) {
  448. expect(Array.isArray(tenant2Data.data)).toBe(true);
  449. // 应该只包含租户2的商户
  450. tenant2Data.data.forEach((merchant: any) => {
  451. expect(merchant.tenantId).toBe(2);
  452. });
  453. }
  454. });
  455. it('应该拒绝跨租户访问商户详情', async () => {
  456. // 为租户2创建商户
  457. const tenant2Merchant = await MerchantTestUtils.createTestMerchant(tenant2User.id, 2);
  458. // 租户1用户尝试访问租户2的商户
  459. const response = await client[':id'].$get({
  460. param: { id: tenant2Merchant.id }
  461. }, {
  462. headers: {
  463. 'Authorization': `Bearer ${userToken}`
  464. }
  465. });
  466. console.debug('跨租户访问商户详情响应状态:', response.status);
  467. expect(response.status).toBe(404); // 应该返回404,而不是403
  468. });
  469. it('应该拒绝跨租户更新商户', async () => {
  470. // 为租户2创建商户
  471. const tenant2Merchant = await MerchantTestUtils.createTestMerchant(tenant2User.id, 2);
  472. const updateData = {
  473. name: '尝试跨租户更新',
  474. phone: '13900139011',
  475. realname: '尝试跨租户更新'
  476. };
  477. // 租户1用户尝试更新租户2的商户
  478. const response = await client[':id'].$put({
  479. param: { id: tenant2Merchant.id },
  480. json: updateData
  481. }, {
  482. headers: {
  483. 'Authorization': `Bearer ${userToken}`
  484. }
  485. });
  486. console.debug('跨租户更新商户响应状态:', response.status);
  487. expect(response.status).toBe(404); // 应该返回404,而不是403
  488. });
  489. it('应该拒绝跨租户删除商户', async () => {
  490. // 为租户2创建商户
  491. const tenant2Merchant = await MerchantTestUtils.createTestMerchant(tenant2User.id, 2);
  492. // 租户1用户尝试删除租户2的商户
  493. const response = await client[':id'].$delete({
  494. param: { id: tenant2Merchant.id }
  495. }, {
  496. headers: {
  497. 'Authorization': `Bearer ${userToken}`
  498. }
  499. });
  500. console.debug('跨租户删除商户响应状态:', response.status);
  501. expect(response.status).toBe(404); // 应该返回404,而不是403
  502. });
  503. });
  504. });