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: '

正文

', authorName: ' 宗亲 ', sortOrder: '2', status: '1', categoryId: '2060000000000000004', articleId: 'must-not-send' }), { articleTitle: '家族源流', articleSummary: '先祖迁徙记录', coverOssId: '2060000000000000003', articleContent: '

正文

', 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: '

正文

', 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: '家史', articleSummary: '摘要', articleContent: '正文', authorName: '宗亲', publishTime: '2026-07-29 10:00:00', viewCount: 5, status: '0' })); assert.doesNotMatch(html, /1<|>2<|>3<|>4 { 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, /name="articleSummary"/); assert.match(editPage, /name="articleContent"/); assert.match(editPage, /name="authorName"/); assert.match(editPage, /name="coverOssId" type="hidden"/); assert.match(editPage, /name="status" type="hidden" value="0"/); assert.match(editPage, /public\/js\/article-pages\.js/); assert.doesNotMatch(editPage, /name="articleId"|name="categoryId"|type="text"[^>]+coverOssId/); });