feat(api): 添加帮助中心反馈系统和VIP服务功能
- 在ApiClient中新增submitFeedback、myFeedback、helpArticles、helpArticleDetail、 siteArticles、promotions、vipPackages、createVipOrder、vipOrders等方法 - 添加帮助文章和站点资讯的参数验证逻辑 - 更新测试文件添加新的API方法测试用例 - 在HTML页面中添加反馈、帮助和VIP服务相关页面的脚本引用 - 更新加入家谱页面为完整的申请流程界面 - 修改资讯详情页面为站点资讯展示页面 - 更新AxiosRequestUtil中认证处理逻辑 - 添加世系树渲染的HTML生成函数用于页面复用 - 更新文档中的API契约说明和页面规划
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const AppPromotionPages = require('../public/js/app-promotion-pages.js');
|
||||
const appPage = fs.readFileSync(path.resolve(__dirname, '../app.html'), 'utf8');
|
||||
|
||||
const promotionId = '2062179707935264769';
|
||||
|
||||
function promotionFixture(overrides) {
|
||||
return Object.assign({
|
||||
promotionId,
|
||||
promotionKey: 'app-download',
|
||||
promotionTitle: '下载移动端',
|
||||
promotionDesc: '随时查看家谱内容',
|
||||
coverOssId: '2062179707935264701',
|
||||
targetUrl: 'https://example.com/download',
|
||||
platform: 'all',
|
||||
sortOrder: 10,
|
||||
status: '0',
|
||||
remark: 'internal-only'
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
test('推广响应只保留 AppPromotionVo 的安全展示字段并保持长 ID 字符串', () => {
|
||||
assert.deepEqual(AppPromotionPages.normalizePromotion(promotionFixture()), {
|
||||
promotionId,
|
||||
promotionTitle: '下载移动端',
|
||||
promotionDesc: '随时查看家谱内容',
|
||||
targetUrl: 'https://example.com/download'
|
||||
});
|
||||
assert.equal(AppPromotionPages.normalizePromotion(promotionFixture({
|
||||
promotionId: Number.MAX_SAFE_INTEGER + 1
|
||||
})), null);
|
||||
});
|
||||
|
||||
test('推广响应拒绝停用记录和空标题', () => {
|
||||
assert.equal(AppPromotionPages.normalizePromotion(promotionFixture({ status: '1' })), null);
|
||||
assert.equal(AppPromotionPages.normalizePromotion(promotionFixture({ promotionTitle: ' ' })), null);
|
||||
});
|
||||
|
||||
test('推广列表只接受直接数组且任一非法元素使整批失败', () => {
|
||||
assert.equal(AppPromotionPages.normalizePromotionList([promotionFixture()]).length, 1);
|
||||
assert.deepEqual(AppPromotionPages.normalizePromotionList([]), []);
|
||||
assert.deepEqual(AppPromotionPages.normalizePromotionList({
|
||||
rows: [promotionFixture()]
|
||||
}), []);
|
||||
assert.deepEqual(AppPromotionPages.normalizePromotionList([
|
||||
promotionFixture(),
|
||||
promotionFixture({ promotionId: '' })
|
||||
]), []);
|
||||
});
|
||||
|
||||
test('推广链接只允许绝对 HTTP 或 HTTPS 地址', () => {
|
||||
assert.equal(
|
||||
AppPromotionPages.normalizeTargetUrl(' https://example.com/download '),
|
||||
'https://example.com/download'
|
||||
);
|
||||
assert.equal(AppPromotionPages.normalizeTargetUrl('http://example.com'), 'http://example.com/');
|
||||
['javascript:alert(1)', 'data:text/html,test', '/download', 'download.html', ''].forEach((value) => {
|
||||
assert.equal(AppPromotionPages.normalizeTargetUrl(value), '');
|
||||
});
|
||||
});
|
||||
|
||||
test('推广卡片转义响应并隐藏 OSS ID、后台键、排序、状态和备注', () => {
|
||||
const html = AppPromotionPages.renderPromotionList([promotionFixture({
|
||||
promotionTitle: '<script>alert(1)</script>',
|
||||
promotionDesc: '<img src=x>',
|
||||
coverOssId: 'secret-oss',
|
||||
promotionKey: 'secret-key',
|
||||
remark: 'secret-remark'
|
||||
})]);
|
||||
|
||||
assert.match(html, /<script>/);
|
||||
assert.match(html, /<img src=x>/);
|
||||
assert.match(html, /target="_blank"/);
|
||||
assert.match(html, /rel="noopener noreferrer"/);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/<script|<img|secret-oss|secret-key|secret-remark|coverOssId|promotionKey|sortOrder|status|remark/
|
||||
);
|
||||
});
|
||||
|
||||
test('危险或缺失的推广链接渲染为不可点击卡片', () => {
|
||||
const html = AppPromotionPages.renderPromotionList([
|
||||
promotionFixture({ targetUrl: 'javascript:alert(1)' })
|
||||
]);
|
||||
|
||||
assert.match(html, /^<article class="promotion-card"/);
|
||||
assert.doesNotMatch(html, /<a(?:\s|>)|href=|target=|rel=/);
|
||||
});
|
||||
|
||||
test('PC 推广列表只请求 SQL 字典确认的 pc 平台', async () => {
|
||||
const calls = [];
|
||||
const api = {
|
||||
async promotions() {
|
||||
calls.push(Array.from(arguments));
|
||||
return [promotionFixture()];
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal((await AppPromotionPages.loadPromotions(api)).length, 1);
|
||||
assert.deepEqual(calls, [[{ platform: 'pc' }]]);
|
||||
await assert.rejects(
|
||||
AppPromotionPages.loadPromotions({
|
||||
async promotions() {
|
||||
return { rows: [promotionFixture()] };
|
||||
}
|
||||
}),
|
||||
/推广列表响应无效/
|
||||
);
|
||||
});
|
||||
|
||||
test('推广区域只把 401 视为登录失效', () => {
|
||||
assert.equal(AppPromotionPages.isUnauthorized({ status: 401 }), true);
|
||||
assert.equal(AppPromotionPages.isUnauthorized({ code: '401' }), true);
|
||||
assert.equal(AppPromotionPages.isUnauthorized({ status: 403 }), false);
|
||||
assert.equal(AppPromotionPages.isUnauthorized(new Error('网络失败')), false);
|
||||
});
|
||||
|
||||
test('推广区域提供明确的未登录和空列表状态', () => {
|
||||
assert.match(
|
||||
AppPromotionPages.renderPromotionLoginRequired(),
|
||||
/<a href="login\.html">立即登录<\/a>/
|
||||
);
|
||||
assert.match(AppPromotionPages.renderPromotionList([]), /当前暂无应用推广/);
|
||||
});
|
||||
|
||||
test('应用下载页只通过页面标记和新推广模块接入,不暴露业务 ID 输入', () => {
|
||||
assert.match(appPage, /<body[^>]*data-promotion-page/);
|
||||
assert.match(appPage, /data-promotion-list/);
|
||||
assert.match(appPage, /src="public\/js\/app-promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /src="public\/js\/promotion-pages\.js"/);
|
||||
assert.doesNotMatch(appPage, /name="(?:promotionId|coverOssId)"/);
|
||||
});
|
||||
Reference in New Issue
Block a user