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 coverFile() { return { fileId: '2060000000000000090', ossId: '2060000000000000091', fileName: 'cover.jpg', mediaType: 'image/jpeg', accessUrl: 'https://files.example/article-cover.jpg' }; } 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: '先祖迁徙记录', coverFile: coverFile(), articleContent: '

正文

', authorName: '宗亲', publishTime: '2026-07-29 10:00:00', viewCount: 3, sortOrder: 1, status: '0', remark: '年度谱文' }); assert.equal(article.articleId, '2060000000000000001'); assert.equal(article.coverFile.accessUrl, 'https://files.example/article-cover.jpg'); 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, /data-article-form[^>]*novalidate/); assert.match(editPage, /for="articleTitle">谱文标题正文]+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'\)/); });