Files
jiapu/tests/site-news-pages.test.js
T
2026-08-03 16:49:38 +08:00

197 lines
8.3 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const SiteNewsPages = require('../public/js/site-news-pages.js');
const newsPage = fs.readFileSync(path.resolve(__dirname, '../news.html'), 'utf8');
const detailPage = fs.readFileSync(path.resolve(__dirname, '../article-detail.html'), 'utf8');
const articleId = '2062179707935264769';
function articleFixture(overrides) {
return Object.assign({
articleId,
articleType: 'notice',
articleTitle: '平台公告',
articleSummary: '公告摘要',
articleContent: '第一行\n第二行',
coverOssId: '2062179707935264701',
externalUrl: 'https://example.com/notice',
publishTime: '2026-07-30 09:00:00',
sortOrder: 1,
status: '0',
remark: 'internal-only'
}, overrides);
}
test('站点文章只保留完整 SiteArticleVo 的安全展示字段和字符串长 ID', () => {
assert.deepEqual(SiteNewsPages.normalizeArticle(articleFixture()), {
articleId,
articleType: 'notice',
articleTitle: '平台公告',
articleSummary: '公告摘要',
articleContent: '第一行\n第二行',
externalUrl: 'https://example.com/notice',
publishTime: '2026-07-30 09:00:00'
});
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({
articleId: Number.MAX_SAFE_INTEGER + 1
})), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleId: '0' })), null);
});
test('站点文章拒绝 SQL 字典外类型、停用记录和空标题', () => {
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleType: 'culture' })), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ status: '1' })), null);
assert.equal(SiteNewsPages.normalizeArticle(articleFixture({ articleTitle: ' ' })), null);
assert.equal(SiteNewsPages.normalizeArticleType('news'), 'news');
assert.equal(SiteNewsPages.normalizeArticleType('notice'), 'notice');
assert.equal(SiteNewsPages.normalizeArticleType('download'), 'download');
assert.equal(SiteNewsPages.normalizeArticleType('culture'), '');
});
test('站点文章列表只接受直接数组且任一非法元素使整批失败', () => {
assert.equal(SiteNewsPages.normalizeArticleList([articleFixture()]).length, 1);
assert.deepEqual(SiteNewsPages.normalizeArticleList([]), []);
assert.deepEqual(SiteNewsPages.normalizeArticleList({ rows: [articleFixture()] }), []);
assert.deepEqual(SiteNewsPages.normalizeArticleList([
articleFixture(),
articleFixture({ articleId: '' })
]), []);
});
test('站点资讯列表使用响应 ID 生成详情链接并隐藏内部字段', () => {
const html = SiteNewsPages.renderArticleList([articleFixture({
articleTitle: '<script>alert(1)</script>',
articleSummary: '<img src=x>',
coverOssId: 'secret-oss',
remark: 'secret-remark'
})]);
assert.match(html, /article-detail\.html\?articleId=2062179707935264769/);
assert.match(html, /公告/);
assert.match(html, /&lt;script&gt;/);
assert.match(html, /&lt;img src=x&gt;/);
assert.doesNotMatch(html, /<script|<img|secret-oss|secret-remark|coverOssId|sortOrder|status|remark/);
assert.equal(SiteNewsPages.renderArticleList([]), '');
});
test('站点资讯详情转义正文、保留换行并只开放安全外链', () => {
const html = SiteNewsPages.renderArticleDetail(articleFixture({
articleContent: '<script>alert(1)</script>\n第二行'
}));
assert.match(html, /&lt;script&gt;alert\(1\)&lt;\/script&gt;<br \/>第二行/);
assert.match(html, /href="https:\/\/example\.com\/notice"/);
assert.match(html, /target="_blank"/);
assert.match(html, /rel="noopener noreferrer"/);
assert.doesNotMatch(html, /<script|coverOssId|secret-remark/);
['javascript:alert(1)', 'data:text/html,test', '/notice', 'notice.html', ''].forEach((value) => {
const unsafeHtml = SiteNewsPages.renderArticleDetail(articleFixture({ externalUrl: value }));
assert.doesNotMatch(unsafeHtml, /data-external-link/);
});
});
test('站点文章外链只接受绝对 HTTP 或 HTTPS', () => {
assert.equal(
SiteNewsPages.normalizeExternalUrl(' https://example.com/notice '),
'https://example.com/notice'
);
assert.equal(SiteNewsPages.normalizeExternalUrl('http://example.com'), 'http://example.com/');
assert.equal(SiteNewsPages.normalizeExternalUrl('javascript:alert(1)'), '');
assert.equal(SiteNewsPages.normalizeExternalUrl('/notice'), '');
});
test('资讯列表只提交已确认分类和系统 limit', async () => {
const calls = [];
const api = {
async siteArticles(query) {
calls.push(query);
return [articleFixture()];
}
};
assert.equal((await SiteNewsPages.loadArticles(api, 'notice')).length, 1);
assert.equal((await SiteNewsPages.loadArticles(api, '')).length, 1);
assert.deepEqual(calls, [
{ articleType: 'notice', limit: 100 },
{ limit: 100 }
]);
await assert.rejects(SiteNewsPages.loadArticles(api, 'culture'), /资讯分类无效/);
await assert.rejects(
SiteNewsPages.loadArticles({
async siteArticles() {
return { rows: [articleFixture()] };
}
}, ''),
/资讯列表响应无效/
);
});
test('资讯详情只精确匹配 URL 中由列表产生的文章 ID', async () => {
const api = {
async siteArticles(query) {
assert.deepEqual(query, { limit: 100 });
return [
articleFixture(),
articleFixture({ articleId: '2062179707935264770', articleTitle: '另一条' })
];
}
};
assert.equal((await SiteNewsPages.loadArticleDetail(api, articleId)).articleTitle, '平台公告');
await assert.rejects(
SiteNewsPages.loadArticleDetail(api, '2062179707935264771'),
/资讯不存在或已下线/
);
await assert.rejects(
SiteNewsPages.loadArticleDetail(api, Number.MAX_SAFE_INTEGER + 1),
/资讯编号无效/
);
});
test('详情 URL 只读取安全字符串文章 ID', () => {
assert.equal(SiteNewsPages.readArticleId('?articleId=' + articleId), articleId);
assert.equal(SiteNewsPages.readArticleId('?articleId=0'), '');
assert.equal(SiteNewsPages.readArticleId('?articleId=unsafe'), '');
assert.equal(SiteNewsPages.readArticleId(''), '');
});
test('资讯列表和详情将加载、空数据、失败及无权限写入统一状态组件', () => {
const source = fs.readFileSync(path.resolve(__dirname, '../public/js/site-news-pages.js'), 'utf8');
assert.equal(SiteNewsPages.isForbidden({ status: 403 }), true);
assert.equal(SiteNewsPages.isForbidden({ status: 401 }), false);
assert.equal(SiteNewsPages.isMissingArticleError(new Error('资讯不存在或已下线')), true);
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
assert.match(source, /setSiteState\(list, 'loading', '正在读取资讯…'\)/);
assert.match(source, /setSiteState\(detail, 'loading', '正在读取资讯详情…'\)/);
assert.match(source, /isForbidden\(error\) \? 'forbidden' : 'error'/);
assert.match(source, /isForbidden\(error\) \? 'forbidden' : \(isMissingArticleError\(error\) \? 'empty' : 'error'\)/);
});
test('官网资讯列表与详情页加载同一个公开内容 owner 且不暴露业务 ID', () => {
assert.match(newsPage, /<body[^>]*data-site-news-page/);
assert.match(newsPage, /data-site-news-list/);
assert.match(newsPage, /data-site-news-status[^>]*role="status"/);
assert.match(newsPage, /data-api-state="loading"/);
assert.match(newsPage, /news\.html\?articleType=news/);
assert.match(newsPage, /news\.html\?articleType=notice/);
assert.match(newsPage, /news\.html\?articleType=download/);
assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/);
assert.match(newsPage, /src="public\/js\/profile-common\.js"/);
assert.match(detailPage, /<body[^>]*data-site-article-page/);
assert.match(detailPage, /<title>资讯详情 - 代代相传<\/title>/);
assert.match(detailPage, /data-site-article-detail/);
assert.match(detailPage, /data-site-article-status[^>]*role="status"/);
assert.match(detailPage, /data-api-state="loading"/);
assert.match(detailPage, /src="public\/js\/profile-common\.js"/);
assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/);
assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/);
assert.doesNotMatch(detailPage, /href="article-detail\.html"/);
assert.doesNotMatch(newsPage + detailPage, /name="(?:articleId|coverOssId)"/);
});