admin-routes.integration.test.ts 18 KB

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