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', 'member-admin-pages.js'); const MemberAdminPages = fs.existsSync(modulePath) ? require(modulePath) : {}; function requireFunction(name) { assert.equal(typeof MemberAdminPages[name], 'function', `缺少 MemberAdminPages.${name}`); return MemberAdminPages[name]; } test('成员管理页只从家谱上下文读取安全 ID', () => { const getCurrentGenealogyId = requireFunction('getCurrentGenealogyId'); assert.equal(getCurrentGenealogyId('?genealogyId=2060000000000000001'), '2060000000000000001'); assert.equal(getCurrentGenealogyId('?genealogyId=unsafe'), ''); assert.equal(getCurrentGenealogyId(''), ''); }); test('成员响应使用完整 GenealogyMemberVo 并保持长 ID 为字符串', () => { const normalizeMember = requireFunction('normalizeMember'); const member = normalizeMember({ memberId: '2060000000000000001', genealogyId: '2060000000000000002', appUserId: '2060000000000000003', lineagePersonId: '2060000000000000004', appUserNickName: '明远', appUserPhone: '19100000000', lineagePersonName: '李明', memberName: '李先生', roleType: 'member', relationName: '族亲', joinSource: 'invite', inviterUserId: '2060000000000000005', inviterNickName: '谱主', inviterPhone: '19111111111', joinTime: '2026-07-29 10:00:00', status: '0' }); assert.equal(member.memberId, '2060000000000000001'); assert.equal(member.appUserId, '2060000000000000003'); assert.equal(member.lineagePersonId, '2060000000000000004'); assert.equal(normalizeMember({ memberId: Number('2060000000000000001'), genealogyId: '2', appUserId: '3', roleType: 'member', status: '0' }), null); }); test('成员修改只构造后端可更新字段并收紧角色枚举', () => { const buildMemberUpdateBody = requireFunction('buildMemberUpdateBody'); const validateMemberUpdateBody = requireFunction('validateMemberUpdateBody'); assert.deepEqual(buildMemberUpdateBody({ memberName: ' 李先生 ', relationName: ' 族亲 ', roleType: 'editor', lineagePersonId: '2060000000000000004', memberId: 'must-drop', appUserId: 'must-drop', status: '3' }), { memberName: '李先生', relationName: '族亲', roleType: 'editor', lineagePersonId: '2060000000000000004' }); assert.deepEqual(buildMemberUpdateBody({ relationName: ' ' }), {}); assert.equal(validateMemberUpdateBody(buildMemberUpdateBody({ memberName: '名'.repeat(51) })), '成员名称不能超过 50 个字符'); assert.equal(validateMemberUpdateBody(buildMemberUpdateBody({ relationName: '关系'.repeat(51) })), '关系名称不能超过 100 个字符'); assert.throws( () => buildMemberUpdateBody({ roleType: 'owner' }), /成员角色只能是管理员、编辑或成员/ ); assert.equal( validateMemberUpdateBody(buildMemberUpdateBody({ lineagePersonId: Number.MAX_SAFE_INTEGER + 1 })), '世系人物选项无效' ); }); test('成员管理动作遵守谱主和管理员的后端权限边界', () => { const canEditMember = requireFunction('canEditMember'); const canRemoveMember = requireFunction('canRemoveMember'); const canLeaveGenealogy = requireFunction('canLeaveGenealogy'); const canTransferOwner = requireFunction('canTransferOwner'); assert.equal(canEditMember('owner', 'admin'), true); assert.equal(canEditMember('admin', 'admin'), false); assert.equal(canEditMember('admin', 'member'), true); assert.equal(canEditMember('owner', 'owner'), false); assert.equal(canRemoveMember('owner', 'owner'), false); assert.equal(canRemoveMember('admin', 'admin'), false); assert.equal(canLeaveGenealogy('owner'), false); assert.equal(canLeaveGenealogy('member'), true); assert.equal(canTransferOwner('owner', true), true); assert.equal(canTransferOwner('owner', false), false); assert.equal(canTransferOwner('owner', 'true'), false); assert.equal(canTransferOwner('admin', true), false); }); test('谱主转移只使用成员列表中的真实目标 memberId', () => { const buildOwnerTransferBody = requireFunction('buildOwnerTransferBody'); const validateOwnerTransferBody = requireFunction('validateOwnerTransferBody'); assert.deepEqual(buildOwnerTransferBody('2060000000000000001'), { targetMemberId: '2060000000000000001' }); assert.equal(validateOwnerTransferBody(buildOwnerTransferBody('')), '请选择新谱主'); assert.equal(validateOwnerTransferBody(buildOwnerTransferBody('unsafe')), '请选择新谱主'); assert.equal( validateOwnerTransferBody(buildOwnerTransferBody('2060000000000000001'), '2060000000000000001'), '不能转让给自己' ); }); test('世系人物绑定选项只接受当前家谱响应中的安全人物 ID', () => { const normalizeLineageOption = requireFunction('normalizeLineageOption'); assert.deepEqual(normalizeLineageOption({ personId: '2060000000000000001', name: '李明', generationName: '德' }), { personId: '2060000000000000001', name: '李明', generationName: '德' }); assert.equal(normalizeLineageOption({ personId: Number('2060000000000000001'), name: '李明' }), null); assert.equal(normalizeLineageOption({ personId: '1', name: '' }), null); }); test('成员列表展示转义内容并隐藏手机号与内部 ID', () => { const normalizeMember = requireFunction('normalizeMember'); const renderMemberRow = requireFunction('renderMemberRow'); const html = renderMemberRow(normalizeMember({ memberId: '2060000000000000001', genealogyId: '2060000000000000002', appUserId: '2060000000000000003', appUserNickName: '明远', appUserPhone: '19100000000', lineagePersonId: '2060000000000000004', lineagePersonName: '李明', memberName: '李先生', roleType: 'member', relationName: '族亲', inviterUserId: '2060000000000000005', inviterPhone: '19111111111', status: '0' }), { actorRole: 'member', currentMemberId: '2060000000000000001' }); assert.doesNotMatch(html, / { const page = fs.readFileSync(path.join(__dirname, '..', 'profile-family-admin.html'), 'utf8'); const script = fs.readFileSync(modulePath, 'utf8'); assert.doesNotMatch(page, /data-feature-status="pending"|pending-pages\.js/); assert.match(page, /public\/js\/member-admin-pages\.js/); assert.match(page, /data-member-list/); assert.match(page, /data-member-form/); assert.match(page, /data-member-form novalidate/); assert.match(page, /name="memberName"/); assert.match(page, /name="relationName"/); assert.match(page, /name="roleType"/); assert.match(page, /select[^>]+name="lineagePersonId"|name="lineagePersonId"[^>]+select/); assert.match(page, /data-member-leave/); assert.match(page, /data-member-action-status/); assert.match(page, /select[^>]+name="targetMemberId"|name="targetMemberId"[^>]+select/); assert.match(page, /data-owner-transfer-form novalidate/); assert.match(page, /name="targetMemberId"[^>]+required/); assert.match(page, /data-member-form-status/); assert.match(page, /空值清除语义/); assert.match(page, /data-owner-transfer-status/); assert.doesNotMatch(page, /name="(?:memberId|appUserId)"|type="(?:text|number)"[^>]+name="(?:lineagePersonId|targetMemberId)"/); assert.doesNotMatch(page, /href="profile-data\.html"[^>]*>\s*添加成员/); assert.match(script, /function setMemberListState\(type, message\)/); assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/); assert.match(script, /setMemberListState\('loading', '正在加载成员…'\)/); assert.match(script, /setStatus\('\[data-member-form-status\]', '正在保存成员…', 'loading'\)/); assert.match(script, /setStatus\('\[data-owner-transfer-status\]', '正在转让谱主…', 'loading'\)/); assert.match(script, /setStatus\('\[data-member-action-status\]', '正在移出成员…', 'loading'\)/); assert.match(script, /canTransferOwner\(actorRole, managementAccess\)/); });