feat(api): 完善家谱系统API客户端契约

- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
fizzleaf
2026-07-29 16:57:27 +08:00
parent 59a72fb22b
commit fb1743aa2a
74 changed files with 13166 additions and 1320 deletions
+98 -11
View File
@@ -5,22 +5,109 @@ const test = require('node:test');
const NotificationPages = require('../public/js/notification-pages.js');
test('notification page preserves only the unexpanded PC list records and unread count', () => {
const rows = [{ notificationId: '2060000000000000001', title: '系统通知' }];
function notificationFixture(overrides) {
return Object.assign({
notificationId: '2060000000000000001',
genealogyId: '2060000000000000002',
genealogyNo: 'G20260729001',
genealogyName: '叶氏家谱',
senderUserId: '2060000000000000003',
senderNickName: '叶子',
senderPhone: '19100000000',
noticeType: 'family_feed_comment',
noticeTitle: '新的评论',
noticeContent: '有人评论了家族动态',
bizType: 'family_feed_comment',
bizId: '2060000000000000004',
bizSummary: '家族圈评论',
publishTime: '2026-07-29 10:30:00',
readStatus: '0',
readTime: null,
status: '0',
remark: ''
}, overrides || {});
}
assert.deepEqual(NotificationPages.normalizeNotifications(rows), rows);
assert.deepEqual(NotificationPages.normalizeNotifications({ rows }), []);
assert.equal(NotificationPages.getUnreadCount(3), 3);
assert.equal(NotificationPages.getUnreadCount(-1), 0);
assert.equal(NotificationPages.formatNotification(rows[0]), '{\n "notificationId": "2060000000000000001",\n "title": "系统通知"\n}');
test('通知筛选只允许全部、未读和已读三种查询', () => {
assert.deepEqual(NotificationPages.buildNotificationQuery(''), {});
assert.deepEqual(NotificationPages.buildNotificationQuery('0'), { readStatus: '0' });
assert.deepEqual(NotificationPages.buildNotificationQuery('1'), { readStatus: '1' });
assert.deepEqual(NotificationPages.buildNotificationQuery('unsafe'), {});
});
test('message page loads the PC notification module instead of pending-page controls', () => {
test('通知列表只接受 PC 的非分页数组并规范完整 NotificationView', () => {
const record = notificationFixture();
assert.deepEqual(NotificationPages.normalizeNotifications([record]), [
NotificationPages.normalizeNotification(record)
]);
assert.deepEqual(NotificationPages.normalizeNotifications({ rows: [record] }), []);
assert.equal(NotificationPages.normalizeNotification({
...record,
notificationId: Number('2060000000000000001')
}), null);
assert.equal(NotificationPages.normalizeNotification({
...record,
readStatus: '2'
}), null);
});
test('通知可选业务 ID 接受 null 但拒绝不安全数字', () => {
assert.ok(NotificationPages.normalizeNotification(notificationFixture({
genealogyId: null,
senderUserId: null,
bizId: null
})));
assert.equal(NotificationPages.normalizeNotification(notificationFixture({
bizId: Number('2060000000000000004')
})), null);
});
test('未读数只接受非负安全整数', () => {
assert.equal(NotificationPages.getUnreadCount(3), 3);
assert.equal(NotificationPages.getUnreadCount('3'), 3);
assert.equal(NotificationPages.getUnreadCount(-1), 0);
assert.equal(NotificationPages.getUnreadCount(1.5), 0);
assert.equal(NotificationPages.getUnreadCount(Number.MAX_SAFE_INTEGER + 1), 0);
});
test('通知列表和详情转义正文且不暴露手机号或内部 ID', () => {
const record = NotificationPages.normalizeNotification(notificationFixture({
noticeTitle: '<script>alert(1)</script>',
noticeContent: '<img src=x onerror=alert(1)>通知正文'
}));
const rowHtml = NotificationPages.renderNotificationRow(record);
const detailHtml = NotificationPages.renderNotificationDetail(record);
assert.doesNotMatch(rowHtml + detailHtml, /<script>|<img/);
assert.match(rowHtml + detailHtml, /通知正文/);
assert.match(rowHtml + detailHtml, /未读/);
assert.doesNotMatch(rowHtml + detailHtml, /19100000000|>206000000000000000[1-4]</);
});
test('通知详情必须与请求的稳定通知编号一致', () => {
const record = notificationFixture();
assert.equal(NotificationPages.matchesNotification(record, '2060000000000000001'), true);
assert.equal(NotificationPages.matchesNotification(record, '2060000000000000009'), false);
assert.equal(NotificationPages.matchesNotification({}, '2060000000000000001'), false);
});
test('只有未读且正常的通知提供单条已读动作', () => {
assert.equal(NotificationPages.canMarkRead({ readStatus: '0', status: '0' }), true);
assert.equal(NotificationPages.canMarkRead({ readStatus: '1', status: '0' }), false);
assert.equal(NotificationPages.canMarkRead({ readStatus: '0', status: '1' }), false);
});
test('消息页提供筛选、业务详情和单条/全部已读,不显示原始 JSON', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-messages.html'), 'utf8');
assert.doesNotMatch(source, /data-feature-status="pending"/);
assert.match(source, /src="public\/js\/notification-pages\.js"/);
assert.doesNotMatch(source, /src="public\/js\/pending-pages\.js"/);
assert.doesNotMatch(source, /data-feature-status="pending"|notification-raw|原始记录/);
assert.match(source, /data-notification-filter/);
assert.match(source, /data-notification-list/);
assert.match(source, /data-notification-detail/);
assert.match(source, /data-notification-unread-count/);
assert.match(source, /data-notification-read-all/);
assert.match(source, /src="public\/js\/notification-pages\.js"/);
assert.doesNotMatch(source, /src="public\/js\/pending-pages\.js"/);
});