admin-routes.integration.test.ts 18 KB

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