300 lines
13 KiB
JavaScript
300 lines
13 KiB
JavaScript
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: '测试联系方式',
|
|
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: '测试联系方式',
|
|
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, /测试联系方式|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, /data-ceremony-form data-ceremony-editor hidden novalidate/);
|
|
assert.match(editPage, /class="field-required"/);
|
|
assert.match(editPage, /<div class="upload-status" data-ceremony-form-status/);
|
|
assert.match(editPage, /Ceremony Editor/);
|
|
assert.doesNotMatch(editPage, /创建贺礼|贺礼标题|贺礼内容/);
|
|
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.match(detailPage, /data-ceremony-gift-form data-ceremony-admin-editor hidden novalidate/);
|
|
assert.match(detailPage, /data-ceremony-invitee-form data-ceremony-admin-editor hidden novalidate/);
|
|
assert.match(detailPage, /<div class="upload-status" data-ceremony-gift-status/);
|
|
assert.match(detailPage, /<div class="upload-status" data-ceremony-invitee-status/);
|
|
assert.doesNotMatch(editPage + detailPage, /name="(?:genealogyId|ceremonyId|giftId|inviteeUserIds)"/);
|
|
});
|
|
|
|
test('活动、祭品和受邀名单复用统一加载、空数据、403 与失败状态', () => {
|
|
const projectRoot = path.join(__dirname, '..');
|
|
const listPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony.html'), 'utf8');
|
|
const detailPage = fs.readFileSync(path.join(projectRoot, 'profile-ceremony-detail.html'), 'utf8');
|
|
const source = fs.readFileSync(modulePath, 'utf8');
|
|
|
|
assert.match(listPage, /data-ceremony-list[^>]*>[\s\S]*data-api-state="loading"/);
|
|
assert.match(detailPage, /data-ceremony-detail[^>]*>[\s\S]*data-api-state="loading"/);
|
|
assert.match(detailPage, /data-ceremony-gift-list[^>]*>[\s\S]*data-api-state="loading"/);
|
|
assert.match(detailPage, /data-ceremony-invitation-list[^>]*>[\s\S]*data-api-state="loading"/);
|
|
assert.match(detailPage, /data-ceremony-invitee-options[^>]*>[\s\S]*data-api-state="loading"/);
|
|
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-list\]', '正在加载祭祀活动…', 'loading'\)/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-list\]', '暂无祭祀活动', 'empty'\)/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-gift-list\]', '暂无祭品记录', 'empty'\)/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-invitation-list\]', '暂无邀请记录', 'empty'\)/);
|
|
assert.match(source, /当前账号无权查看或维护受邀名单。/);
|
|
assert.match(source, /正在读取家谱权限…', 'loading'/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-form-status\]', validation, 'error'\)/);
|
|
assert.match(source, /setStatus\('\[data-ceremony-gift-status\]', validation, 'error'\)/);
|
|
assert.match(source, /正在更新受邀人…', 'loading'/);
|
|
assert.match(source, /getApiStateType\(error\)/);
|
|
});
|
|
|
|
test('祭祀管理只接受后端布尔能力字段,并在全部写路径上门控', () => {
|
|
const source = fs.readFileSync(modulePath, 'utf8');
|
|
const canEditCeremonies = requireFunction('canEditCeremonies');
|
|
|
|
assert.equal(canEditCeremonies({ canEditContent: true }), true);
|
|
assert.equal(canEditCeremonies({ canManage: true }), true);
|
|
assert.equal(canEditCeremonies({ canEditContent: 'true', canManage: false }), false);
|
|
assert.match(source, /canEditContent = canEditCeremonies\(detail\)/);
|
|
assert.match(source, /root\.AppRichEditor && root\.AppRichEditor\.syncAll/);
|
|
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| redirectUnauthorized\(api\)\) return;/);
|
|
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !ceremonyId \|\| redirectUnauthorized\(api\)\) return;/);
|
|
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !normalizeId\(ceremonyId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
|
assert.match(source, /if \(writePending \|\| !canEditContent \|\| !genealogyId \|\| !ceremonyId \|\| !normalizeId\(giftId\) \|\| redirectUnauthorized\(api\)\) return;/);
|
|
});
|