232 lines
9.4 KiB
JavaScript
232 lines
9.4 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const RelativePages = require('../public/js/relative-pages.js');
|
|
|
|
test('亲友记录页只从 URL 读取安全的真实家谱和记录编号', () => {
|
|
assert.equal(
|
|
RelativePages.getCurrentGenealogyId('?genealogyId=2060000000000000001&relativeId=2060000000000000002'),
|
|
'2060000000000000001'
|
|
);
|
|
assert.equal(
|
|
RelativePages.getCurrentRelativeId('?genealogyId=2060000000000000001&relativeId=2060000000000000002'),
|
|
'2060000000000000002'
|
|
);
|
|
assert.equal(RelativePages.getCurrentGenealogyId(''), '');
|
|
assert.equal(RelativePages.getCurrentRelativeId('?relativeId=unsafe'), '');
|
|
});
|
|
|
|
test('亲友记录写入只构造 YAML RelativeRecordBody 并转换后端时间格式', () => {
|
|
assert.deepEqual(RelativePages.buildRelativeRecordBody({
|
|
relativeName: ' 王叔 ',
|
|
relationName: ' 长辈 ',
|
|
eventName: ' 寿宴 ',
|
|
eventTime: '2026-07-24T09:30',
|
|
giftAmount: '500.5',
|
|
recordContent: ' 出席 ',
|
|
mediaOssIds: '2060000000000000003,2060000000000000004',
|
|
sortOrder: '3',
|
|
status: '0',
|
|
relativeId: 'should-not-send',
|
|
appUserId: 'should-not-send'
|
|
}), {
|
|
relativeName: '王叔',
|
|
relationName: '长辈',
|
|
eventName: '寿宴',
|
|
eventTime: '2026-07-24 09:30:00',
|
|
giftAmount: 500.5,
|
|
recordContent: '出席',
|
|
mediaOssIds: '2060000000000000003,2060000000000000004',
|
|
sortOrder: 3,
|
|
status: '0'
|
|
});
|
|
});
|
|
|
|
test('亲友记录校验姓名、金额、时间、附件、排序和可重读状态', () => {
|
|
assert.equal(RelativePages.validateRelativeRecordBody({ relativeName: '' }), '请填写亲友姓名');
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({ relativeName: '王叔', giftAmount: '一百' })),
|
|
'礼金金额必须是有限数字'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({
|
|
relativeName: '王叔',
|
|
giftAmount: '9007199254740993.01'
|
|
})),
|
|
'礼金金额超出浏览器可安全提交的精度'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({ relativeName: '王叔', giftAmount: '500.50' })),
|
|
''
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody({ relativeName: '王叔', eventTime: '2026-07-24T09:30:00+08:00' }),
|
|
'事件时间格式无效'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody({ relativeName: '王叔', eventTime: '2026-99-24 09:30:00' }),
|
|
'事件时间格式无效'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody({ relativeName: '王叔', mediaOssIds: '101, 102' }),
|
|
'附件上传结果无效'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody(RelativePages.buildRelativeRecordBody({ relativeName: '王叔', sortOrder: '1.5' })),
|
|
'排序值必须是安全整数'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody({ relativeName: '王叔', status: '1' }),
|
|
'当前 PC 无法重新读取停用记录,暂不开放停用'
|
|
);
|
|
assert.equal(
|
|
RelativePages.validateRelativeRecordBody({ relativeName: '王叔', giftAmount: -500.25, status: '0' }),
|
|
''
|
|
);
|
|
});
|
|
|
|
test('亲友记录响应使用 PC RelativeRecordVo 并保留 BigDecimal 字符串', () => {
|
|
assert.deepEqual(RelativePages.normalizeRelativeRecord({
|
|
relativeId: '2060000000000000001',
|
|
genealogyId: '2060000000000000002',
|
|
genealogyNo: 'G20260729001',
|
|
genealogyName: '叶氏家谱',
|
|
surname: '叶',
|
|
appUserId: '2060000000000000003',
|
|
appUserNickName: '叶子',
|
|
appUserPhone: '19100000000',
|
|
relativeName: '王叔',
|
|
relationName: '长辈',
|
|
eventName: '寿宴',
|
|
eventTime: '2026-07-24 09:30:00',
|
|
giftAmount: '500.50',
|
|
recordContent: '出席',
|
|
mediaOssIds: '2060000000000000004',
|
|
sortOrder: 1,
|
|
status: '0',
|
|
remark: '往来记录'
|
|
}), {
|
|
relativeId: '2060000000000000001',
|
|
genealogyId: '2060000000000000002',
|
|
genealogyNo: 'G20260729001',
|
|
genealogyName: '叶氏家谱',
|
|
surname: '叶',
|
|
appUserId: '2060000000000000003',
|
|
appUserNickName: '叶子',
|
|
appUserPhone: '19100000000',
|
|
relativeName: '王叔',
|
|
relationName: '长辈',
|
|
eventName: '寿宴',
|
|
eventTime: '2026-07-24 09:30:00',
|
|
giftAmount: '500.50',
|
|
recordContent: '出席',
|
|
mediaOssIds: '2060000000000000004',
|
|
sortOrder: 1,
|
|
status: '0',
|
|
remark: '往来记录'
|
|
});
|
|
assert.equal(RelativePages.normalizeRelativeRecord({
|
|
relativeId: Number('2060000000000000001'),
|
|
genealogyId: '2',
|
|
relativeName: '王叔',
|
|
status: '0'
|
|
}), null);
|
|
});
|
|
|
|
test('亲友记录渲染业务字段但不暴露手机号、OSS ID 或未转义内容', () => {
|
|
const record = RelativePages.normalizeRelativeRecord({
|
|
relativeId: '1',
|
|
genealogyId: '2',
|
|
appUserId: '3',
|
|
appUserNickName: '叶子',
|
|
appUserPhone: '19100000000',
|
|
relativeName: '<script>alert(1)</script>',
|
|
relationName: '长辈',
|
|
eventName: '寿宴',
|
|
eventTime: '2026-07-24 09:30:00',
|
|
giftAmount: '500.50',
|
|
recordContent: '<img src=x onerror=alert(1)>出席',
|
|
mediaOssIds: '4,5',
|
|
sortOrder: 1,
|
|
status: '0'
|
|
});
|
|
const html = RelativePages.renderRelativeDetail(record);
|
|
|
|
assert.doesNotMatch(html, /<script>|<img/);
|
|
assert.match(html, /出席/);
|
|
assert.match(html, /500\.50/);
|
|
assert.match(html, /2 个附件/);
|
|
assert.doesNotMatch(html, /19100000000|>4<|>5</);
|
|
});
|
|
|
|
test('亲友记录写后重读必须返回同一条稳定记录', () => {
|
|
const detail = {
|
|
relativeId: '2060000000000000001',
|
|
genealogyId: '2060000000000000002',
|
|
relativeName: '王叔',
|
|
status: '0'
|
|
};
|
|
|
|
assert.equal(RelativePages.matchesSavedRecord(detail, '2060000000000000001'), true);
|
|
assert.equal(RelativePages.matchesSavedRecord(detail, '2060000000000000009'), false);
|
|
assert.equal(RelativePages.matchesSavedRecord({}, '2060000000000000001'), false);
|
|
});
|
|
|
|
test('亲友记录编辑器拒绝不可重读的停用记录', () => {
|
|
assert.equal(RelativePages.isEditableRecord({ status: '0' }), true);
|
|
assert.equal(RelativePages.isEditableRecord({ status: '1' }), false);
|
|
assert.equal(RelativePages.isEditableRecord(null), false);
|
|
});
|
|
|
|
test('亲友记录列表和编辑页按家谱能力开放 PC CRUD,且不允许手填 OSS ID 或状态', () => {
|
|
const root = path.join(__dirname, '..');
|
|
const listPage = fs.readFileSync(path.join(root, 'profile-relative.html'), 'utf8');
|
|
const editPage = fs.readFileSync(path.join(root, 'profile-relative-edit.html'), 'utf8');
|
|
const styles = fs.readFileSync(path.join(root, 'public', 'css', 'profile-module.css'), 'utf8');
|
|
|
|
assert.match(listPage, /data-relative-list/);
|
|
assert.match(listPage, /data-relative-detail/);
|
|
assert.match(listPage, /data-relative-create-link hidden/);
|
|
assert.doesNotMatch(listPage, /原始 JSON|不开放编辑和删除/);
|
|
assert.match(editPage, /name="eventTime" type="datetime-local"/);
|
|
assert.match(editPage, /data-relative-form[^>]*novalidate/);
|
|
assert.match(editPage, /for="relativeName">亲友姓名<span class="field-required"/);
|
|
assert.match(editPage, /name="status" type="hidden" value="0"/);
|
|
assert.match(editPage, /name="mediaOssIds" type="hidden"/);
|
|
assert.match(editPage, /data-relative-form-status role="status" aria-live="polite"/);
|
|
assert.match(editPage, /data-relative-editor-placeholder>[\s\S]*api-state--loading" role="status" aria-live="polite" aria-busy="true"/);
|
|
assert.match(editPage, /data-relative-clear-media/);
|
|
assert.doesNotMatch(editPage, /name="(?:mediaOssIds|status)"[^>]*type="text"/);
|
|
assert.match(editPage, /public\/js\/md5\.js/);
|
|
assert.match(editPage, /public\/js\/upload-pages\.js/);
|
|
assert.match(editPage, /public\/js\/relative-pages\.js/);
|
|
assert.match(styles, /\[data-relative-editor\]\[hidden\][^{]*\{[^}]*display:\s*none\s*!important/s);
|
|
});
|
|
|
|
test('亲友记录只向内容维护者或管理员开放写操作', () => {
|
|
const root = path.join(__dirname, '..');
|
|
const script = fs.readFileSync(path.join(root, 'public', 'js', 'relative-pages.js'), 'utf8');
|
|
|
|
assert.equal(RelativePages.canEditRecords({ canEditContent: true }), true);
|
|
assert.equal(RelativePages.canEditRecords({ canManage: true }), true);
|
|
assert.equal(RelativePages.canEditRecords({ canEditContent: false, canManage: false }), false);
|
|
assert.match(script, /await loadCapability\(api, genealogyId\);/);
|
|
assert.match(script, /if \(writePending \|\| !canEditContent \|\| !genealogyId/);
|
|
assert.match(script, /if \(canEditContent\) \{[\s\S]*data-relative-delete-id/);
|
|
});
|
|
|
|
test('亲友记录使用统一的加载、空数据、失败和无权限状态,以及提交中的按钮反馈', () => {
|
|
const root = path.join(__dirname, '..');
|
|
const script = fs.readFileSync(path.join(root, 'public', 'js', 'relative-pages.js'), 'utf8');
|
|
|
|
assert.match(script, /function getApiStateType\(error\)[\s\S]*isForbidden\(error\) \? 'forbidden' : 'error'/);
|
|
assert.match(script, /setState\(query\('\[data-relative-list\]'\), 'loading', '正在加载亲友记录…'\)/);
|
|
assert.match(script, /setState\(query\('\[data-relative-detail\]'\), 'loading', '正在加载亲友记录详情…'\)/);
|
|
assert.match(script, /setFormStatus\('正在读取家谱权限…', 'loading'\)/);
|
|
assert.match(script, /setFormStatus\(validation, 'error'\)/);
|
|
assert.match(script, /setWritePending\(true, '正在保存…'\)/);
|
|
assert.match(script, /setFormStatus\('正在保存亲友记录…', 'loading'\)/);
|
|
});
|