feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
+176
-27
@@ -1,51 +1,200 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const GrowthPages = require('../public/js/growth-pages.js');
|
||||
|
||||
test('成长记录页只从 URL 读取真实家谱编号', () => {
|
||||
assert.equal(GrowthPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
|
||||
test('成长记录页只从 URL 读取安全的真实家谱和记录编号', () => {
|
||||
assert.equal(
|
||||
GrowthPages.getCurrentGenealogyId('?genealogyId=2060000000000000001&recordId=2060000000000000002'),
|
||||
'2060000000000000001'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.getCurrentRecordId('?genealogyId=2060000000000000001&recordId=2060000000000000002'),
|
||||
'2060000000000000002'
|
||||
);
|
||||
assert.equal(GrowthPages.getCurrentGenealogyId(''), '');
|
||||
assert.equal(GrowthPages.getCurrentRecordId('?recordId=unsafe'), '');
|
||||
});
|
||||
|
||||
test('成长记录写入只构造 Apifox GrowthRecordBody 字段', () => {
|
||||
test('成长记录写入只构造 YAML GrowthRecordBody 字段并转换后端日期格式', () => {
|
||||
assert.deepEqual(
|
||||
GrowthPages.buildGrowthRecordBody({
|
||||
lineagePersonId: '2060000000000000001',
|
||||
recordType: 'birth',
|
||||
recordTitle: ' 出生记录 ',
|
||||
recordContent: ' 平安出生 ',
|
||||
lineagePersonId: '2060000000000000003',
|
||||
recordType: ' 入学 ',
|
||||
recordTitle: ' 入学记录 ',
|
||||
recordContent: ' 顺利入学 ',
|
||||
recordDate: '2026-07-24',
|
||||
remindTime: '2026-07-24T09:00:00+08:00',
|
||||
mediaOssIds: '101,102',
|
||||
remindTime: '2026-07-24T09:30',
|
||||
mediaOssIds: '2060000000000000004,2060000000000000005',
|
||||
sortOrder: '3',
|
||||
status: 'enabled',
|
||||
content: '旧字段',
|
||||
personId: '旧字段'
|
||||
status: '0',
|
||||
recordId: 'should-not-send',
|
||||
appUserId: 'should-not-send'
|
||||
}),
|
||||
{
|
||||
lineagePersonId: '2060000000000000001',
|
||||
recordType: 'birth',
|
||||
recordTitle: '出生记录',
|
||||
recordContent: '平安出生',
|
||||
lineagePersonId: '2060000000000000003',
|
||||
recordType: '入学',
|
||||
recordTitle: '入学记录',
|
||||
recordContent: '顺利入学',
|
||||
recordDate: '2026-07-24',
|
||||
remindTime: '2026-07-24T09:00:00+08:00',
|
||||
mediaOssIds: '101,102',
|
||||
remindTime: '2026-07-24 09:30:00',
|
||||
mediaOssIds: '2060000000000000004,2060000000000000005',
|
||||
sortOrder: 3,
|
||||
status: 'enabled'
|
||||
status: '0'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('成长记录校验标题、整数 ID、附件 OSS ID 和排序值', () => {
|
||||
test('成长记录校验标题、选择器 ID、附件、排序和可重读状态', () => {
|
||||
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '' }), '请填写记录标题');
|
||||
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', lineagePersonId: 'A-1' }), '世系人物 ID 必须是整数');
|
||||
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101, 102' }), '附件 OSS ID 请使用英文逗号分隔的正整数');
|
||||
assert.equal(GrowthPages.validateGrowthRecordBody(GrowthPages.buildGrowthRecordBody({ recordTitle: '记录', sortOrder: '1.5' })), '排序值必须是安全整数');
|
||||
assert.equal(GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101,102', sortOrder: 1 }), '');
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', lineagePersonId: 'A-1' }),
|
||||
'请选择有效的世系人物'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101, 102' }),
|
||||
'附件上传结果无效'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody(GrowthPages.buildGrowthRecordBody({ recordTitle: '记录', sortOrder: '1.5' })),
|
||||
'排序值必须是安全整数'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', recordDate: '2026/07/24' }),
|
||||
'记录日期格式无效'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', remindTime: '2026-07-24T09:30:00+08:00' }),
|
||||
'提醒时间格式无效'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', status: '1' }),
|
||||
'当前 PC 无法重新读取停用记录,暂不开放停用'
|
||||
);
|
||||
assert.equal(
|
||||
GrowthPages.validateGrowthRecordBody({ recordTitle: '记录', mediaOssIds: '101,102', sortOrder: 1, status: '0' }),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
test('成长记录列表不猜测未展开的响应 DTO', () => {
|
||||
assert.deepEqual(GrowthPages.normalizeGrowthList([{ property1: 'value' }]), [{ property1: 'value' }]);
|
||||
assert.deepEqual(GrowthPages.normalizeGrowthList({ rows: [] }), []);
|
||||
test('成长记录写后重读必须返回同一条稳定记录', () => {
|
||||
const detail = {
|
||||
recordId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
recordTitle: '入学记录',
|
||||
status: '0'
|
||||
};
|
||||
|
||||
assert.equal(GrowthPages.matchesSavedRecord(detail, '2060000000000000001'), true);
|
||||
assert.equal(GrowthPages.matchesSavedRecord(detail, '2060000000000000009'), false);
|
||||
assert.equal(GrowthPages.matchesSavedRecord({}, '2060000000000000001'), false);
|
||||
});
|
||||
|
||||
test('成长记录响应使用 PC GrowthRecordVo 并拒绝不安全长 ID', () => {
|
||||
assert.deepEqual(GrowthPages.normalizeGrowthRecord({
|
||||
recordId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
appUserId: '2060000000000000003',
|
||||
appUserNickName: '叶子',
|
||||
appUserPhone: '19100000000',
|
||||
lineagePersonId: '2060000000000000004',
|
||||
lineagePersonNo: 'P001',
|
||||
lineagePersonName: '叶小明',
|
||||
recordType: '入学',
|
||||
recordTitle: '小学入学',
|
||||
recordContent: '<p>第一天上学</p>',
|
||||
recordDate: '2026-09-01 00:00:00',
|
||||
remindTime: '2027-09-01 08:30:00',
|
||||
mediaOssIds: '2060000000000000005,2060000000000000006',
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: '成长节点'
|
||||
}), {
|
||||
recordId: '2060000000000000001',
|
||||
genealogyId: '2060000000000000002',
|
||||
genealogyNo: 'G20260729001',
|
||||
genealogyName: '叶氏家谱',
|
||||
surname: '叶',
|
||||
appUserId: '2060000000000000003',
|
||||
appUserNickName: '叶子',
|
||||
appUserPhone: '19100000000',
|
||||
lineagePersonId: '2060000000000000004',
|
||||
lineagePersonNo: 'P001',
|
||||
lineagePersonName: '叶小明',
|
||||
recordType: '入学',
|
||||
recordTitle: '小学入学',
|
||||
recordContent: '<p>第一天上学</p>',
|
||||
recordDate: '2026-09-01 00:00:00',
|
||||
remindTime: '2027-09-01 08:30:00',
|
||||
mediaOssIds: '2060000000000000005,2060000000000000006',
|
||||
sortOrder: 1,
|
||||
status: '0',
|
||||
remark: '成长节点'
|
||||
});
|
||||
assert.equal(GrowthPages.normalizeGrowthRecord({
|
||||
recordId: Number('2060000000000000001'),
|
||||
genealogyId: '2',
|
||||
recordTitle: '标题',
|
||||
status: '0'
|
||||
}), null);
|
||||
});
|
||||
|
||||
test('成长记录渲染业务字段但不暴露手机号、OSS ID 或未转义正文', () => {
|
||||
const record = GrowthPages.normalizeGrowthRecord({
|
||||
recordId: '1',
|
||||
genealogyId: '2',
|
||||
appUserId: '3',
|
||||
appUserNickName: '叶子',
|
||||
appUserPhone: '19100000000',
|
||||
lineagePersonId: '4',
|
||||
lineagePersonName: '叶小明',
|
||||
recordType: '入学',
|
||||
recordTitle: '<script>alert(1)</script>',
|
||||
recordContent: '<img src=x onerror=alert(1)>第一天上学',
|
||||
recordDate: '2026-09-01 00:00:00',
|
||||
mediaOssIds: '5,6',
|
||||
sortOrder: 1,
|
||||
status: '0'
|
||||
});
|
||||
const html = GrowthPages.renderGrowthDetail(record);
|
||||
|
||||
assert.doesNotMatch(html, /<script>|<img/);
|
||||
assert.match(html, /第一天上学/);
|
||||
assert.match(html, /叶小明/);
|
||||
assert.match(html, /2 个附件/);
|
||||
assert.doesNotMatch(html, /19100000000|>5<|>6</);
|
||||
});
|
||||
|
||||
test('世系人物选择器只使用稳定人物编号和名称', () => {
|
||||
assert.equal(
|
||||
GrowthPages.renderLineageOptions([
|
||||
{ personId: '2060000000000000001', name: '叶小明', generationName: '承' },
|
||||
{ personId: Number('2060000000000000002'), name: '不安全编号' }
|
||||
], '2060000000000000001'),
|
||||
'<option value="">不关联世系人物</option><option value="2060000000000000001" selected>叶小明 · 承</option>'
|
||||
);
|
||||
});
|
||||
|
||||
test('成长记录列表和编辑页开放 PC CRUD 且不允许手填人物或 OSS ID', () => {
|
||||
const root = path.join(__dirname, '..');
|
||||
const listPage = fs.readFileSync(path.join(root, 'profile-growth.html'), 'utf8');
|
||||
const editPage = fs.readFileSync(path.join(root, 'profile-growth-edit.html'), 'utf8');
|
||||
const styles = fs.readFileSync(path.join(root, 'public', 'css', 'profile-module.css'), 'utf8');
|
||||
|
||||
assert.match(listPage, /data-growth-list/);
|
||||
assert.match(listPage, /data-growth-detail/);
|
||||
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
|
||||
assert.match(editPage, /name="lineagePersonId"[^>]*data-growth-lineage-person/);
|
||||
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
||||
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
|
||||
assert.doesNotMatch(editPage, /name="(?:lineagePersonId|mediaOssIds)"[^>]*type="text"/);
|
||||
assert.match(editPage, /public\/js\/md5\.js/);
|
||||
assert.match(editPage, /public\/js\/upload-pages\.js/);
|
||||
assert.match(editPage, /public\/js\/growth-pages\.js/);
|
||||
assert.match(styles, /\[data-growth-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user