| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- import { describe, it, expect, beforeEach } from 'vitest';
- import { testClient } from 'hono/testing';
- import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
- import { JWTUtil } from '@d8d/shared-utils';
- import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
- import { FileMt } from '@d8d/file-module-mt';
- import { SupplierMt } from '@d8d/supplier-module-mt';
- import { MerchantMt } from '@d8d/merchant-module-mt';
- import { adminGoodsRoutesMt } from '../../src/routes/index.mt';
- import { GoodsMt, GoodsCategoryMt } from '../../src/entities/index.mt';
- import { GoodsTestFactory } from '../factories/goods-test-factory';
- // 设置集成测试钩子
- setupIntegrationDatabaseHooksWithEntities([
- UserEntityMt, RoleMt, GoodsMt, GoodsCategoryMt, FileMt, SupplierMt, MerchantMt
- ])
- describe('管理员父子商品管理API集成测试', () => {
- let client: ReturnType<typeof testClient<typeof adminGoodsRoutesMt>>;
- let adminToken: string;
- let testUser: UserEntityMt;
- let testAdmin: UserEntityMt;
- let testCategory: GoodsCategoryMt;
- let testSupplier: SupplierMt;
- let testMerchant: MerchantMt;
- let testFactory: GoodsTestFactory;
- let parentGoods: GoodsMt;
- let childGoods1: GoodsMt;
- let childGoods2: GoodsMt;
- beforeEach(async () => {
- // 创建测试客户端
- client = testClient(adminGoodsRoutesMt);
- // 获取数据源并创建测试工厂
- const dataSource = await IntegrationTestDatabase.getDataSource();
- testFactory = new GoodsTestFactory(dataSource);
- // 使用测试工厂创建测试数据
- testUser = await testFactory.createTestUser();
- testAdmin = await testFactory.createTestAdmin();
- testCategory = await testFactory.createTestCategory(testUser.id);
- testSupplier = await testFactory.createTestSupplier(testUser.id);
- testMerchant = await testFactory.createTestMerchant(testUser.id);
- // 生成测试管理员的token
- adminToken = JWTUtil.generateToken({
- id: testAdmin.id,
- username: testAdmin.username,
- roles: [{name:'admin'}]
- });
- // 创建父商品
- parentGoods = await testFactory.createTestGoods(testUser.id, {
- name: '父商品测试',
- price: 200.00,
- costPrice: 150.00,
- categoryId1: testCategory.id,
- categoryId2: testCategory.id,
- categoryId3: testCategory.id,
- supplierId: testSupplier.id,
- merchantId: testMerchant.id,
- stock: 100,
- spuId: 0, // 父商品
- spuName: null
- });
- // 创建子商品1
- childGoods1 = await testFactory.createTestGoods(testUser.id, {
- name: '子商品1 - 红色',
- price: 210.00,
- costPrice: 160.00,
- categoryId1: testCategory.id,
- categoryId2: testCategory.id,
- categoryId3: testCategory.id,
- supplierId: testSupplier.id,
- merchantId: testMerchant.id,
- stock: 50,
- spuId: parentGoods.id, // 父商品ID
- spuName: parentGoods.name
- });
- // 创建子商品2
- childGoods2 = await testFactory.createTestGoods(testUser.id, {
- name: '子商品2 - 蓝色',
- price: 220.00,
- costPrice: 170.00,
- categoryId1: testCategory.id,
- categoryId2: testCategory.id,
- categoryId3: testCategory.id,
- supplierId: testSupplier.id,
- merchantId: testMerchant.id,
- stock: 60,
- spuId: parentGoods.id, // 父商品ID
- spuName: parentGoods.name
- });
- });
- describe('GET /goods/:id/children', () => {
- it('应该成功获取父商品的子商品列表', async () => {
- const response = await client[':id']['children'].$get({
- param: { id: parentGoods.id },
- query: { page: 1, pageSize: 10 }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200 ){
- const data = await response.json();
- expect(data.data).toHaveLength(2);
- expect(data.total).toBe(2);
- expect(data.page).toBe(1);
- expect(data.pageSize).toBe(10);
- expect(data.totalPages).toBe(1);
- // 验证子商品数据
- const childIds = data.data.map((item) => item.id);
- expect(childIds).toContain(childGoods1.id);
- expect(childIds).toContain(childGoods2.id);
- }
- });
- it('应该验证父商品是否存在', async () => {
- const response = await client[':id']['children'].$get({
- param: { id: 99999 }, // 不存在的商品ID
- query: { page: 1, pageSize: 10 }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(404);
- if (response.status === 404) {
- const data = await response.json();
- expect(data.code).toBe(404);
- expect(data.message).toContain('父商品不存在');
- }
- });
- it('应该支持搜索关键词过滤', async () => {
- const response = await client[':id']['children'].$get({
- param: { id: parentGoods.id },
- query: { page: 1, pageSize: 10, keyword: '红色' }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.data).toHaveLength(1);
- expect(data.data[0].name).toBe('子商品1 - 红色');
- }
- });
- it('应该支持排序', async () => {
- const response = await client[':id']['children'].$get({
- param: { id: parentGoods.id },
- query: { page: 1, pageSize: 10, sortBy: 'price', sortOrder: 'DESC' }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.data).toHaveLength(2);
- // 价格降序排列:220 > 210
- expect(data.data[0].price).toBe(220.00);
- expect(data.data[1].price).toBe(210.00);
- }
- });
- });
- describe('POST /goods/:id/set-as-parent', () => {
- it('应该成功将普通商品设为父商品', async () => {
- // 创建一个普通商品(不是子商品)
- const normalGoods = await testFactory.createTestGoods(testUser.id, {
- name: '普通商品',
- price: 300.00,
- costPrice: 250.00,
- categoryId1: testCategory.id,
- categoryId2: testCategory.id,
- categoryId3: testCategory.id,
- supplierId: testSupplier.id,
- merchantId: testMerchant.id,
- stock: 80,
- spuId: 0,
- spuName: null
- });
- const response = await client[':id']['set-as-parent'].$post({
- param: { id: normalGoods.id }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.id).toBe(normalGoods.id);
- expect(data.spuId).toBe(0);
- expect(data.spuName).toBeUndefined(); // spuName字段已从API响应中移除
- }
- });
- it('应该拒绝将子商品设为父商品', async () => {
- const response = await client[':id']['set-as-parent'].$post({
- param: { id: childGoods1.id }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(400);
- if (response.status === 400) {
- const data = await response.json();
- expect(data.code).toBe(400);
- expect(data.message).toContain('子商品不能设为父商品');
- }
- });
- it('应该验证商品是否存在', async () => {
- const response = await client[':id']['set-as-parent'].$post({
- param: { id: 99999 }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(404);
- if (response.status === 404) {
- const data = await response.json();
- expect(data.code).toBe(404);
- expect(data.message).toContain('商品不存在');
- }
- });
- });
- describe('DELETE /goods/:id/parent', () => {
- it('应该成功解除子商品的父子关系', async () => {
- const response = await client[':id']['parent'].$delete({
- param: { id: childGoods1.id }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.id).toBe(childGoods1.id);
- expect(data.spuId).toBe(0);
- expect(data.spuName).toBeUndefined(); // spuName字段已从API响应中移除
- }
- });
- it('应该拒绝解除非子商品的父子关系', async () => {
- const response = await client[':id']['parent'].$delete({
- param: { id: parentGoods.id }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(400);
- if (response.status === 400) {
- const data = await response.json();
- expect(data.code).toBe(400);
- expect(data.message).toContain('该商品不是子商品');
- }
- });
- it('应该验证商品是否存在', async () => {
- const response = await client[':id']['parent'].$delete({
- param: { id: 99999 }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(404);
- if (response.status === 404) {
- const data = await response.json();
- expect(data.code).toBe(404);
- expect(data.message).toContain('商品不存在');
- }
- });
- });
- describe('POST /goods/batchCreateChildren', () => {
- it('应该成功批量创建子商品', async () => {
- const specs = [
- { name: '规格1 - 黑色', price: 230.00, costPrice: 180.00, stock: 50, sort: 1 },
- { name: '规格2 - 白色', price: 240.00, costPrice: 190.00, stock: 60, sort: 2 },
- { name: '规格3 - 金色', price: 250.00, costPrice: 200.00, stock: 70, sort: 3 }
- ];
- const response = await client.batchCreateChildren.$post({
- json: {
- parentGoodsId: parentGoods.id,
- specs
- }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.success).toBe(true);
- expect(data.count).toBe(3);
- expect(data.children).toHaveLength(3);
- // 验证子商品数据
- data.children.forEach((child: any, index: number) => {
- expect(child.name).toBe(specs[index].name);
- expect(child.price).toBe(specs[index].price);
- expect(child.costPrice).toBe(specs[index].costPrice);
- expect(child.stock).toBe(specs[index].stock);
- expect(child.sort).toBe(specs[index].sort);
- expect(child.spuId).toBe(parentGoods.id);
- // spuName字段已从API响应中移除,改为通过parent对象获取父商品名称
- // expect(child.spuName).toBe(parentGoods.name);
- });
- }
- });
- it('应该验证父商品是否存在', async () => {
- const specs = [
- { name: '测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
- ];
- const response = await client.batchCreateChildren.$post({
- json: {
- parentGoodsId: 99999,
- specs
- }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(404);
- if (response.status === 404) {
- const data = await response.json();
- expect(data.code).toBe(404);
- expect(data.message).toContain('父商品不存在');
- }
- });
- it('应该验证父商品必须是父商品', async () => {
- // 尝试为子商品创建子商品
- const specs = [
- { name: '测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
- ];
- const response = await client.batchCreateChildren.$post({
- json: {
- parentGoodsId: childGoods1.id,
- specs
- }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(400);
- if (response.status === 400) {
- const data = await response.json();
- expect(data.code).toBe(400);
- expect(data.message).toContain('只能为父商品创建子商品');
- }
- });
- it('应该验证规格数据有效性', async () => {
- const specs = [
- { name: '', price: -100, costPrice: -80, stock: -10, sort: 1 } // 无效数据
- ];
- const response = await client.batchCreateChildren.$post({
- json: {
- parentGoodsId: parentGoods.id,
- specs
- }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(400);
- if (response.status === 400) {
- const data = await response.json();
- console.debug('验证规格数据有效性测试 - 响应状态:', response.status);
- console.debug('验证规格数据有效性测试 - 响应数据:', data);
- // Zod验证错误返回 { success: false, error: { name: 'ZodError', message: '...' } } 格式
- // 业务逻辑错误返回 { code: 400, message: '...' } 格式
- if ('success' in data && data.success === false) {
- expect(data.error.message).toMatch(/规格名称不能为空/);
- } else if ('code' in data) {
- expect(data.code).toBe(400);
- expect(data.message).toContain('规格名称不能为空');
- }
- }
- });
- it('应该继承父商品的分类和其他信息', async () => {
- const specs = [
- { name: '继承测试规格', price: 100.00, costPrice: 80.00, stock: 10, sort: 1 }
- ];
- const response = await client.batchCreateChildren.$post({
- json: {
- parentGoodsId: parentGoods.id,
- specs
- }
- }, {
- headers: {
- 'Authorization': `Bearer ${adminToken}`
- }
- });
- expect(response.status).toBe(200);
- if (response.status === 200) {
- const data = await response.json();
- expect(data.success).toBe(true);
- expect(data.count).toBe(1);
- const child = data.children[0];
- expect(child.categoryId1).toBe(parentGoods.categoryId1);
- expect(child.categoryId2).toBe(parentGoods.categoryId2);
- expect(child.categoryId3).toBe(parentGoods.categoryId3);
- expect(child.supplierId).toBe(parentGoods.supplierId);
- expect(child.merchantId).toBe(parentGoods.merchantId);
- expect(child.goodsType).toBe(parentGoods.goodsType);
- }
- });
- });
- describe('认证和授权', () => {
- it('应该要求认证', async () => {
- const response = await client[':id']['children'].$get({
- param: { id: parentGoods.id },
- query: { page: 1, pageSize: 10 }
- });
- expect(response.status).toBe(401);
- });
- it('应该验证租户隔离', async () => {
- // 创建另一个租户的用户
- const dataSource = await IntegrationTestDatabase.getDataSource();
- const otherTenantUser = await testFactory.createTestUser(2); // 不同租户
- const otherTenantToken = JWTUtil.generateToken({
- id: otherTenantUser.id,
- username: otherTenantUser.username,
- roles: [{name:'admin'}]
- });
- const response = await client[':id']['children'].$get({
- param: { id: parentGoods.id },
- query: { page: 1, pageSize: 10 }
- }, {
- headers: {
- 'Authorization': `Bearer ${otherTenantToken}`
- }
- });
- // 不同租户应该看不到其他租户的数据
- expect(response.status).toBe(404);
- });
- });
- });
|