feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -14,19 +14,203 @@ test('家谱入口只接受本地家谱业务页作为返回目标', () => {
|
||||
assert.equal(GenealogyEntryPages.getTargetPage('?next=https%3A%2F%2Fevil.example'), 'profile-family-home.html');
|
||||
});
|
||||
|
||||
test('家谱入口只展示当前 PC 已定义的额度,不伪造家谱编号', () => {
|
||||
const message = GenealogyEntryPages.buildStatus({ createRemaining: 1, joinRemaining: 2 }, 'profile-feed.html');
|
||||
test('家谱入口只使用 AppGenealogyVo 的真实字段并保持大整数 ID 字符串', () => {
|
||||
assert.deepEqual(GenealogyEntryPages.normalizeGenealogy({
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionFullName: '四川省广安市武胜县',
|
||||
memberCount: 12,
|
||||
personCount: 34,
|
||||
roleType: 'owner',
|
||||
canManage: true,
|
||||
legacyId: 'forbidden'
|
||||
}), {
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionFullName: '四川省广安市武胜县',
|
||||
memberCount: 12,
|
||||
personCount: 34,
|
||||
roleType: 'owner',
|
||||
canManage: true
|
||||
});
|
||||
assert.equal(
|
||||
GenealogyEntryPages.buildEntryUrl('2062179707935264769', 'profile-feed.html'),
|
||||
'profile-feed.html?genealogyId=2062179707935264769'
|
||||
);
|
||||
});
|
||||
|
||||
assert.match(message, /还可创建 1 部/);
|
||||
assert.match(message, /还可加入 2 部/);
|
||||
assert.match(message, /不能伪造家谱编号/);
|
||||
test('家谱创建只构造 AppGenealogyCreateBody 并省略空可选字段', () => {
|
||||
assert.deepEqual(GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
genealogyName: ' 汤氏家谱 ',
|
||||
surname: ' 汤 ',
|
||||
regionCode: '511622',
|
||||
ancestralHall: '',
|
||||
originPlace: ' 四川 ',
|
||||
addressDetail: '',
|
||||
coverOssId: '2062179707935264769',
|
||||
intro: ' 家谱简介 ',
|
||||
visibility: '1',
|
||||
joinMode: '1',
|
||||
genealogyId: 'must-drop',
|
||||
ownerUserId: 'must-drop'
|
||||
}), {
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
originPlace: '四川',
|
||||
coverOssId: '2062179707935264769',
|
||||
intro: '家谱简介',
|
||||
visibility: '1',
|
||||
joinMode: '1'
|
||||
});
|
||||
});
|
||||
|
||||
test('家谱创建校验必填、枚举、上传派生 ID 和真实额度', () => {
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({ surname: '汤', regionCode: '511622' })
|
||||
),
|
||||
'请填写谱名'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({ genealogyName: '汤氏家谱', regionCode: '511622' })
|
||||
),
|
||||
'请填写姓氏'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({ genealogyName: '汤氏家谱', surname: '汤' })
|
||||
),
|
||||
'请选择家谱所在地区'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
visibility: '9'
|
||||
})
|
||||
),
|
||||
'可见范围选项无效'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
joinMode: '9'
|
||||
})
|
||||
),
|
||||
'加入方式选项无效'
|
||||
);
|
||||
assert.equal(
|
||||
GenealogyEntryPages.validateGenealogyCreateBody(
|
||||
GenealogyEntryPages.buildGenealogyCreateBody({
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionCode: '511622',
|
||||
coverOssId: Number.MAX_SAFE_INTEGER + 1
|
||||
})
|
||||
),
|
||||
'封面文件无效,请重新选择'
|
||||
);
|
||||
assert.equal(GenealogyEntryPages.canCreateGenealogy({ createRemaining: 1 }), true);
|
||||
assert.equal(GenealogyEntryPages.canCreateGenealogy({ createRemaining: 0 }), false);
|
||||
assert.equal(GenealogyEntryPages.canCreateGenealogy({ createRemaining: -1, canCreate: true }), true);
|
||||
assert.equal(GenealogyEntryPages.canCreateGenealogy({ createRemaining: 3, canCreate: false }), false);
|
||||
});
|
||||
|
||||
test('创建响应必须提供稳定家谱 ID、谱名和姓氏', () => {
|
||||
const created = GenealogyEntryPages.normalizeCreatedGenealogy({
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionFullName: '四川省广安市武胜县',
|
||||
status: '0'
|
||||
});
|
||||
|
||||
assert.deepEqual(created, {
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤',
|
||||
regionFullName: '四川省广安市武胜县'
|
||||
});
|
||||
assert.equal(
|
||||
GenealogyEntryPages.buildCreatedGenealogyUrl(created),
|
||||
'profile-family-home.html?genealogyId=2062179707935264769'
|
||||
);
|
||||
assert.equal(GenealogyEntryPages.normalizeCreatedGenealogy({
|
||||
genealogyId: Number.MAX_SAFE_INTEGER + 1,
|
||||
genealogyName: '汤氏家谱',
|
||||
surname: '汤'
|
||||
}), null);
|
||||
assert.equal(GenealogyEntryPages.normalizeCreatedGenealogy({
|
||||
genealogyId: '2062179707935264769',
|
||||
genealogyName: '汤氏家谱'
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('负数额度不向用户展示为可用数量', () => {
|
||||
assert.doesNotMatch(
|
||||
GenealogyEntryPages.buildStatus({ createRemaining: -1, joinRemaining: -1 }, 'profile-feed.html'),
|
||||
/-1/
|
||||
);
|
||||
const status = GenealogyEntryPages.buildStatus({ createRemaining: -1, joinRemaining: -1 });
|
||||
|
||||
assert.doesNotMatch(status, /-1/);
|
||||
assert.match(status, /创建不限/);
|
||||
assert.match(status, /加入不限/);
|
||||
});
|
||||
|
||||
test('我的家谱页是可用的真实列表入口', () => {
|
||||
const source = read('profile-families.html');
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"/);
|
||||
assert.doesNotMatch(source, /pending-pages\.js/);
|
||||
assert.match(source, /data-genealogy-list="mine"/);
|
||||
assert.match(source, /genealogy-entry-pages\.js/);
|
||||
});
|
||||
|
||||
test('创建家谱页开放真实 PC 表单且不允许手填业务 ID', () => {
|
||||
const source = read('profile-create-family.html');
|
||||
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"/);
|
||||
assert.doesNotMatch(source, /pending-pages\.js/);
|
||||
assert.match(source, /data-genealogy-create-page/);
|
||||
assert.match(source, /data-genealogy-create-quota/);
|
||||
assert.match(source, /data-genealogy-create-status/);
|
||||
assert.match(source, /name="coverOssId"\s+type="hidden"/);
|
||||
assert.match(source, /data-upload-target="#familyCoverOssId"/);
|
||||
assert.match(source, /public\/js\/region-pages\.js/);
|
||||
assert.match(source, /public\/js\/md5\.js/);
|
||||
assert.match(source, /public\/js\/upload-pages\.js/);
|
||||
assert.match(source, /public\/js\/genealogy-entry-pages\.js/);
|
||||
assert.doesNotMatch(source, /name="(?:genealogyId|applyId|inviterUserId)"/);
|
||||
assert.doesNotMatch(source, /name="coverOssId"[^>]*type="text"/);
|
||||
});
|
||||
|
||||
test('加入家谱页面不允许用户手工填写 genealogyId', () => {
|
||||
const source = read('join-genealogy.html');
|
||||
|
||||
assert.doesNotMatch(source, /name="genealogyId"/);
|
||||
assert.doesNotMatch(source, /请输入家谱ID/);
|
||||
});
|
||||
|
||||
test('家谱业务模块运行时统一委托 ProfileUI 读取和传播上下文', () => {
|
||||
[
|
||||
'feed-pages.js',
|
||||
'growth-pages.js',
|
||||
'lineage-pages.js',
|
||||
'memo-pages.js',
|
||||
'relative-pages.js',
|
||||
'generation-pages.js'
|
||||
].forEach((file) => {
|
||||
const source = read('public/js/' + file);
|
||||
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.getGenealogyId/);
|
||||
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.syncGenealogyContextLinks/);
|
||||
});
|
||||
});
|
||||
|
||||
test('个人中心和内容发布在缺少家谱上下文时统一进入家谱入口', () => {
|
||||
|
||||
Reference in New Issue
Block a user