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: '叶子', sponsorPhone: '19181970173', ceremonyType: 'ancestor', ceremonyTitle: '祭祖', coverOssId: '2060000000000000004', giftCount: 1, giftAmount: 66.66, status: '0' })) + renderGiftList([normalizeCeremonyGift({ giftId: '2060000000000000005', genealogyId: '2060000000000000002', ceremonyId: '2060000000000000001', giverUserId: '2060000000000000006', giverNickName: '宗亲', giverPhone: '19100000000', giverName: '叶先生', giftAmount: 66.66, giftMessage: '缅怀', status: '0' })]); assert.doesNotMatch(html, / { 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)"/); });