import * as fs from 'fs'; import * as path from 'path'; import { BaseController } from '@cool-midway/core'; import { CardCasesEntity } from '../../../src/modules/card/entity/cases'; import { CardCasesService } from '../../../src/modules/card/service/cases'; import { AdminCardCasesController, adminCardCasesCurdOption, } from '../../../src/modules/card/controller/admin/cases'; describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => { const entityFile = path.join( __dirname, '../../../src/modules/card/entity/cases.ts' ); const serviceFile = path.join( __dirname, '../../../src/modules/card/service/cases.ts' ); const controllerFile = path.join( __dirname, '../../../src/modules/card/controller/admin/cases.ts' ); describe('1. 契约文件与自动路由约定', () => { it('文件路径应符合 /admin/card/cases 路由与模块约定', () => { expect(fs.existsSync(entityFile)).toBe(true); expect(fs.existsSync(serviceFile)).toBe(true); expect(fs.existsSync(controllerFile)).toBe(true); }); it('控制器应继承 BaseController 并挂载 CoolController CRUD', () => { expect(AdminCardCasesController.prototype).toBeInstanceOf(BaseController); expect(adminCardCasesCurdOption.entity).toBe(CardCasesEntity); expect(adminCardCasesCurdOption.service).toBe(CardCasesService); }); it('装饰器 api 必须包含 add/delete/update/info/list/page', () => { expect(adminCardCasesCurdOption.api).toEqual([ 'add', 'delete', 'update', 'info', 'list', 'page', ]); }); it('pageQueryOp 应支持 a.cardId 精确筛选与 title/client/metrics 模糊搜索', () => { const pageQueryOp = adminCardCasesCurdOption.pageQueryOp; expect(pageQueryOp.fieldEq).toEqual( expect.arrayContaining([{ column: 'a.card_id', requestParam: 'cardId' }]) ); expect(pageQueryOp.keyWordLikeFields).toEqual( expect.arrayContaining(['a.title', 'a.client', 'a.metrics']) ); expect(pageQueryOp.addOrderBy).toMatchObject({ sort_order: 'asc' }); }); }); describe('2. 实体字段契约 (CardCasesEntity)', () => { it('实体实例应支持出海案例必需契约字段', () => { const item = new CardCasesEntity(); item.cardId = 1001; item.title = '智能电动滑板车欧美 DTC 出海'; item.client = 'VoltRide Global'; item.duration = '2024.01 - 2024.06'; item.metrics = '+320% ROI, 45,000+ 留资量'; item.coverImage = 'https://cdn.example.com/cases/voltride.webp'; item.pdfUrl = 'https://cdn.example.com/cases/voltride-deck.pdf'; item.description = '品牌从 0 到 1 进军北美市场独立站及全渠道获客'; item.sortOrder = 1; expect(item.cardId).toBe(1001); expect(item.title).toBe('智能电动滑板车欧美 DTC 出海'); expect(item.client).toBe('VoltRide Global'); expect(item.duration).toBe('2024.01 - 2024.06'); expect(item.metrics).toBe('+320% ROI, 45,000+ 留资量'); expect(item.coverImage).toBe('https://cdn.example.com/cases/voltride.webp'); expect(item.pdfUrl).toBe('https://cdn.example.com/cases/voltride-deck.pdf'); expect(item.description).toBe('品牌从 0 到 1 进军北美市场独立站及全渠道获客'); expect(item.sortOrder).toBe(1); }); }); describe('3. 服务层按 cardId 筛选与 CRUD 逻辑 (CardCasesService)', () => { let service: CardCasesService; let mockList: CardCasesEntity[] = []; beforeEach(() => { mockList = [ Object.assign(new CardCasesEntity(), { id: 1, cardId: 101, title: '跨境智能硬件众筹出海', client: 'TechNova', duration: '3个月', metrics: '+280% 众筹目标达成', coverImage: 'https://cdn.example.com/case1.png', pdfUrl: 'https://cdn.example.com/case1.pdf', sortOrder: 2, }), Object.assign(new CardCasesEntity(), { id: 2, cardId: 101, title: '东南亚 TikTok 跨境直播获客', client: 'FashionGo', duration: '6个月', metrics: '千万级曝光, +150% GMV', coverImage: 'https://cdn.example.com/case2.png', pdfUrl: 'https://cdn.example.com/case2.pdf', sortOrder: 1, }), Object.assign(new CardCasesEntity(), { id: 3, cardId: 102, title: 'B2B 重工机械中东拓客', client: 'HeavyMach', duration: '12个月', metrics: '$5M+ 意向大单', coverImage: 'https://cdn.example.com/case3.png', pdfUrl: 'https://cdn.example.com/case3.pdf', sortOrder: 1, }), ]; service = new CardCasesService(); service.cardCasesEntity = { find: jest.fn().mockImplementation(async (options?: { where?: { cardId?: number }; order?: Record }) => { let res = [...mockList]; if (options?.where?.cardId !== undefined) { res = res.filter(item => Number(item.cardId) === Number(options.where.cardId)); } if (options?.order?.sortOrder === 'ASC') { res.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); } return res; }), findOneBy: jest.fn().mockImplementation(async (where: { id: number }) => { return mockList.find(item => item.id === where.id) || null; }), save: jest.fn().mockImplementation(async (entity: Partial) => { if (entity.id) { const index = mockList.findIndex(item => item.id === entity.id); if (index >= 0) { mockList[index] = Object.assign(mockList[index], entity); return mockList[index]; } } const created = Object.assign(new CardCasesEntity(), entity, { id: mockList.length + 1, }); mockList.push(created); return created; }), delete: jest.fn().mockImplementation(async (id: number) => { mockList = mockList.filter(item => item.id !== id); return { affected: 1 }; }), } as unknown as typeof service.cardCasesEntity; }); it('getCasesByCardId 应根据名片 cardId 筛选并按 sortOrder 升序排列', async () => { const cases = await service.getCasesByCardId(101); expect(cases).toHaveLength(2); expect(cases[0].title).toBe('东南亚 TikTok 跨境直播获客'); expect(cases[0].sortOrder).toBe(1); expect(cases[1].title).toBe('跨境智能硬件众筹出海'); expect(cases[1].sortOrder).toBe(2); }); it('getCasesByCardId 传入无效 cardId 时应返回空数组', async () => { const cases = await service.getCasesByCardId(0); expect(cases).toEqual([]); }); it('创建案例并能查询出最新案例', async () => { const created = await service.cardCasesEntity.save({ cardId: 101, title: '拉美电商本土化品牌出海', client: 'LatamDirect', duration: '4个月', metrics: '+90% 复购率', coverImage: 'https://cdn.example.com/case4.png', pdfUrl: 'https://cdn.example.com/case4.pdf', sortOrder: 3, }); expect(created.id).toBeDefined(); expect(created.title).toBe('拉美电商本土化品牌出海'); const found = await service.cardCasesEntity.findOneBy({ id: created.id }); expect(found).not.toBeNull(); expect(found?.client).toBe('LatamDirect'); }); it('更新案例信息应成功', async () => { await service.cardCasesEntity.save({ id: 1, metrics: '+350% 众筹目标达成', }); const updated = await service.cardCasesEntity.findOneBy({ id: 1 }); expect(updated?.metrics).toBe('+350% 众筹目标达成'); }); }); });