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

212 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 modulePath = path.join(__dirname, '..', 'public', 'js', 'article-pages.js');
const ArticlePages = fs.existsSync(modulePath) ? require(modulePath) : {};
function requireFunction(name) {
assert.equal(typeof ArticlePages[name], 'function', `缺少 ArticlePages.${name}`);
return ArticlePages[name];
}
test('谱文页只从 URL 读取安全的真实家谱和谱文编号', () => {
const getCurrentGenealogyId = requireFunction('getCurrentGenealogyId');
const getCurrentArticleId = requireFunction('getCurrentArticleId');
assert.equal(
getCurrentGenealogyId('?genealogyId=2060000000000000001&articleId=2060000000000000002'),
'2060000000000000001'
);
assert.equal(
getCurrentArticleId('?genealogyId=2060000000000000001&articleId=2060000000000000002'),
'2060000000000000002'
);
assert.equal(getCurrentGenealogyId('?genealogyId=unsafe'), '');
assert.equal(getCurrentArticleId('?articleId=unsafe'), '');
});
test('谱文写入只构造 ArticleBody 且固定为可重读正常状态', () => {
const buildArticleBody = requireFunction('buildArticleBody');
assert.deepEqual(buildArticleBody({
articleTitle: ' 家族源流 ',
articleSummary: ' 先祖迁徙记录 ',
coverOssId: '2060000000000000003',
articleContent: ' <p>正文</p> ',
authorName: ' 宗亲 ',
sortOrder: '2',
status: '1',
categoryId: '2060000000000000004',
articleId: 'must-not-send'
}), {
articleTitle: '家族源流',
articleSummary: '先祖迁徙记录',
coverOssId: '2060000000000000003',
articleContent: '<p>正文</p>',
authorName: '宗亲',
sortOrder: 2,
status: '0'
});
});
test('谱文校验必填、上传派生 ID、排序和可重读状态', () => {
const buildArticleBody = requireFunction('buildArticleBody');
const validateArticleBody = requireFunction('validateArticleBody');
assert.equal(validateArticleBody({ articleTitle: '', articleContent: '正文', status: '0' }), '请填写谱文标题');
assert.equal(validateArticleBody({ articleTitle: '标题', articleContent: '', status: '0' }), '请填写谱文正文');
assert.equal(
validateArticleBody(buildArticleBody({
articleTitle: '标题',
articleContent: '正文',
coverOssId: Number.MAX_SAFE_INTEGER + 1
})),
'封面文件编号无效,请重新选择文件'
);
assert.equal(
validateArticleBody(buildArticleBody({
articleTitle: '标题',
articleContent: '正文',
sortOrder: '1.5'
})),
'排序值必须是安全整数'
);
assert.equal(validateArticleBody({ articleTitle: '标题', articleContent: '正文', status: '1' }), '当前 PC 无法重新读取停用谱文,暂不开放停用');
assert.equal(validateArticleBody({ articleTitle: '标题', articleContent: '正文', status: '0' }), '');
});
test('谱文响应使用完整 ArticleVo 并拒绝不安全长 ID', () => {
const normalizeArticle = requireFunction('normalizeArticle');
const article = normalizeArticle({
articleId: '2060000000000000001',
genealogyId: '2060000000000000002',
genealogyNo: 'G20260729001',
genealogyName: '叶氏家谱',
surname: '叶',
categoryId: null,
categoryName: '',
categoryCode: '',
articleTitle: '家族源流',
articleSummary: '先祖迁徙记录',
coverOssId: '2060000000000000003',
articleContent: '<p>正文</p>',
authorName: '宗亲',
publishTime: '2026-07-29 10:00:00',
viewCount: 3,
sortOrder: 1,
status: '0',
remark: '年度谱文'
});
assert.equal(article.articleId, '2060000000000000001');
assert.equal(article.coverOssId, '2060000000000000003');
assert.equal(article.viewCount, 3);
assert.equal(article.articleTitle, '家族源流');
assert.equal(normalizeArticle({
articleId: Number('2060000000000000001'),
genealogyId: '2',
articleTitle: '标题',
articleContent: '正文',
status: '0'
}), null);
});
test('谱文列表只接受直接数组且任一非法元素使整批失败', () => {
const normalizeArticles = requireFunction('normalizeArticles');
const valid = {
articleId: '1',
genealogyId: '2',
articleTitle: '标题',
articleContent: '正文',
status: '0'
};
assert.equal(normalizeArticles([valid]).length, 1);
assert.deepEqual(normalizeArticles({ rows: [valid] }), []);
assert.deepEqual(normalizeArticles([valid, {}]), []);
});
test('谱文详情转义正文并隐藏内部 ID 和 OSS ID', () => {
const normalizeArticle = requireFunction('normalizeArticle');
const renderArticleDetail = requireFunction('renderArticleDetail');
const html = renderArticleDetail(normalizeArticle({
articleId: '1',
genealogyId: '2',
categoryId: '3',
coverOssId: '4',
articleTitle: '<img src=x onerror=alert(1)>家史',
articleSummary: '摘要',
articleContent: '<script>alert(1)</script>正文',
authorName: '宗亲',
publishTime: '2026-07-29 10:00:00',
viewCount: 5,
status: '0'
}));
assert.doesNotMatch(html, /<script|<img/);
assert.match(html, /&lt;img|&lt;script/);
assert.match(html, /家史|正文|宗亲|5/);
assert.doesNotMatch(html, />1<|>2<|>3<|>4</);
});
test('谱文写后重读必须返回同一条稳定记录', () => {
const matchesSavedArticle = requireFunction('matchesSavedArticle');
const detail = {
articleId: '2060000000000000001',
genealogyId: '2060000000000000002',
articleTitle: '标题',
articleContent: '正文',
status: '0'
};
assert.equal(matchesSavedArticle(detail, '2060000000000000001'), true);
assert.equal(matchesSavedArticle(detail, '2060000000000000009'), false);
assert.equal(matchesSavedArticle({}, '2060000000000000001'), false);
});
test('谱文页面按家谱能力开放 PC CRUD,且不提供手填分类、业务 ID 或停用状态', () => {
const projectRoot = path.join(__dirname, '..');
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-article.html'), 'utf8');
const editPage = fs.readFileSync(path.join(projectRoot, 'profile-article-edit.html'), 'utf8');
assert.doesNotMatch(listPage, /data-feature-status="pending"|pending-pages\.js/);
assert.match(listPage, /data-article-list/);
assert.match(listPage, /data-article-create-link[^>]+hidden|hidden[^>]+data-article-create-link/);
assert.match(listPage, /data-article-detail/);
assert.match(listPage, /public\/js\/article-pages\.js/);
assert.doesNotMatch(editPage, /data-feature-status="pending"|pending-pages\.js/);
assert.match(editPage, /name="articleTitle"/);
assert.match(editPage, /data-article-form[^>]*novalidate/);
assert.match(editPage, /for="articleTitle">谱文标题<span class="field-required"/);
assert.match(editPage, /name="articleSummary"/);
assert.match(editPage, /name="articleContent"/);
assert.match(editPage, /for="articleContent">正文<span class="field-required"/);
assert.match(editPage, /name="authorName"/);
assert.match(editPage, /name="coverOssId" type="hidden"/);
assert.match(editPage, /name="status" type="hidden" value="0"/);
assert.match(editPage, /data-article-form-status role="status" aria-live="polite"/);
assert.match(editPage, /public\/js\/article-pages\.js/);
assert.doesNotMatch(editPage, /name="articleId"|name="categoryId"|type="text"[^>]+coverOssId/);
});
test('谱文只接受后端布尔能力字段,并在提交层保持写权限门控', () => {
const script = fs.readFileSync(modulePath, 'utf8');
assert.equal(requireFunction('canEditArticles')({ canEditContent: true }), true);
assert.equal(requireFunction('canEditArticles')({ canManage: true }), true);
assert.equal(requireFunction('canEditArticles')({ canEditContent: 'true', canManage: false }), false);
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
assert.match(script, /canEditContent = canEditArticles\(detail\)/);
});
test('谱文编辑表单使用统一状态与提交中反馈', () => {
const script = fs.readFileSync(modulePath, 'utf8');
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
assert.match(script, /setFormStatus\(validation, 'error'\)/);
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
assert.match(script, /setFormStatus\('正在保存谱文…', 'loading'\)/);
});