user-routes.integration.test.ts 18 KB

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