feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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.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);
|
||||
assert.equal(canTransferOwner('admin'), 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: '<img src=x>明远',
|
||||
appUserPhone: '19100000000',
|
||||
lineagePersonId: '2060000000000000004',
|
||||
lineagePersonName: '<script>alert(1)</script>李明',
|
||||
memberName: '李先生',
|
||||
roleType: 'member',
|
||||
relationName: '族亲',
|
||||
inviterUserId: '2060000000000000005',
|
||||
inviterPhone: '19111111111',
|
||||
status: '0'
|
||||
}), {
|
||||
actorRole: 'member',
|
||||
currentMemberId: '2060000000000000001'
|
||||
});
|
||||
|
||||
assert.doesNotMatch(html, /<script|<img/);
|
||||
assert.doesNotMatch(html, /19100000000|19111111111|206000000000000000[1-5]/);
|
||||
assert.match(html, /李先生|族亲|成员/);
|
||||
});
|
||||
|
||||
test('家族管理页开放成员维护、退出和谱主转移且不允许手填 ID', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-family-admin.html'), '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, /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, /select[^>]+name="targetMemberId"|name="targetMemberId"[^>]+select/);
|
||||
assert.doesNotMatch(page, /name="(?:memberId|appUserId)"|type="(?:text|number)"[^>]+name="(?:lineagePersonId|targetMemberId)"/);
|
||||
assert.doesNotMatch(page, /href="profile-data\.html"[^>]*>\s*添加成员/);
|
||||
});
|
||||
Reference in New Issue
Block a user