feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const FeedPages = require('../public/js/feed-pages.js');
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
function read(file) {
|
||||
return fs.readFileSync(path.join(rootDir, file), 'utf8');
|
||||
}
|
||||
|
||||
test('动态表单只构造最新文档定义的 FamilyFeedBody 字段', () => {
|
||||
assert.deepEqual(
|
||||
@@ -71,3 +78,192 @@ test('评论响应使用 commentContent,并保留已删除评论的占位状
|
||||
userDeleted: true
|
||||
});
|
||||
});
|
||||
|
||||
test('动态响应保留页面闭环所需的完整 FamilyFeedView 字段和字符串 ID', () => {
|
||||
assert.deepEqual(FeedPages.normalizeFeed({
|
||||
feedId: 2060000000000000000n,
|
||||
genealogyId: '2061000000000000000',
|
||||
genealogyNo: 'G20260728001',
|
||||
genealogyName: '叶氏家谱',
|
||||
publisherUserId: '2062000000000000000',
|
||||
publisherNickName: '叶子',
|
||||
publisherStatus: '0',
|
||||
feedType: 'text',
|
||||
feedContent: '今日修谱。',
|
||||
mediaOssIds: '301,302',
|
||||
likedByMe: true,
|
||||
likeCount: 8,
|
||||
commentCount: 3,
|
||||
pinned: '1',
|
||||
pinnedTime: '2026-07-28 10:00:00',
|
||||
sortOrder: 2,
|
||||
status: '0',
|
||||
remark: '族长发布',
|
||||
createTime: '2026-07-28 09:00:00',
|
||||
updateTime: '2026-07-28 09:30:00'
|
||||
}), {
|
||||
feedId: '2060000000000000000',
|
||||
genealogyId: '2061000000000000000',
|
||||
genealogyNo: 'G20260728001',
|
||||
genealogyName: '叶氏家谱',
|
||||
publisherUserId: '2062000000000000000',
|
||||
publisherNickName: '叶子',
|
||||
publisherStatus: '0',
|
||||
feedType: 'text',
|
||||
feedContent: '今日修谱。',
|
||||
mediaOssIds: '301,302',
|
||||
likedByMe: true,
|
||||
likeCount: 8,
|
||||
commentCount: 3,
|
||||
pinned: '1',
|
||||
pinnedTime: '2026-07-28 10:00:00',
|
||||
sortOrder: 2,
|
||||
status: '0',
|
||||
remark: '族长发布',
|
||||
createTime: '2026-07-28 09:00:00',
|
||||
updateTime: '2026-07-28 09:30:00'
|
||||
});
|
||||
});
|
||||
|
||||
test('评论响应保留回复、归属、层级和时间字段', () => {
|
||||
assert.deepEqual(FeedPages.normalizeComment({
|
||||
commentId: '2060000000000000001',
|
||||
genealogyId: '2061000000000000000',
|
||||
feedId: '2060000000000000000',
|
||||
parentCommentId: '2060000000000000002',
|
||||
appUserId: '2062000000000000000',
|
||||
parentAppUserId: '2062000000000000003',
|
||||
appUserNickName: '叶子',
|
||||
appUserAvatar: '401',
|
||||
parentAppUserNickName: '小叶',
|
||||
commentContent: '收到',
|
||||
replyCount: 2,
|
||||
commentLevel: 'reply',
|
||||
userDeleted: '0',
|
||||
status: '0',
|
||||
createTime: '2026-07-28 10:30:00'
|
||||
}), {
|
||||
commentId: '2060000000000000001',
|
||||
genealogyId: '2061000000000000000',
|
||||
feedId: '2060000000000000000',
|
||||
parentCommentId: '2060000000000000002',
|
||||
appUserId: '2062000000000000000',
|
||||
parentAppUserId: '2062000000000000003',
|
||||
appUserNickName: '叶子',
|
||||
appUserAvatar: '401',
|
||||
parentAppUserNickName: '小叶',
|
||||
commentContent: '收到',
|
||||
replyCount: 2,
|
||||
commentLevel: 'reply',
|
||||
userDeleted: false,
|
||||
status: '0',
|
||||
createTime: '2026-07-28 10:30:00'
|
||||
});
|
||||
});
|
||||
|
||||
test('动态、评论和回复分页只接受正整数页码并使用固定页面大小', () => {
|
||||
assert.deepEqual(FeedPages.buildPageQuery('3'), { pageNum: 3, pageSize: 20 });
|
||||
assert.deepEqual(FeedPages.buildPageQuery('-1'), { pageNum: 1, pageSize: 20 });
|
||||
assert.deepEqual(FeedPages.buildPageQuery('1.5'), { pageNum: 1, pageSize: 20 });
|
||||
});
|
||||
|
||||
test('动态列表、详情和编辑深链始终同时携带真实家谱和动态 ID', () => {
|
||||
assert.equal(
|
||||
FeedPages.buildFeedUrl('detail', '2061000000000000000', '2060000000000000000'),
|
||||
'profile-feed-detail.html?genealogyId=2061000000000000000&feedId=2060000000000000000'
|
||||
);
|
||||
assert.equal(
|
||||
FeedPages.buildFeedUrl('edit', '2061000000000000000', '2060000000000000000'),
|
||||
'profile-feed-edit.html?genealogyId=2061000000000000000&feedId=2060000000000000000'
|
||||
);
|
||||
assert.equal(
|
||||
FeedPages.buildFeedUrl('list', '2061000000000000000'),
|
||||
'profile-feed.html?genealogyId=2061000000000000000'
|
||||
);
|
||||
});
|
||||
|
||||
test('正常动态保存后进入详情,停用动态保存后返回可读取的列表', () => {
|
||||
assert.equal(
|
||||
FeedPages.buildPostSaveUrl('2061000000000000000', '2060000000000000000', '0'),
|
||||
'profile-feed-detail.html?genealogyId=2061000000000000000&feedId=2060000000000000000'
|
||||
);
|
||||
assert.equal(
|
||||
FeedPages.buildPostSaveUrl('2061000000000000000', '2060000000000000000', '1'),
|
||||
'profile-feed.html?genealogyId=2061000000000000000'
|
||||
);
|
||||
});
|
||||
|
||||
test('动态表单省略未填写的可选字段并拒绝契约外状态', () => {
|
||||
assert.deepEqual(FeedPages.buildFeedBody({
|
||||
feedContent: '仅发布文字'
|
||||
}), {
|
||||
feedType: 'text',
|
||||
feedContent: '仅发布文字'
|
||||
});
|
||||
assert.throws(
|
||||
() => FeedPages.buildFeedBody({ feedContent: '错误状态', status: 'draft' }),
|
||||
/状态只能是 0 或 1/
|
||||
);
|
||||
});
|
||||
|
||||
test('同一动态动作在请求完成前只执行一次', async () => {
|
||||
let resolveAction;
|
||||
let calls = 0;
|
||||
const action = new Promise((resolve) => {
|
||||
resolveAction = resolve;
|
||||
});
|
||||
|
||||
const first = FeedPages.withActionLock('like:2060', async () => {
|
||||
calls += 1;
|
||||
await action;
|
||||
return 'ok';
|
||||
});
|
||||
const second = FeedPages.withActionLock('like:2060', async () => {
|
||||
calls += 1;
|
||||
});
|
||||
|
||||
assert.equal(first, second);
|
||||
assert.equal(calls, 1);
|
||||
resolveAction();
|
||||
assert.equal(await first, 'ok');
|
||||
});
|
||||
|
||||
test('家谱能力和当前用户决定编辑、管理与本人删除上下文', () => {
|
||||
assert.deepEqual(
|
||||
FeedPages.normalizeViewerContext(
|
||||
{ canEditContent: true, canManage: false },
|
||||
{ userId: 2062000000000000000n }
|
||||
),
|
||||
{
|
||||
userId: '2062000000000000000',
|
||||
canEditContent: true,
|
||||
canManage: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('家族圈页面提供真实详情、分页和上传控件,不暴露 OSS ID 或草稿伪语义', () => {
|
||||
const listPage = read('profile-feed.html');
|
||||
const editPage = read('profile-feed-edit.html');
|
||||
const detailPage = read('profile-feed-detail.html');
|
||||
const script = read('public/js/feed-pages.js');
|
||||
|
||||
assert.match(listPage, /data-feed-page-prev/);
|
||||
assert.match(listPage, /data-feed-page-next/);
|
||||
assert.match(listPage, /data-feed-create-link[^>]+data-genealogy-context-link|data-genealogy-context-link[^>]+data-feed-create-link/);
|
||||
assert.match(detailPage, /data-feed-detail-page/);
|
||||
assert.match(detailPage, /data-feed-detail/);
|
||||
assert.match(detailPage, /data-feed-detail-comments/);
|
||||
assert.match(detailPage, /data-feed-list-link[^>]+data-genealogy-context-link|data-genealogy-context-link[^>]+data-feed-list-link/);
|
||||
assert.doesNotMatch(editPage, /type="text"[^>]+name="mediaOssIds"|name="mediaOssIds"[^>]+type="text"/);
|
||||
assert.match(editPage, /name="mediaOssIds"[^>]+type="hidden"|type="hidden"[^>]+name="mediaOssIds"/);
|
||||
assert.match(editPage, /multiple/);
|
||||
assert.match(editPage, /data-feed-advanced[^>]+hidden/);
|
||||
assert.match(editPage, /name="status"[^>]+disabled|disabled[^>]+name="status"/);
|
||||
assert.doesNotMatch(editPage, /草稿|图片 OSS ID/);
|
||||
assert.match(script, /feedCommentsPage/);
|
||||
assert.match(script, /feedCommentRepliesPage/);
|
||||
assert.match(script, /currentProfile/);
|
||||
assert.match(script, /genealogyDetail/);
|
||||
assert.match(script, /field\.disabled/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user