admin-goods-parent-child.integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import { describe, it, expect, beforeEach } 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 { SupplierMt } from '@d8d/supplier-module-mt';
  8. import { MerchantMt } from '@d8d/merchant-module-mt';
  9. import { adminGoodsRoutesMt } from '../../src/routes/index.mt';
  10. import { GoodsMt, GoodsCategoryMt } from '../../src/entities/index.mt';
  11. import { GoodsTestFactory } from '../factories/goods-test-factory';
  12. // 设置集成测试钩子
  13. setupIntegrationDatabaseHooksWithEntities([
  14. UserEntityMt, RoleMt, GoodsMt, GoodsCategoryMt, FileMt, SupplierMt, MerchantMt
  15. ])
  16. describe('管理员父子商品管理API集成测试', () => {
  17. let client: ReturnType<typeof testClient<typeof adminGoodsRoutesMt>>;
  18. let adminToken: string;
  19. let testUser: UserEntityMt;
  20. let testAdmin: UserEntityMt;
  21. let testCategory: GoodsCategoryMt;
  22. let testSupplier: SupplierMt;
  23. let testMerchant: MerchantMt;
  24. let testFactory: GoodsTestFactory;
  25. let parentGoods: GoodsMt;
  26. let childGoods1: GoodsMt;
  27. let childGoods2: GoodsMt;
  28. beforeEach(async () => {
  29. // 创建测试客户端
  30. client = testClient(adminGoodsRoutesMt);
  31. // 获取数据源并创建测试工厂
  32. const dataSource = await IntegrationTestDatabase.getDataSource();
  33. testFactory = new GoodsTestFactory(dataSource);
  34. // 使用测试工厂创建测试数据
  35. testUser = await testFactory.createTestUser();
  36. testAdmin = await testFactory.createTestAdmin();
  37. testCategory = await testFactory.createTestCategory(testUser.id);
  38. testSupplier = await testFactory.createTestSupplier(testUser.id);
  39. testMerchant = await testFactory.createTestMerchant(testUser.id);
  40. // 生成测试管理员的token
  41. adminToken = JWTUtil.generateToken({
  42. id: testAdmin.id,
  43. username: testAdmin.username,
  44. roles: [{name:'admin'}]
  45. });
  46. // 创建父商品
  47. parentGoods = await testFactory.createTestGoods(testUser.id, {
  48. name: '父商品测试',
  49. price: 200.00,
  50. costPrice: 150.00,
  51. categoryId1: testCategory.id,
  52. categoryId2: testCategory.id,
  53. categoryId3: testCategory.id,
  54. supplierId: testSupplier.id,
  55. merchantId: testMerchant.id,
  56. stock: 100,
  57. spuId: 0, // 父商品
  58. spuName: null
  59. });
  60. // 创建子商品1
  61. childGoods1 = await testFactory.createTestGoods(testUser.id, {
  62. name: '子商品1 - 红色',
  63. price: 210.00,
  64. costPrice: 160.00,
  65. categoryId1: testCategory.id,
  66. categoryId2: testCategory.id,
  67. categoryId3: testCategory.id,
  68. supplierId: testSupplier.id,
  69. merchantId: testMerchant.id,
  70. stock: 50,
  71. spuId: parentGoods.id, // 父商品ID
  72. spuName: parentGoods.name
  73. });
  74. // 创建子商品2
  75. childGoods2 = await testFactory.createTestGoods(testUser.id, {
  76. name: '子商品2 - 蓝色',
  77. price: 220.00,
  78. costPrice: 170.00,
  79. categoryId1: testCategory.id,
  80. categoryId2: testCategory.id,
  81. categoryId3: testCategory.id,
  82. supplierId: testSupplier.id,
  83. merchantId: testMerchant.id,
  84. stock: 60,
  85. spuId: parentGoods.id, // 父商品ID
  86. spuName: parentGoods.name
  87. });
  88. });
  89. describe('GET /goods/:id/children', () => {
  90. it('应该成功获取父商品的子商品列表', async () => {
  91. const response = await client[':id']['children'].$get({
  92. param: { id: parentGoods.id },
  93. query: { page: 1, pageSize: 10 }
  94. }, {
  95. headers: {
  96. 'Authorization': `Bearer ${adminToken}`
  97. }
  98. });
  99. expect(response.status).toBe(200);
  100. if (response.status === 200 ){
  101. const data = await response.json();
  102. expect(data.data).toHaveLength(2);
  103. expect(data.total).toBe(2);
  104. expect(data.page).toBe(1);
  105. expect(data.pageSize).toBe(10);
  106. expect(data.totalPages).toBe(1);
  107. // 验证子商品数据
  108. const childIds = data.data.map((item) => item.id);
  109. expect(childIds).toContain(childGoods1.id);
  110. expect(childIds).toContain(childGoods2.id);
  111. }
  112. });
  113. it('应该验证父商品是否存在', async () => {
  114. const response = await client[':id']['children'].$get({
  115. param: { id: 99999 }, // 不存在的商品ID
  116. query: { page: 1, pageSize: 10 }
  117. }, {
  118. headers: {
  119. 'Authorization': `Bearer ${adminToken}`
  120. }
  121. });
  122. expect(response.status).toBe(404);
  123. if (response.status === 404) {
  124. const data = await response.json();
  125. expect(data.code).toBe(404);
  126. expect(data.message).toContain('父商品不存在');
  127. }
  128. });
  129. it('应该支持搜索关键词过滤', async () => {
  130. const response = await client[':id']['children'].$get({
  131. param: { id: parentGoods.id },
  132. query: { page: 1, pageSize: 10, keyword: '红色' }
  133. }, {
  134. headers: {
  135. 'Authorization': `Bearer ${adminToken}`
  136. }
  137. });
  138. expect(response.status).toBe(200);
  139. if (response.status === 200) {
  140. const data = await response.json();
  141. expect(data.data).toHaveLength(1);
  142. expect(data.data[0].name).toBe('子商品1 - 红色');
  143. }
  144. });
  145. it('应该支持排序', async () => {
  146. const response = await client[':id']['children'].$get({
  147. param: { id: parentGoods.id },
  148. query: { page: 1, pageSize: 10, sortBy: 'price', sortOrder: 'DESC' }
  149. }, {
  150. headers: {
  151. 'Authorization': `Bearer ${adminToken}`
  152. }
  153. });
  154. expect(response.status).toBe(200);
  155. if (response.status === 200) {
  156. const data = await response.json();
  157. expect(data.data).toHaveLength(2);
  158. // 价格降序排列:220 > 210
  159. expect(data.data[0].price).toBe(220.00);
  160. expect(data.data[1].price).toBe(210.00);
  161. }
  162. });
  163. });
  164. describe('POST /goods/:id/set-as-parent', () => {
  165. it('应该成功将普通商品设为父商品', async () => {
  166. // 创建一个普通商品(不是子商品)
  167. const normalGoods = await testFactory.createTestGoods(testUser.id, {
  168. name: '普通商品',
  169. price: 300.00,
  170. costPrice: 250.00,
  171. categoryId1: testCategory.id,
  172. categoryId2: testCategory.id,
  173. categoryId3: testCategory.id,
  174. supplierId: testSupplier.id,
  175. merchantId: testMerchant.id,
  176. stock: 80,
  177. spuId: 0,
  178. spuName: null
  179. });
  180. const response = await client[':id']['set-as-parent'].$post({
  181. param: { id: normalGoods.id }
  182. }, {
  183. headers: {
  184. 'Authorization': `Bearer ${adminToken}`
  185. }
  186. });
  187. expect(response.status).toBe(200);
  188. if (response.status === 200) {
  189. const data = await response.json();
  190. expect(data.id).toBe(normalGoods.id);
  191. expect(data.spuId).toBe(0);
  192. expect(data.spuName).toBeUndefined(); // spuName字段已从API响应中移除
  193. }
  194. });
  195. it('应该拒绝将子商品设为父商品', async () => {
  196. const response = await client[':id']['set-as-parent'].$post({
  197. param: { id: childGoods1.id }
  198. }, {
  199. headers: {
  200. 'Authorization': `Bearer ${adminToken}`
  201. }
  202. });
  203. expect(response.status).toBe(400);
  204. if (response.status === 400) {
  205. const data = await response.json();
  206. expect(data.code).toBe(400);
  207. expect(data.message).toContain('子商品不能设为父商品');
  208. }
  209. });
  210. it('应该验证商品是否存在', async () => {
  211. const response = await client[':id']['set-as-parent'].$post({
  212. param: { id: 99999 }
  213. }, {
  214. headers: {
  215. 'Authorization': `Bearer ${adminToken}`
  216. }
  217. });
  218. expect(response.status).toBe(404);
  219. if (response.status === 404) {
  220. const data = await response.json();
  221. expect(data.code).toBe(404);
  222. expect(data.message).toContain('商品不存在');
  223. }
  224. });
  225. });
  226. describe('DELETE /goods/:id/parent', () => {
  227. it('应该成功解除子商品的父子关系', async () => {
  228. const response = await client[':id']['parent'].$delete({
  229. param: { id: childGoods1.id }
  230. }, {
  231. headers: {
  232. 'Authorization': `Bearer ${adminToken}`
  233. }
  234. });
  235. expect(response.status).toBe(200);
  236. if (response.status === 200) {
  237. const data = await response.json();
  238. expect(data.id).toBe(childGoods1.id);
  239. expect(data.spuId).toBe(0);
  240. expect(data.spuName).toBeUndefined(); // spuName字段已从API响应中移除
  241. }
  242. });
  243. it('应该拒绝解除非子商品的父子关系', async () => {
  244. const response = await client[':id']['parent'].$delete({
  245. param: { id: parentGoods.id }
  246. }, {
  247. headers: {
  248. 'Authorization': `Bearer ${adminToken}`
  249. }
  250. });
  251. expect(response.status).toBe(400);
  252. if (response.status === 400) {
  253. const data = await response.json();
  254. expect(data.code).toBe(400);
  255. expect(data.message).toContain('该商品不是子商品');
  256. }
  257. });
  258. it('应该验证商品是否存在', async () => {
  259. const response = await client[':id']['parent'].$delete({
  260. param: { id: 99999 }
  261. }, {
  262. headers: {
  263. 'Authorization': `Bearer ${adminToken}`
  264. }
  265. });
  266. expect(response.status).toBe(404);
  267. if (response.status === 404) {
  268. const data = await response.json();
  269. expect(data.code).toBe(404);
  270. expect(data.message).toContain('商品不存在');
  271. }
  272. });
  273. });
  274. describe('POST /goods/batchCreateChildren', () => {
  275. it('应该成功批量创建子商品', async () => {
  276. const specs = [
  277. { name: '规格1 - 黑色', price: 230.00, costPrice: 180.00, stock: 50, sort: 1 },
  278. { name: '规格2 - 白色', price: 240.00, costPrice: 190.00, stock: 60, sort: 2 },
  279. { name: '规格3 - 金色', price: 250.00, costPrice: 200.00, stock: 70, sort: 3 }
  280. ];
  281. const response = await client.batchCreateChildren.$post({
  282. json: {
  283. parentGoodsId: parentGoods.id,
  284. specs
  285. }
  286. }, {
  287. headers: {
  288. 'Authorization': `Bearer ${adminToken}`
  289. }
  290. });
  291. expect(response.status).toBe(200);
  292. if (response.status === 200) {
  293. const data = await response.json();
  294. expect(data.success).toBe(true);
  295. expect(data.count).toBe(3);
  296. expect(data.children).toHaveLength(3);
  297. // 验证子商品数据
  298. data.children.forEach((child: any, index: number) => {
  299. expect(child.name).toBe(specs[index].name);
  300. expect(child.price).toBe(specs[index].price);
  301. expect(child.costPrice).toBe(specs[index].costPrice);
  302. expect(child.stock).toBe(specs[index].stock);
  303. expect(child.sort).toBe(specs[index].sort);
  304. expect(child.spuId).toBe(parentGoods.id);
  305. // spuName字段已从API响应中移除,改为通过parent对象获取父商品名称
  306. // expect(child.spuName).toBe(parentGoods.name);
  307. });
  308. }
  309. });
  310. it('应该验证父商品是否存在', async () => {
  311. const specs = [
  312. { name: '测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
  313. ];
  314. const response = await client.batchCreateChildren.$post({
  315. json: {
  316. parentGoodsId: 99999,
  317. specs
  318. }
  319. }, {
  320. headers: {
  321. 'Authorization': `Bearer ${adminToken}`
  322. }
  323. });
  324. expect(response.status).toBe(404);
  325. if (response.status === 404) {
  326. const data = await response.json();
  327. expect(data.code).toBe(404);
  328. expect(data.message).toContain('父商品不存在');
  329. }
  330. });
  331. it('应该验证父商品必须是父商品', async () => {
  332. // 尝试为子商品创建子商品
  333. const specs = [
  334. { name: '测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
  335. ];
  336. const response = await client.batchCreateChildren.$post({
  337. json: {
  338. parentGoodsId: childGoods1.id,
  339. specs
  340. }
  341. }, {
  342. headers: {
  343. 'Authorization': `Bearer ${adminToken}`
  344. }
  345. });
  346. expect(response.status).toBe(400);
  347. if (response.status === 400) {
  348. const data = await response.json();
  349. expect(data.code).toBe(400);
  350. expect(data.message).toContain('只能为父商品创建子商品');
  351. }
  352. });
  353. it('应该验证规格数据有效性', async () => {
  354. const specs = [
  355. { name: '', price: -100, costPrice: -80, stock: -10, sort: 1 } // 无效数据
  356. ];
  357. const response = await client.batchCreateChildren.$post({
  358. json: {
  359. parentGoodsId: parentGoods.id,
  360. specs
  361. }
  362. }, {
  363. headers: {
  364. 'Authorization': `Bearer ${adminToken}`
  365. }
  366. });
  367. expect(response.status).toBe(400);
  368. if (response.status === 400) {
  369. const data = await response.json();
  370. console.debug('验证规格数据有效性测试 - 响应状态:', response.status);
  371. console.debug('验证规格数据有效性测试 - 响应数据:', data);
  372. // Zod验证错误返回 { success: false, error: { name: 'ZodError', message: '...' } } 格式
  373. // 业务逻辑错误返回 { code: 400, message: '...' } 格式
  374. if ('success' in data && data.success === false) {
  375. expect(data.error.message).toMatch(/规格名称不能为空/);
  376. } else if ('code' in data) {
  377. expect(data.code).toBe(400);
  378. expect(data.message).toContain('规格名称不能为空');
  379. }
  380. }
  381. });
  382. it('应该继承父商品的分类和其他信息', async () => {
  383. const specs = [
  384. { name: '继承测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
  385. ];
  386. const response = await client.batchCreateChildren.$post({
  387. json: {
  388. parentGoodsId: parentGoods.id,
  389. specs
  390. }
  391. }, {
  392. headers: {
  393. 'Authorization': `Bearer ${adminToken}`
  394. }
  395. });
  396. expect(response.status).toBe(200);
  397. if (response.status === 200) {
  398. const data = await response.json();
  399. expect(data.success).toBe(true);
  400. expect(data.count).toBe(1);
  401. const child = data.children[0];
  402. expect(child.categoryId1).toBe(parentGoods.categoryId1);
  403. expect(child.categoryId2).toBe(parentGoods.categoryId2);
  404. expect(child.categoryId3).toBe(parentGoods.categoryId3);
  405. expect(child.supplierId).toBe(parentGoods.supplierId);
  406. expect(child.merchantId).toBe(parentGoods.merchantId);
  407. expect(child.goodsType).toBe(parentGoods.goodsType);
  408. }
  409. });
  410. });
  411. describe('认证和授权', () => {
  412. it('应该要求认证', async () => {
  413. const response = await client[':id']['children'].$get({
  414. param: { id: parentGoods.id },
  415. query: { page: 1, pageSize: 10 }
  416. });
  417. expect(response.status).toBe(401);
  418. });
  419. it('应该验证租户隔离', async () => {
  420. // 创建另一个租户的用户
  421. const dataSource = await IntegrationTestDatabase.getDataSource();
  422. const otherTenantUser = await testFactory.createTestUser(2); // 不同租户
  423. const otherTenantToken = JWTUtil.generateToken({
  424. id: otherTenantUser.id,
  425. username: otherTenantUser.username,
  426. roles: [{name:'admin'}]
  427. });
  428. const response = await client[':id']['children'].$get({
  429. param: { id: parentGoods.id },
  430. query: { page: 1, pageSize: 10 }
  431. }, {
  432. headers: {
  433. 'Authorization': `Bearer ${otherTenantToken}`
  434. }
  435. });
  436. // 不同租户应该看不到其他租户的数据
  437. expect(response.status).toBe(404);
  438. });
  439. });
  440. });