212 lines
8.3 KiB
JavaScript
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, /<img|<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'\)/);
|
|
});
|