文件
shanghaipeiwan/server/tests/banner-manage.spec.ts

177 行
7.4 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

// 已读取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);
});
});
});