Files
jiapu/tests/ceremony-admin-pages.test.js
T
2026-08-29 19:06:07 +08:00

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, /活动编辑/);
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;/);
});