feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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', 'ceremony-admin-pages.js');
|
||||
const CeremonyAdminPages = fs.existsSync(modulePath) ? require(modulePath) : {};
|
||||
|
||||
function requireFunction(name) {
|
||||
assert.equal(typeof CeremonyAdminPages[name], 'function', `缺少 CeremonyAdminPages.${name}`);
|
||||
return CeremonyAdminPages[name];
|
||||
}
|
||||
|
||||
test('祭祀管理页只从上下文读取安全的家谱和活动 ID', () => {
|
||||
const getCurrentGenealogyId = requireFunction('getCurrentGenealogyId');
|
||||
const getCurrentCeremonyId = requireFunction('getCurrentCeremonyId');
|
||||
|
||||
assert.equal(getCurrentGenealogyId('?genealogyId=2060000000000000001'), '2060000000000000001');
|
||||
assert.equal(getCurrentCeremonyId('?ceremonyId=2060000000000000002'), '2060000000000000002');
|
||||
assert.equal(getCurrentCeremonyId('?ceremonyId=unsafe'), '');
|
||||
});
|
||||
|
||||
test('活动写入只构造 CeremonyBody 并固定正常状态', () => {
|
||||
const buildCeremonyBody = requireFunction('buildCeremonyBody');
|
||||
|
||||
assert.deepEqual(buildCeremonyBody({
|
||||
ceremonyType: ' ancestor ',
|
||||
ceremonyTitle: ' 清明祭祖 ',
|
||||
ceremonyDesc: ' 缅怀先祖 ',
|
||||
ceremonyTime: '2026-04-04T09:00',
|
||||
location: ' 祠堂 ',
|
||||
locationAddress: ' 四川省成都市示例路1号 ',
|
||||
longitude: '104.066541',
|
||||
latitude: '30.572269',
|
||||
coverOssId: '2060000000000000003',
|
||||
sortOrder: '1',
|
||||
status: '1',
|
||||
ceremonyId: 'must-drop',
|
||||
giftAmount: 999
|
||||
}), {
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '清明祭祖',
|
||||
ceremonyDesc: '缅怀先祖',
|
||||
ceremonyTime: '2026-04-04 09:00:00',
|
||||
location: '祠堂',
|
||||
locationAddress: '四川省成都市示例路1号',
|
||||
longitude: 104.066541,
|
||||
latitude: 30.572269,
|
||||
coverOssId: '2060000000000000003',
|
||||
sortOrder: 1,
|
||||
status: '0'
|
||||
});
|
||||
});
|
||||
|
||||
test('活动校验必填字段、成对坐标、范围、长度和上传派生 ID', () => {
|
||||
const buildCeremonyBody = requireFunction('buildCeremonyBody');
|
||||
const validateCeremonyBody = requireFunction('validateCeremonyBody');
|
||||
|
||||
assert.equal(validateCeremonyBody({ ceremonyType: '', ceremonyTitle: '', status: '0' }), '请填写活动类型');
|
||||
assert.equal(validateCeremonyBody({ ceremonyType: 'ancestor', ceremonyTitle: '', status: '0' }), '请填写活动标题');
|
||||
assert.equal(
|
||||
validateCeremonyBody(buildCeremonyBody({ ceremonyType: 'ancestor', ceremonyTitle: '祭祖', longitude: '104' })),
|
||||
'经度和纬度必须同时提供'
|
||||
);
|
||||
assert.equal(
|
||||
validateCeremonyBody(buildCeremonyBody({ ceremonyType: 'ancestor', ceremonyTitle: '祭祖', longitude: '181', latitude: '30' })),
|
||||
'经度范围必须是 -180 到 180'
|
||||
);
|
||||
assert.equal(
|
||||
validateCeremonyBody(buildCeremonyBody({ ceremonyType: 'ancestor', ceremonyTitle: '祭祖', locationAddress: '地'.repeat(301) })),
|
||||
'详细地址不能超过 300 个字符'
|
||||
);
|
||||
assert.equal(
|
||||
validateCeremonyBody(buildCeremonyBody({ ceremonyType: 'ancestor', ceremonyTitle: '祭祖', coverOssId: Number.MAX_SAFE_INTEGER + 1 })),
|
||||
'封面文件编号无效,请重新选择文件'
|
||||
);
|
||||
});
|
||||
|
||||
test('祭品写入只提交 CeremonyGiftBody 并校验后端非负金额', () => {
|
||||
const buildCeremonyGiftBody = requireFunction('buildCeremonyGiftBody');
|
||||
const validateCeremonyGiftBody = requireFunction('validateCeremonyGiftBody');
|
||||
|
||||
assert.deepEqual(buildCeremonyGiftBody({
|
||||
giverName: ' 叶子 ',
|
||||
giftAmount: '66.66',
|
||||
giftMessage: ' 缅怀先祖 ',
|
||||
giverUserId: 'must-drop',
|
||||
giftTime: 'must-drop'
|
||||
}), {
|
||||
giverName: '叶子',
|
||||
giftAmount: 66.66,
|
||||
giftMessage: '缅怀先祖'
|
||||
});
|
||||
assert.equal(validateCeremonyGiftBody(buildCeremonyGiftBody({ giftAmount: '' })), '请填写礼金金额');
|
||||
assert.equal(validateCeremonyGiftBody(buildCeremonyGiftBody({ giftAmount: '-0.01' })), '礼金金额不能小于 0');
|
||||
assert.equal(validateCeremonyGiftBody(buildCeremonyGiftBody({ giftAmount: 'not-number' })), '礼金金额必须是有效数字');
|
||||
});
|
||||
|
||||
test('活动、祭品和邀请响应使用完整 PC VO 并拒绝不安全 ID', () => {
|
||||
const normalizeCeremony = requireFunction('normalizeCeremony');
|
||||
const normalizeCeremonyGift = requireFunction('normalizeCeremonyGift');
|
||||
const normalizeInvitation = requireFunction('normalizeInvitation');
|
||||
const ceremony = normalizeCeremony({
|
||||
ceremonyId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
sponsorUserId: '2060000000000000003',
|
||||
sponsorNickName: '叶子',
|
||||
sponsorPhone: '19181970173',
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '清明祭祖',
|
||||
ceremonyDesc: '缅怀先祖',
|
||||
ceremonyTime: '2026-04-04 09:00:00',
|
||||
location: '祠堂',
|
||||
locationAddress: '成都',
|
||||
longitude: 104.066541,
|
||||
latitude: 30.572269,
|
||||
coverOssId: '2060000000000000004',
|
||||
giftCount: 2,
|
||||
giftAmount: 88.88,
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: ''
|
||||
});
|
||||
const gift = normalizeCeremonyGift({
|
||||
giftId: '2060000000000000005',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000001',
|
||||
ceremonyTitle: '清明祭祖',
|
||||
giverUserId: '2060000000000000006',
|
||||
giverNickName: '宗亲',
|
||||
giverPhone: '19100000000',
|
||||
giverName: '叶先生',
|
||||
giftAmount: 66.66,
|
||||
giftMessage: '缅怀先祖',
|
||||
giftTime: '2026-04-04 10:00:00',
|
||||
status: '0',
|
||||
remark: ''
|
||||
});
|
||||
const invitation = normalizeInvitation({
|
||||
invitationId: '2060000000000000007',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000001',
|
||||
inviteeUserId: '2060000000000000006',
|
||||
inviteStatus: 'PENDING',
|
||||
inviteVersion: 1
|
||||
});
|
||||
|
||||
assert.equal(ceremony.ceremonyId, '2060000000000000001');
|
||||
assert.equal(ceremony.giftCount, 2);
|
||||
assert.equal(gift.giftId, '2060000000000000005');
|
||||
assert.equal(invitation.inviteeUserId, '2060000000000000006');
|
||||
assert.equal(normalizeCeremony({
|
||||
ceremonyId: Number('2060000000000000001'),
|
||||
genealogyId: '2',
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '祭祖',
|
||||
status: '0'
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('管理员邀约名单只使用正常成员中的真实 appUserId 并去重', () => {
|
||||
const normalizeInviteeOption = requireFunction('normalizeInviteeOption');
|
||||
const buildInviteesBody = requireFunction('buildInviteesBody');
|
||||
const normal = normalizeInviteeOption({
|
||||
memberId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: '2060000000000000001',
|
||||
appUserNickName: '宗亲',
|
||||
memberName: '叶先生',
|
||||
status: '0'
|
||||
});
|
||||
|
||||
assert.equal(normal.appUserId, '2060000000000000001');
|
||||
assert.equal(normalizeInviteeOption({
|
||||
memberId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: null,
|
||||
memberName: '未绑定成员',
|
||||
status: '0'
|
||||
}), null);
|
||||
assert.equal(normalizeInviteeOption({
|
||||
memberId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: '3',
|
||||
memberName: '停用成员',
|
||||
status: '1'
|
||||
}), null);
|
||||
assert.deepEqual(buildInviteesBody([
|
||||
'2060000000000000001',
|
||||
'2060000000000000001',
|
||||
'2060000000000000002',
|
||||
'unsafe'
|
||||
]), {
|
||||
inviteeUserIds: ['2060000000000000001', '2060000000000000002']
|
||||
});
|
||||
});
|
||||
|
||||
test('活动和祭品展示转义内容且不暴露手机号、OSS ID 或内部 ID', () => {
|
||||
const normalizeCeremony = requireFunction('normalizeCeremony');
|
||||
const normalizeCeremonyGift = requireFunction('normalizeCeremonyGift');
|
||||
const renderCeremonyDetail = requireFunction('renderCeremonyDetail');
|
||||
const renderGiftList = requireFunction('renderGiftList');
|
||||
const html = renderCeremonyDetail(normalizeCeremony({
|
||||
ceremonyId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
sponsorUserId: '2060000000000000003',
|
||||
sponsorNickName: '<img src=x>叶子',
|
||||
sponsorPhone: '19181970173',
|
||||
ceremonyType: 'ancestor',
|
||||
ceremonyTitle: '<script>alert(1)</script>祭祖',
|
||||
coverOssId: '2060000000000000004',
|
||||
giftCount: 1,
|
||||
giftAmount: 66.66,
|
||||
status: '0'
|
||||
})) + renderGiftList([normalizeCeremonyGift({
|
||||
giftId: '2060000000000000005',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000001',
|
||||
giverUserId: '2060000000000000006',
|
||||
giverNickName: '<img src=x>宗亲',
|
||||
giverPhone: '19100000000',
|
||||
giverName: '叶先生',
|
||||
giftAmount: 66.66,
|
||||
giftMessage: '<script>alert(2)</script>缅怀',
|
||||
status: '0'
|
||||
})]);
|
||||
|
||||
assert.doesNotMatch(html, /<script|<img/);
|
||||
assert.doesNotMatch(html, /19181970173|19100000000|206000000000000000[1-6]/);
|
||||
assert.match(html, /祭祖|缅怀|66\.66/);
|
||||
});
|
||||
|
||||
test('祭祀功能拆分列表、编辑和详情页面且不允许手填 ID', () => {
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(projectRoot, 'profile-gift-edit.html'), 'utf8');
|
||||
const detailPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony-detail.html'), 'utf8');
|
||||
|
||||
[listPage, editPage, detailPage].forEach((source) => {
|
||||
assert.doesNotMatch(source, /data-feature-status="pending"|pending-pages\.js/);
|
||||
assert.match(source, /public\/js\/ceremony-admin-pages\.js/);
|
||||
});
|
||||
assert.match(listPage, /data-ceremony-list/);
|
||||
assert.match(editPage, /name="ceremonyType"/);
|
||||
assert.match(editPage, /name="coverOssId" type="hidden"/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(detailPage, /data-ceremony-gift-list/);
|
||||
assert.match(detailPage, /data-ceremony-invitee-options/);
|
||||
assert.match(detailPage, /name="giftAmount"/);
|
||||
assert.doesNotMatch(editPage + detailPage, /name="(?:genealogyId|ceremonyId|giftId|inviteeUserIds)"/);
|
||||
});
|
||||
Reference in New Issue
Block a user