// 已读取AGENTS.md import { BannerService } from '../src/core/banner/banner.service'; import * as fs from 'fs'; import * as path from 'path'; describe('Banner Management System (TDD 轮播图后台运维与小程序动态联动套件)', () => { let bannerService: BannerService; const rootDir = path.resolve(__dirname, '../../'); const serverIndexPath = path.join(rootDir, 'server/src/index.ts'); const mysqlPath = path.join(rootDir, 'server/src/db/mysql.ts'); const repoPath = path.join(rootDir, 'server/src/db/repository.ts'); const mobileIndexPath = path.join(rootDir, 'pwuniapp/pages/index/index.vue'); beforeEach(() => { bannerService = new BannerService(); }); describe('1. 核心业务服务 BannerService 单测', () => { it('默认初始加载 3 条上架状态的种子轮播图,且按 sortOrder 正序排序', () => { const activeBanners = bannerService.getActiveBanners(); expect(activeBanners.length).toBe(3); expect(activeBanners[0].id).toBe(1); expect(activeBanners[0].sortOrder).toBe(1); expect(activeBanners[1].id).toBe(2); expect(activeBanners[2].id).toBe(3); expect(activeBanners.every((b) => b.status === 1)).toBe(true); }); it('成功创建新运营轮播图并支持排序与分类标签', () => { const created = bannerService.createBanner({ title: '万圣节南瓜派对狂欢夜', subtitle: '全场酒水半价 · 专属大神带队', tag: '节日特惠', badge: '节日爆款', badgeType: 'badge-hot', bg: 'linear-gradient(135deg, #FF7D00 0%, #F53F3F 100%)', image: 'https://images.unsplash.com/photo-1509198397868-475647b2a1e5?w=800', link: '/pages/activity/list?category=PARTY', sortOrder: 0, // 置顶 status: 1 }); expect(created.id).toBe(4); expect(created.title).toBe('万圣节南瓜派对狂欢夜'); expect(created.sortOrder).toBe(0); // 验证置顶生效:getActiveBanners 第一项即为新轮播图 const activeList = bannerService.getActiveBanners(); expect(activeList.length).toBe(4); expect(activeList[0].id).toBe(4); }); it('创建或编辑轮播图时,空标题应被拒绝拦截', () => { expect(() => { bannerService.createBanner({ title: ' ', subtitle: '空标题测试' }); }).toThrow('轮播图标题不能为空'); }); it('创建或编辑轮播图时,涉嫌违规敏感词应触发微信内容安全拦截', () => { expect(() => { bannerService.createBanner({ title: '私下转账免手续费活动返利', subtitle: '欺诈引流测试' }); }).toThrow(); expect(() => { bannerService.createBanner({ title: '美女上门特殊服务招嫖', subtitle: '违规低俗' }); }).toThrow(); }); it('支持快速切换上下线状态并在移动端实时动态显隐', () => { // 下线 ID 为 1 的轮播图 const offlineItem = bannerService.toggleStatus(1, 0); expect(offlineItem.status).toBe(0); // 移动端列表中已不再包含 ID 为 1 const activeList = bannerService.getActiveBanners(); expect(activeList.some((b) => b.id === 1)).toBe(false); // 后台全量列表中仍保留 const allList = bannerService.getAllBanners(); expect(allList.some((b) => b.id === 1)).toBe(true); // 重新上线 const onlineItem = bannerService.toggleStatus(1, 1); expect(onlineItem.status).toBe(1); expect(bannerService.getActiveBanners().some((b) => b.id === 1)).toBe(true); }); it('非法状态值切换应被拦截', () => { expect(() => { bannerService.toggleStatus(1, 99 as any); }).toThrow('状态参数必须为 0 或 1'); }); it('支持修改轮播图配置与排序', () => { const updated = bannerService.updateBanner(2, { title: '工体纯KTV超人气嗨唱局', subtitle: '豪华大包 · 专属金牌麦霸', sortOrder: 10 }); expect(updated.title).toBe('工体纯KTV超人气嗨唱局'); expect(updated.sortOrder).toBe(10); }); it('支持删除轮播图,删除不存在项时抛出异常', () => { const ok = bannerService.deleteBanner(3); expect(ok).toBe(true); expect(bannerService.getAllBanners().some((b) => b.id === 3)).toBe(false); expect(() => { bannerService.deleteBanner(999); }).toThrow('轮播图不存在或已被删除'); }); }); describe('2. 服务端数据库与 API 架构持久化门禁校验', () => { it('MySQL 数据库 schema 必须包含 banners 表定义与默认种子数据', () => { const mysqlContent = fs.readFileSync(mysqlPath, 'utf-8'); expect(mysqlContent).toContain('CREATE TABLE IF NOT EXISTS banners'); expect(mysqlContent).toContain('sort_order INT DEFAULT 0'); expect(mysqlContent).toContain('status TINYINT DEFAULT 1'); expect(mysqlContent).toContain('INSERT INTO banners'); }); it('Repository 数据仓储层必须实现完整的 Banner CRUD 接口', () => { const repoContent = fs.readFileSync(repoPath, 'utf-8'); expect(repoContent).toContain('async getBanners(): Promise'); expect(repoContent).toContain('async getAdminBanners(): Promise'); expect(repoContent).toContain('async createBanner('); expect(repoContent).toContain('async updateBanner('); expect(repoContent).toContain('async updateBannerStatus('); expect(repoContent).toContain('async deleteBanner('); // 必须带状态过滤与排序 expect(repoContent).toContain('WHERE status = 1'); expect(repoContent).toContain('ORDER BY sort_order ASC'); }); it('server/src/index.ts 必须注册完整的 Banner 运维 API 端点并配置微信内容安全检查', () => { const indexContent = fs.readFileSync(serverIndexPath, 'utf-8'); expect(indexContent).toContain("app.get('/api/banners'"); expect(indexContent).toContain("app.get('/api/admin/banners'"); expect(indexContent).toContain("app.post('/api/admin/banners'"); expect(indexContent).toContain("app.put('/api/admin/banners/:id'"); expect(indexContent).toContain("app.post('/api/admin/banners/:id/status'"); expect(indexContent).toContain("app.delete('/api/admin/banners/:id'"); expect(indexContent).toContain('contentSecurityService.checkText('); }); it('小程序前端 index.vue 必须通过 getBanners 动态接入服务端轮播图数据', () => { const mobileContent = fs.readFileSync(mobileIndexPath, 'utf-8'); expect(mobileContent).toContain('v-for="banner in banners"'); expect(mobileContent).toContain('fetchLiveBanners()'); expect(mobileContent).toContain('getBanners()'); expect(mobileContent).toContain('goToActivity(banner)'); }); }); describe('3. 规范门禁 (AGENTS.md & 零 Emoji 规则)', () => { it('服务文件与测试套件必须严格满足零系统 Emoji 规范', () => { const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; const serviceContent = fs.readFileSync(path.join(rootDir, 'server/src/core/banner/banner.service.ts'), 'utf-8'); const testContent = fs.readFileSync(__filename, 'utf-8'); expect(emojiRegex.test(serviceContent)).toBe(false); expect(emojiRegex.test(testContent)).toBe(false); }); }); });