feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const CeremonyPages = require('../public/js/ceremony-pages.js');
|
||||
|
||||
test('我的邀请响应保留完整展示字段并保持业务 ID 为字符串', () => {
|
||||
assert.deepEqual(CeremonyPages.normalizeInvitation({
|
||||
invitationId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000003',
|
||||
inviteeUserId: '2060000000000000004',
|
||||
inviteStatus: 'PENDING',
|
||||
inviteVersion: 2,
|
||||
deliveredTime: '2026-07-29T09:00:00+08:00',
|
||||
readTime: null,
|
||||
responseTime: null,
|
||||
ceremonyTitle: '家族答谢宴',
|
||||
ceremonyTime: '2026-08-08T18:00:00+08:00',
|
||||
location: '锦江厅',
|
||||
locationAddress: '成都市锦江区示例路 8 号',
|
||||
longitude: 104.0668,
|
||||
latitude: 30.5728
|
||||
}), {
|
||||
invitationId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000003',
|
||||
inviteeUserId: '2060000000000000004',
|
||||
inviteStatus: 'PENDING',
|
||||
inviteVersion: 2,
|
||||
deliveredTime: '2026-07-29T09:00:00+08:00',
|
||||
readTime: '',
|
||||
responseTime: '',
|
||||
ceremonyTitle: '家族答谢宴',
|
||||
ceremonyTime: '2026-08-08T18:00:00+08:00',
|
||||
location: '锦江厅',
|
||||
locationAddress: '成都市锦江区示例路 8 号',
|
||||
longitude: 104.0668,
|
||||
latitude: 30.5728
|
||||
});
|
||||
});
|
||||
|
||||
test('邀请响应拒绝缺少稳定 ID、非法状态和不安全数字长 ID', () => {
|
||||
assert.equal(CeremonyPages.normalizeInvitation({
|
||||
invitationId: '1',
|
||||
genealogyId: '2',
|
||||
inviteeUserId: '4',
|
||||
inviteStatus: 'PENDING'
|
||||
}), null);
|
||||
assert.equal(CeremonyPages.normalizeInvitation({
|
||||
invitationId: '1',
|
||||
genealogyId: '2',
|
||||
ceremonyId: '3',
|
||||
inviteeUserId: '4',
|
||||
inviteStatus: 'UNKNOWN'
|
||||
}), null);
|
||||
assert.equal(CeremonyPages.normalizeInvitation({
|
||||
invitationId: Number('2060000000000000001'),
|
||||
genealogyId: '2',
|
||||
ceremonyId: '3',
|
||||
inviteeUserId: '4',
|
||||
inviteStatus: 'PENDING'
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('YAML 未声明必返的 inviteeUserId 缺失时仍可用活动上下文响应', () => {
|
||||
const invitation = CeremonyPages.normalizeInvitation({
|
||||
invitationId: '1',
|
||||
genealogyId: '2',
|
||||
ceremonyId: '3',
|
||||
inviteStatus: 'PENDING'
|
||||
});
|
||||
|
||||
assert.notEqual(invitation, null);
|
||||
assert.equal(invitation.inviteeUserId, undefined);
|
||||
assert.equal(CeremonyPages.canRespondToInvitation(invitation), true);
|
||||
});
|
||||
|
||||
test('响应邀请只允许提交 YAML 定义的 inviteStatus 字段', () => {
|
||||
assert.deepEqual(CeremonyPages.buildInvitationResponseBody({
|
||||
inviteStatus: 'ACCEPTED',
|
||||
invitationId: '2060000000000000001',
|
||||
inviteVersion: 2
|
||||
}), {
|
||||
inviteStatus: 'ACCEPTED'
|
||||
});
|
||||
assert.deepEqual(CeremonyPages.buildInvitationResponseBody({ inviteStatus: 'DECLINED' }), {
|
||||
inviteStatus: 'DECLINED'
|
||||
});
|
||||
assert.equal(CeremonyPages.validateInvitationResponseBody({ inviteStatus: 'PENDING' }), '邀请只能选择接受或拒绝');
|
||||
});
|
||||
|
||||
test('只有待响应邀请显示接受和拒绝动作', () => {
|
||||
assert.equal(CeremonyPages.canRespondToInvitation({ inviteStatus: 'PENDING' }), true);
|
||||
assert.equal(CeremonyPages.canRespondToInvitation({ inviteStatus: 'ACCEPTED' }), false);
|
||||
assert.equal(CeremonyPages.canRespondToInvitation({ inviteStatus: 'DECLINED' }), false);
|
||||
assert.equal(CeremonyPages.canRespondToInvitation({ inviteStatus: 'CANCELED' }), false);
|
||||
});
|
||||
|
||||
test('邀请列表渲染转义内容、展示详情但不直接暴露内部 ID 和坐标', () => {
|
||||
const invitation = CeremonyPages.normalizeInvitation({
|
||||
invitationId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
ceremonyId: '2060000000000000003',
|
||||
inviteeUserId: '2060000000000000004',
|
||||
inviteStatus: 'PENDING',
|
||||
inviteVersion: 1,
|
||||
ceremonyTitle: '<script>alert(1)</script>',
|
||||
ceremonyTime: '2026-08-08T18:00:00+08:00',
|
||||
location: '锦江厅',
|
||||
locationAddress: '成都市锦江区示例路 8 号',
|
||||
longitude: 104.0668,
|
||||
latitude: 30.5728
|
||||
});
|
||||
const row = CeremonyPages.renderInvitationRow(invitation);
|
||||
const detail = CeremonyPages.renderInvitationDetail(invitation);
|
||||
|
||||
assert.doesNotMatch(row, /<script>/);
|
||||
assert.match(row, /接受邀请/);
|
||||
assert.match(row, /拒绝邀请/);
|
||||
assert.match(detail, /成都市锦江区示例路 8 号/);
|
||||
assert.match(detail, /打开地图/);
|
||||
assert.doesNotMatch(detail, /206000000000000000[1-4]/);
|
||||
assert.doesNotMatch(detail, />104\.0668</);
|
||||
assert.doesNotMatch(detail, />30\.5728</);
|
||||
});
|
||||
|
||||
test('贺礼邀请页面只接入我的邀请闭环,不提供活动或献礼猜测表单', () => {
|
||||
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-gift.html'), 'utf8');
|
||||
|
||||
assert.doesNotMatch(page, /data-feature-status="pending"/);
|
||||
assert.match(page, /data-my-invitation-list/);
|
||||
assert.match(page, /data-my-invitation-detail/);
|
||||
assert.match(page, /src="public\/js\/ceremony-pages\.js"/);
|
||||
assert.doesNotMatch(page, /data-ceremony-gift-form/);
|
||||
assert.doesNotMatch(page, /href="profile-gift-edit\.html"/);
|
||||
});
|
||||
Reference in New Issue
Block a user