177 行
7.4 KiB
TypeScript
177 行
7.4 KiB
TypeScript
// 已读取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<any[]>');
|
||
expect(repoContent).toContain('async getAdminBanners(): Promise<any[]>');
|
||
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);
|
||
});
|
||
});
|
||
});
|