const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const LineagePages = require('../public/js/lineage-pages.js'); test('世系页只从 URL 读取真实家谱编号', () => { assert.equal(LineagePages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001'); assert.equal(LineagePages.getCurrentGenealogyId(''), ''); }); test('世系管理页和家谱主页复用同一棵安全树的 HTML', () => { const html = LineagePages.renderLineageTreeHtml([{ personId: '2062179707935264770', genealogyId: '2062179707935264769', name: '<始祖>', generationName: '始', spouses: [], children: [] }]); assert.match(html, /<始祖>/); assert.match(html, /data-lineage-person="2062179707935264770"/); assert.doesNotMatch(html, /2062179707935264769/); assert.match(LineagePages.renderLineageTreeHtml([]), /暂无世系树/); }); test('世系人物表单只构造 Apifox LineagePersonBody 字段', () => { assert.deepEqual( LineagePages.buildLineagePersonBody({ bindingMode: 'SPECIFIED', appUserId: '2061000000000000001', avatarOssId: '2061000000000000002', name: '李明', sex: '0', generation: '3', generationName: '德', fatherId: '2061000000000000003', motherId: '2061000000000000004', personNo: 'P202607090001', aliasName: '明远', birthDate: '1990-01-01T08:30', birthLunar: '0', birthPlace: '成都', deathDate: '2050-02-03T10:45', deathLunar: '1', deathPlace: '重庆', burialPlace: '青城山', personStatus: '1', sortOrder: '1', relationName: '妻', biography: '人物简介', remark: '家族备注', personName: '旧字段', generationNo: 4, introduction: '旧字段' }), { bindingMode: 'SPECIFIED', appUserId: '2061000000000000001', avatarOssId: '2061000000000000002', name: '李明', sex: '0', generation: 3, generationName: '德', fatherId: '2061000000000000003', motherId: '2061000000000000004', personNo: 'P202607090001', aliasName: '明远', birthDate: '1990-01-01 08:30:00', birthLunar: '0', birthPlace: '成都', deathDate: '2050-02-03 10:45:00', deathLunar: '1', deathPlace: '重庆', burialPlace: '青城山', personStatus: '1', sortOrder: 1, relationName: '妻', biography: '人物简介', remark: '家族备注' } ); assert.deepEqual(LineagePages.buildLineagePersonBody({ bindingMode: 'SELF', appUserId: '2061000000000000001', name: '李明' }), { bindingMode: 'SELF', name: '李明' }); }); test('世系人物要求安全的 ID、姓名和有效世代值', () => { assert.deepEqual( LineagePages.normalizeLineagePerson({ personId: '2060000000000000000', name: '李明', generation: 3, generationName: '德', sex: '0' }), { personId: '2060000000000000000', name: '李明', sex: '0', generationName: '德', generation: 3 } ); assert.equal(LineagePages.normalizeLineagePerson({ personId: Number('2060000000000000000'), name: '李明' }), null); assert.equal(LineagePages.normalizeLineagePerson({ personId: '2060000000000000000', fatherId: Number('2061000000000000000'), name: '李明' }), null); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '' }), '请填写成员姓名'); assert.equal(LineagePages.validateLineagePersonBody({ name: '李明' }), '请选择账号绑定方式'); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'OTHER', name: '李明' }), '账号绑定方式无效'); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'SPECIFIED', name: '李明' }), '指定账号绑定缺少用户选项'); assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', generation: '1.5' })), '世代序号必须是整数'); assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ bindingMode: 'NONE', name: '李明', sortOrder: '1.5' })), '排序值必须是整数'); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', sex: '3' }), '性别选项无效'); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', personStatus: '3' }), '人物状态选项无效'); assert.equal(LineagePages.validateLineagePersonBody({ bindingMode: 'NONE', name: '李明', birthLunar: '2' }), '出生历法选项无效'); }); test('世系人物响应保留完整详情字段并拒绝不安全的嵌套 ID', () => { assert.deepEqual(LineagePages.normalizeLineagePerson({ personId: '2060000000000000000', genealogyId: '2060000000000000001', appUserId: '2060000000000000002', avatarOssId: '2060000000000000003', fatherId: '2060000000000000004', motherId: '2060000000000000005', genealogyName: '李氏家谱', genealogyNo: 'G20260728001', appUserNickName: '明远', personNo: 'P202607090001', name: '李明', aliasName: '明远', sex: '0', generation: 3, generationName: '德', fatherName: '李父', motherName: '王母', spouseNames: '张氏', birthDate: '1990-01-01 08:30:00', birthLunar: '0', birthPlace: '成都', deathDate: '2050-02-03 10:45:00', deathLunar: '1', deathPlace: '重庆', burialPlace: '青城山', personStatus: '1', biography: '人物简介', sortOrder: 1, status: '0', remark: '家族备注' }), { personId: '2060000000000000000', name: '李明', genealogyName: '李氏家谱', genealogyNo: 'G20260728001', appUserNickName: '明远', personNo: 'P202607090001', aliasName: '明远', sex: '0', generationName: '德', fatherName: '李父', motherName: '王母', spouseNames: '张氏', birthDate: '1990-01-01 08:30:00', birthLunar: '0', birthPlace: '成都', deathDate: '2050-02-03 10:45:00', deathLunar: '1', deathPlace: '重庆', burialPlace: '青城山', personStatus: '1', biography: '人物简介', status: '0', remark: '家族备注', appUserId: '2060000000000000002', genealogyId: '2060000000000000001', fatherId: '2060000000000000004', motherId: '2060000000000000005', avatarOssId: '2060000000000000003', generation: 3, sortOrder: 1 }); }); test('家谱内容权限只控制世系写操作,读取和分页始终可用', () => { assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: false }), { canEditContent: false }); assert.deepEqual(LineagePages.normalizeLineageAccess({ canEditContent: true }), { canEditContent: true }); assert.equal(LineagePages.canUseLineagePagination(false), true); assert.equal(LineagePages.canUseLineagePagination(true), false); }); test('世代选项来自正常字辈列表并自动映射序号和字辈', () => { assert.deepEqual(LineagePages.normalizeGenerationOption({ poemId: '2060000000000000000', generationNo: 3, generationText: '德', status: '0' }), { generation: 3, generationName: '德' }); assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 0, generationText: '德' }), null); assert.equal(LineagePages.normalizeGenerationOption({ generationNo: 3, generationText: '' }), null); }); test('已有账号绑定按当前登录用户区分 SELF 与只读保留的 SPECIFIED', () => { assert.deepEqual(LineagePages.normalizeLineageBinding({}, '2060000000000000001'), { bindingMode: 'NONE', appUserId: '' }); assert.deepEqual(LineagePages.normalizeLineageBinding({ appUserId: '2060000000000000001' }, '2060000000000000001'), { bindingMode: 'SELF', appUserId: '' }); assert.deepEqual(LineagePages.normalizeLineageBinding({ appUserId: '2060000000000000002' }, '2060000000000000001'), { bindingMode: 'SPECIFIED', appUserId: '2060000000000000002' }); }); test('指定账号绑定只使用当前家谱正常且已绑定账号的成员选项', () => { assert.deepEqual(LineagePages.normalizeGenealogyMemberOption({ memberId: '2060000000000000001', genealogyId: '2060000000000000002', appUserId: '2060000000000000003', appUserNickName: '明远', memberName: '李明', roleType: 'member', status: '0' }), { memberId: '2060000000000000001', genealogyId: '2060000000000000002', appUserId: '2060000000000000003', appUserNickName: '明远', memberName: '李明', roleType: 'member', status: '0' }); assert.equal(LineagePages.normalizeGenealogyMemberOption({ memberId: '1', genealogyId: '2', appUserId: null, memberName: '未绑定成员', status: '0' }), null); assert.equal(LineagePages.normalizeGenealogyMemberOption({ memberId: '1', genealogyId: '2', appUserId: '3', memberName: '停用成员', status: '1' }), null); assert.deepEqual(LineagePages.normalizeGenealogyMemberOptions([ { memberId: '1', genealogyId: '2', appUserId: '3', memberName: '正常成员', status: '0' }, { memberId: '4', genealogyId: '2', appUserId: null, memberName: '未绑定成员', status: '0' } ]).map((option) => option.appUserId), ['3']); }); test('世系关系模式只使用 PC 已定义的四种新增关系', () => { assert.equal(LineagePages.relationLabel('parents'), '父母'); assert.equal(LineagePages.relationLabel('spouses'), '配偶'); assert.equal(LineagePages.relationLabel('children'), '子女'); assert.equal(LineagePages.relationLabel('siblings'), '兄弟姐妹'); assert.equal(LineagePages.relationLabel('remove'), ''); }); test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => { const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8'); assert.deepEqual(LineagePages.buildLineagePageQuery({ keyword: ' 李明 ', generation: '3', personStatus: '1' }, 2, 20), { pageNum: 2, pageSize: 20, keyword: '李明', generation: 3, personStatus: '1' }); assert.deepEqual(LineagePages.buildLineagePageQuery({}, 1, 20), { pageNum: 1, pageSize: 20 }); assert.match(page, /placeholder="输入姓名、别名或人物编号搜索"/); assert.match(page, /data-lineage-generation-filter/); assert.match(page, /data-lineage-status-filter/); assert.match(page, /data-lineage-pagination/); assert.match(page, /data-lineage-page-action="previous"/); assert.match(page, /data-lineage-page-action="next"/); }); test('世系表单为全部 LineagePersonBody 字段提供真实来源且不允许手填业务 ID', () => { const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8'); const styles = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'profile-module.css'), 'utf8'); [ 'bindingMode', 'appUserId', 'avatarOssId', 'personNo', 'name', 'aliasName', 'sex', 'generation', 'generationName', 'fatherId', 'motherId', 'birthDate', 'birthLunar', 'birthPlace', 'deathDate', 'deathLunar', 'deathPlace', 'burialPlace', 'personStatus', 'biography', 'sortOrder', 'remark', 'relationName' ].forEach((name) => assert.match(page, new RegExp('name="' + name + '"'))); assert.match(page, /value="SPECIFIED"(?![^>]+disabled)/); assert.match(page, /select[^>]+name="appUserId"[^>]+data-lineage-member-options|select[^>]+data-lineage-member-options[^>]+name="appUserId"/); assert.doesNotMatch(page, /name="appUserId"[^>]+type="(?:text|number)"|type="(?:text|number)"[^>]+name="appUserId"/); assert.doesNotMatch(page, /name="appUserId"[^>]+type="hidden"|type="hidden"[^>]+name="appUserId"/); assert.match(page, /name="avatarOssId"[^>]+type="hidden"|type="hidden"[^>]+name="avatarOssId"/); assert.match(page, /data-upload-target="#lineage-avatar-oss-id"/); assert.match(page, /name="generation"[^>]*data-lineage-generation/); assert.match(page, /name="generationName"[^>]+readonly|readonly[^>]+name="generationName"/); assert.match(page, /name="fatherId"[^>]*data-lineage-parent-option/); assert.match(page, /name="motherId"[^>]*data-lineage-parent-option/); assert.match(page, /data-lineage-management[^>]+hidden/); assert.match(page, /public\/js\/upload-pages\.js/); assert.match(styles, /\[data-lineage-management\]\[hidden\]\s*\{[^}]*display:\s*none\s*!important/s); assert.match(styles, /\[data-lineage-spouse-field\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s); assert.match(styles, /\[data-lineage-pagination\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s); }); test('世系管理的 403 不会被当作登录失效', () => { assert.equal(LineagePages.isForbidden({ status: 403 }), true); assert.equal(LineagePages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false); });