完成10%

This commit is contained in:
rain
2026-07-24 18:11:16 +08:00
parent 8716a68fdb
commit 8870136d1b
33 changed files with 3853 additions and 336 deletions
+358 -4
View File
@@ -25,12 +25,18 @@ test('API client exposes latest document-defined PC operations', () => {
'clientId', 'tenantId', 'tokenKey', 'getToken', 'setToken', 'clearToken', 'buildApiUrl',
'register', 'login', 'loginBySms', 'sendSmsCode',
'currentProfile', 'updateProfile', 'changePassword', 'resetPassword', 'changePhone',
'deactivateAccount', 'logout', 'uploadFile',
'deactivateAccount', 'logout', 'uploadFile', 'initResumableUpload', 'uploadResumableChunk',
'completeResumableUpload', 'createFileReference', 'deleteFileReference',
'captchaRequirement', 'captchaChallenge', 'captchaVerify',
'regionChildren', 'regionPath', 'regionSearch', 'regionDetail',
'feeds', 'feedsPage', 'feedDetail', 'createFeed', 'updateFeed', 'deleteFeed',
'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
'deleteFeedComment'
'feedCommentReplies', 'feedCommentRepliesPage', 'deleteFeedComment',
'generationPoems', 'generationPoemsManagement', 'createGenerationPoem', 'updateGenerationPoem',
'previewGenerationPoems', 'saveGenerationPoems',
'lineagePersons', 'lineagePersonsPage', 'lineagePersonOptions', 'lineageTree',
'createLineagePerson', 'lineagePersonDetail', 'updateLineagePerson', 'disableLineagePerson',
'createLineageChild', 'createLineageParent', 'createLineageSibling', 'createLineageSpouse'
].sort();
assert.deepEqual(Object.keys(client).sort(), allowed);
@@ -72,7 +78,7 @@ test('sending an SMS code uses the latest documented request fields', async () =
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
sceneCode: 'WEB_H5_LOGIN',
sceneCode: 'PC_SMS_LOGIN',
validToken: 'captcha-ticket'
});
return Promise.resolve({ data: { code: 200, data: null } });
@@ -82,11 +88,243 @@ test('sending an SMS code uses the latest documented request fields', async () =
await client.sendSmsCode({
phone: '13800000000',
sceneCode: 'WEB_H5_LOGIN',
sceneCode: 'PC_SMS_LOGIN',
validToken: 'captcha-ticket'
});
});
test('password reset adds the documented grant, tenant and client fields', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.method, 'put');
assert.equal(config.url, '/genealogy/pc/auth/password/reset');
assert.equal(config.headers.Authorization, undefined);
assert.deepEqual(config.data, {
grantType: 'password',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456',
newPassword: 'password-md5'
});
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.resetPassword({
phone: '13800000000',
smsCode: '123456',
newPassword: 'password-md5'
});
});
test('registration and SMS login use the PC auth endpoints with SMS fields', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
if (config.url.endsWith('/login/sms')) {
return Promise.resolve({ data: { code: 200, data: { access_token: 'sms-token' } } });
}
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.register({
phone: '13800000000',
smsCode: '123456',
nickName: '小李',
password: 'password-md5'
});
await client.loginBySms({ phone: '13800000000', smsCode: '123456' });
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['post', '/genealogy/pc/auth/register', {
grantType: 'password',
registerSource: 'PC',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456',
nickName: '小李',
password: 'password-md5'
}],
['post', '/genealogy/pc/auth/login/sms', {
grantType: 'sms',
tenantId: '000000',
clientId: 'ced7e5f0498645c6ec642dcf450b036f',
phone: '13800000000',
smsCode: '123456'
}]
]);
});
test('phone change and account deactivation retain their PC request shapes', async () => {
let stored = 'access-token';
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return stored; },
setItem(_, value) { stored = value; },
removeItem() { stored = ''; }
},
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.changePhone({ phone: '13900000000', smsCode: '123456' });
await client.deactivateAccount({ smsCode: '654321' });
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['put', '/genealogy/pc/auth/phone', {
phone: '13900000000',
smsCode: '123456',
clientId: 'ced7e5f0498645c6ec642dcf450b036f'
}],
['post', '/genealogy/pc/auth/account/deactivate', {
smsCode: '654321',
clientId: 'ced7e5f0498645c6ec642dcf450b036f'
}]
]);
assert.equal(stored, '');
});
test('region methods use PC paths and retain the logged-in authorization header', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: [] } });
}
}
});
await client.regionChildren('0');
await client.regionPath('11');
await client.regionSearch({ keyword: '北京', limit: 20 });
await client.regionDetail('110101');
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.headers.Authorization
]), [
['get', '/genealogy/region/children', { parentCode: '0' }, 'Bearer access-token'],
['get', '/genealogy/region/path/11', undefined, 'Bearer access-token'],
['get', '/genealogy/region/search', { keyword: '北京', limit: 20 }, 'Bearer access-token'],
['get', '/genealogy/region/110101', undefined, 'Bearer access-token']
]);
});
test('remaining PC file methods use the documented paths, request forms and login header', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
const chunk = new Blob(['part'], { type: 'application/octet-stream' });
await client.initResumableUpload({
fileName: 'video.mp4',
fileSize: 8388608,
fileMd5: 'file-md5',
chunkSize: 4194304,
totalChunks: 2,
contentType: 'video/mp4',
bizType: 'video',
usageScene: 'family_video'
});
await client.uploadResumableChunk({
uploadId: 'UPLOAD202607240001',
chunkIndex: 0,
chunkMd5: 'chunk-md5',
file: chunk
});
await client.completeResumableUpload({
uploadId: 'UPLOAD202607240001',
fileMd5: 'file-md5',
fileSize: 8388608
});
await client.createFileReference({
bizType: 'family_feed',
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids',
ossId: '2060000000000000000'
});
await client.deleteFileReference({
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids'
});
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.headers.Authorization
]), [
['post', '/genealogy/pc/files/resumable/init', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/resumable/chunk', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/resumable/complete', undefined, 'Bearer access-token'],
['post', '/genealogy/pc/files/reference', undefined, 'Bearer access-token'],
['delete', '/genealogy/pc/files/reference', {
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids'
}, 'Bearer access-token']
]);
assert.deepEqual(calls[0].data, {
fileName: 'video.mp4',
fileSize: 8388608,
fileMd5: 'file-md5',
chunkSize: 4194304,
totalChunks: 2,
contentType: 'video/mp4',
bizType: 'video',
usageScene: 'family_video'
});
assert.equal(calls[1].data.get('uploadId'), 'UPLOAD202607240001');
assert.equal(calls[1].data.get('chunkIndex'), '0');
assert.equal(calls[1].data.get('chunkMd5'), 'chunk-md5');
assert.equal(calls[1].data.get('file').name, 'blob');
assert.deepEqual(calls[2].data, {
uploadId: 'UPLOAD202607240001',
fileMd5: 'file-md5',
fileSize: 8388608
});
assert.deepEqual(calls[3].data, {
bizType: 'family_feed',
bizTable: 'gen_family_feed',
bizId: '900013001',
bizField: 'media_oss_ids',
ossId: '2060000000000000000'
});
});
test('family feed methods use the documented paths and request bodies', async () => {
const calls = [];
const client = GenealogyApi.createClient({
@@ -107,6 +345,8 @@ test('family feed methods use the documented paths and request bodies', async ()
status: '0'
});
await client.feedCommentsPage(900001001, 7001, { pageNum: 1, pageSize: 10 });
await client.feedCommentReplies(900001001, 7001, 8001);
await client.feedCommentRepliesPage(900001001, 7001, 8001, { pageNum: 2, pageSize: 10 });
await client.deleteFeedComment(900001001, 7001, 8001);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
@@ -117,6 +357,120 @@ test('family feed methods use the documented paths and request bodies', async ()
status: '0'
}],
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/page', { pageNum: 1, pageSize: 10 }, undefined],
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/8001/replies', undefined, undefined],
['get', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/8001/replies/page', { pageNum: 2, pageSize: 10 }, undefined],
['delete', '/genealogy/pc/genealogies/900001001/feeds/7001/comments/8001', undefined, undefined]
]);
});
test('generation poem methods use the Apifox PC paths and request bodies', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
const poem = {
generationNo: 3,
generationText: '万',
description: '第三世',
sortOrder: 30,
status: '0'
};
const batch = { poemText: '德 承 家 亦', disableMissing: true };
await client.generationPoems(900001001);
await client.generationPoemsManagement(900001001);
await client.createGenerationPoem(900001001, poem);
await client.updateGenerationPoem(900001001, 2060000000000000000n, poem);
await client.previewGenerationPoems(900001001, batch);
await client.saveGenerationPoems(900001001, batch);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/genealogies/900001001/generation-poems', undefined],
['get', '/genealogy/pc/genealogies/900001001/generation-poems/management', undefined],
['post', '/genealogy/pc/genealogies/900001001/generation-poems', poem],
['put', '/genealogy/pc/genealogies/900001001/generation-poems/2060000000000000000', poem],
['post', '/genealogy/pc/genealogies/900001001/generation-poems/batch/preview', batch],
['post', '/genealogy/pc/genealogies/900001001/generation-poems/batch/save', batch]
]);
});
test('lineage person methods use the Apifox PC paths, query and LineagePersonBody', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
const person = {
name: '李明',
sex: '1',
generation: 3,
generationName: '德',
personNo: 'L-003',
aliasName: '明远',
birthDate: '1990-01-01',
deathDate: null,
biography: '人物简介'
};
const genealogyId = '2060000000000000001';
const personId = '2060000000000000002';
await client.lineagePersons(genealogyId);
await client.lineagePersonsPage(genealogyId, {
pageNum: 1,
pageSize: 10,
keyword: '李',
generation: 3,
personStatus: '0'
});
await client.lineagePersonOptions(genealogyId);
await client.lineageTree(genealogyId);
await client.createLineagePerson(genealogyId, person);
await client.lineagePersonDetail(genealogyId, personId);
await client.updateLineagePerson(genealogyId, personId, person);
await client.disableLineagePerson(genealogyId, personId);
await client.createLineageChild(genealogyId, personId, person);
await client.createLineageParent(genealogyId, personId, person);
await client.createLineageSibling(genealogyId, personId, person);
await client.createLineageSpouse(genealogyId, personId, person);
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.data,
config.headers.Authorization,
config.headers.clientid
]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/page', {
pageNum: 1,
pageSize: 10,
keyword: '李',
generation: 3,
personStatus: '0'
}, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/options', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/lineage/tree', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['put', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002/children', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002/parents', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002/siblings', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/lineage/persons/2060000000000000002/spouses', undefined, person, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
+69
View File
@@ -0,0 +1,69 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const AuthPages = require('../public/js/auth-pages.js');
test('registration body carries the SMS code but never sends confirmation or captcha fields', () => {
assert.deepEqual(
AuthPages.buildRegisterBody({
phone: '13800000000',
smsCode: '123456',
nickName: '小李',
password: 'plain-password',
confirmPassword: 'plain-password',
validToken: 'captcha-ticket'
}, (value) => 'md5-' + value),
{
phone: '13800000000',
smsCode: '123456',
nickName: '小李',
password: 'md5-plain-password'
}
);
});
test('password reset body carries only the reset DTO fields', () => {
assert.deepEqual(
AuthPages.buildPasswordResetBody({
phone: '13800000000',
smsCode: '123456',
newPassword: 'plain-password',
validToken: 'captcha-ticket'
}, (value) => 'md5-' + value),
{
phone: '13800000000',
smsCode: '123456',
newPassword: 'md5-plain-password'
}
);
});
test('authentication forms use the PC SMS scenes defined by the PC endpoint', () => {
assert.equal(AuthPages.getCaptchaScene('login'), 'PC_SMS_LOGIN');
assert.equal(AuthPages.getCaptchaScene('sms-login'), 'PC_SMS_LOGIN');
assert.equal(AuthPages.getCaptchaScene('register'), 'PC_REGISTER');
assert.equal(AuthPages.getCaptchaScene('password-reset'), 'PC_FORGOT_PASSWORD');
});
test('registration validates SMS code and password confirmation before submission', () => {
const base = {
phone: '13800000000',
smsCode: '123456',
password: 'password',
confirmPassword: 'password'
};
assert.equal(AuthPages.validateAuthValues('register', base), '');
assert.equal(AuthPages.validateAuthValues('register', { ...base, smsCode: '' }), '请填写短信验证码');
assert.equal(AuthPages.validateAuthValues('register', { ...base, confirmPassword: 'different' }), '两次输入的密码不一致');
});
test('registration page exposes the documented SMS and confirmation inputs', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'register.html'), 'utf8');
assert.match(source, /name="smsCode"/);
assert.match(source, /data-api-send-code/);
assert.match(source, /name="confirmPassword"/);
});
+38
View File
@@ -33,3 +33,41 @@ test('动态列表要求服务端返回 feedId 和 feedContent', () => {
});
assert.equal(FeedPages.normalizeFeed({ feedId: 7001, content: '旧字段' }), null);
});
test('评论请求只提交 Apifox 定义的 commentContent 和父评论 ID', () => {
assert.deepEqual(FeedPages.buildCommentBody({
commentContent: '收到',
parentCommentId: '2060000000000000000',
replyUserId: '2060000000000000001'
}), {
commentContent: '收到',
parentCommentId: '2060000000000000000'
});
});
test('评论响应使用 commentContent,并保留已删除评论的占位状态', () => {
assert.deepEqual(FeedPages.normalizeComment({
commentId: '2060000000000000000',
commentContent: '收到',
appUserNickName: '小李',
replyCount: 2,
userDeleted: '0'
}), {
commentId: '2060000000000000000',
commentContent: '收到',
appUserNickName: '小李',
replyCount: 2,
userDeleted: false
});
assert.deepEqual(FeedPages.normalizeComment({
commentId: '2060000000000000001',
commentContent: null,
userDeleted: '1'
}), {
commentId: '2060000000000000001',
commentContent: '该评论已删除',
appUserNickName: undefined,
replyCount: undefined,
userDeleted: true
});
});
+99
View File
@@ -0,0 +1,99 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const GenerationPages = require('../public/js/generation-pages.js');
test('字辈页只从 URL 读取真实家谱编号', () => {
assert.equal(GenerationPages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(GenerationPages.getCurrentGenealogyId(''), '');
});
test('字辈新增和修改只构造 Apifox 定义的字段', () => {
assert.deepEqual(
GenerationPages.buildGenerationPoemBody({
generationNo: '3',
generationText: ' 万 ',
description: '第三世',
sortOrder: '30',
status: '1',
content: '旧字段',
stopMissingOldGeneration: true
}),
{
generationNo: 3,
generationText: '万',
description: '第三世',
sortOrder: 30,
status: '1'
}
);
});
test('批量字辈请求只提交 poemText 与 disableMissing', () => {
assert.deepEqual(
GenerationPages.buildBatchBody({
poemText: '德 承 家 亦',
disableMissing: true,
content: '旧字段',
stopMissingOldGeneration: false
}),
{
poemText: '德 承 家 亦',
disableMissing: true
}
);
});
test('字辈列表要求管理接口返回稳定的直接字段', () => {
assert.deepEqual(
GenerationPages.normalizeGenerationPoem({
poemId: '2060000000000000000',
generationNo: 3,
generationText: '万',
description: '第三世',
sortOrder: 30,
status: '0'
}),
{
poemId: '2060000000000000000',
generationNo: 3,
generationText: '万',
status: '0',
description: '第三世',
sortOrder: 30
}
);
assert.equal(GenerationPages.normalizeGenerationPoem({ poemId: 1, generationNo: 3, content: '旧字段', status: '0' }), null);
assert.equal(GenerationPages.normalizeGenerationPoem({
poemId: Number('2060000000000000000'),
generationNo: 3,
generationText: '万',
status: '0'
}), null);
});
test('字辈批量输入按 Apifox 限制校验', () => {
assert.equal(GenerationPages.validateGenerationPoemBody({ generationNo: 0, generationText: '万' }), '世代序号必须是 1 到 2147483647 之间的整数');
assert.equal(GenerationPages.validateGenerationPoemBody({ generationNo: 3, generationText: '' }), '请填写字辈文字');
assert.equal(GenerationPages.validateGenerationPoemBody(GenerationPages.buildGenerationPoemBody({ generationNo: 3, generationText: '万', sortOrder: '1.5' })), '排序值必须是整数');
assert.equal(GenerationPages.validateGenerationPoemBody({ generationNo: 3, generationText: '万', sortOrder: 2147483648 }), '排序值必须在 -2147483648 到 2147483647 之间');
assert.equal(GenerationPages.validateBatchBody({ poemText: '', disableMissing: false }), '请填写批量字辈内容');
assert.deepEqual(GenerationPages.splitPoemText('德,承;家、亦/传|芳\n远'), ['德', '承', '家', '亦', '传', '芳', '远']);
assert.equal(GenerationPages.validateBatchBody({ poemText: '甲'.repeat(51), disableMissing: false }), '单个字辈不能超过 50 个字符');
assert.equal(GenerationPages.validateBatchBody({ poemText: Array(502).fill('甲').join(' '), disableMissing: false }), '一次最多导入 500 个世代');
assert.equal(GenerationPages.validateBatchBody({ poemText: '德承家亦', disableMissing: false }), '');
});
test('字辈批量示例明确使用 Apifox 支持的分隔符', () => {
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-generation.html'), 'utf8');
assert.match(page, /placeholder="例如:德 承 家 亦(用空格或标点分隔)"/);
assert.doesNotMatch(page, /placeholder="例如:德承家亦"/);
});
test('字辈管理接口的 403 不是登录失效', () => {
assert.equal(GenerationPages.isForbidden({ status: 403 }), true);
assert.equal(GenerationPages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false);
});
+88
View File
@@ -0,0 +1,88 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const LineagePages = require('../public/js/lineage-pages.js');
test('世系页只从 URL 读取真实家谱编号', () => {
assert.equal(LineagePages.getCurrentGenealogyId('?genealogyId=900001001'), '900001001');
assert.equal(LineagePages.getCurrentGenealogyId(''), '');
});
test('世系人物表单只构造 Apifox LineagePersonBody 字段', () => {
assert.deepEqual(
LineagePages.buildLineagePersonBody({
name: '李明',
sex: '0',
generation: '3',
generationName: '德',
personNo: 'P202607090001',
aliasName: '明远',
birthDate: '1990-01-01',
sortOrder: '1',
relationName: '妻',
biography: '人物简介',
personName: '旧字段',
generationNo: 4,
introduction: '旧字段'
}),
{
name: '李明',
sex: '0',
generation: 3,
generationName: '德',
personNo: 'P202607090001',
aliasName: '明远',
birthDate: '1990-01-01',
sortOrder: 1,
relationName: '妻',
biography: '人物简介'
}
);
});
test('世系人物要求安全的 ID、姓名和有效世代值', () => {
assert.deepEqual(
LineagePages.normalizeLineagePerson({
personId: '2060000000000000000',
name: '李明',
generation: 3,
generationName: '德',
sex: '0'
}),
{
personId: '2060000000000000000',
name: '李明',
sex: '0',
generationName: '德',
generation: 3
}
);
assert.equal(LineagePages.normalizeLineagePerson({ personId: Number('2060000000000000000'), name: '李明' }), null);
assert.equal(LineagePages.validateLineagePersonBody({ name: '' }), '请填写成员姓名');
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ name: '李明', generation: '1.5' })), '世代序号必须是整数');
assert.equal(LineagePages.validateLineagePersonBody(LineagePages.buildLineagePersonBody({ name: '李明', sortOrder: '1.5' })), '排序值必须是整数');
});
test('世系关系模式只使用 PC 已定义的四种新增关系', () => {
assert.equal(LineagePages.relationLabel('parents'), '父母');
assert.equal(LineagePages.relationLabel('spouses'), '配偶');
assert.equal(LineagePages.relationLabel('children'), '子女');
assert.equal(LineagePages.relationLabel('siblings'), '兄弟姐妹');
assert.equal(LineagePages.relationLabel('remove'), '');
});
test('世系页按 Apifox 关键词范围搜索并提供分页入口', () => {
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-tree.html'), 'utf8');
assert.match(page, /placeholder="输入姓名、别名或人物编号搜索"/);
assert.match(page, /data-lineage-pagination/);
assert.match(page, /data-lineage-page-action="previous"/);
assert.match(page, /data-lineage-page-action="next"/);
});
test('世系管理的 403 不会被当作登录失效', () => {
assert.equal(LineagePages.isForbidden({ status: 403 }), true);
assert.equal(LineagePages.shouldRedirectToLogin({ getToken() { return 'token'; } }, { status: 403 }), false);
});
+10 -15
View File
@@ -16,29 +16,22 @@ test('配置始终使用后端提供的 PC 接口地址', () => {
assert.equal(development.getEnvironment(), 'development');
assert.equal(production.getEnvironment(), 'production');
assert.equal(development.getApiBaseUrl(), 'http://182.61.18.23:8080');
assert.equal(production.getApiBaseUrl(), 'http://182.61.18.23:8080');
assert.equal(development.getApiBaseUrl(), 'https://backend-api.ddxcjp.cn/');
assert.equal(production.getApiBaseUrl(), 'https://backend-api.ddxcjp.cn/');
});
test('最新 OpenAPI 文档包含已交付的家族圈接口', () => {
const contract = JSON.parse(read('PC.openapi2.json'));
test('PC 对接规划明确以 Apifox 目录而非导出快照为契约源', () => {
const plan = read('docs/PC接口对接规划.md');
assert.ok(contract.paths['/genealogy/pc/auth/login']);
assert.ok(contract.paths['/genealogy/pc/files/upload']);
assert.ok(contract.paths['/genealogy/region/children']);
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds']);
assert.ok(contract.paths['/genealogy/pc/genealogies/{genealogyId}/feeds/{feedId}/comments/{commentId}']);
});
test('旧 YAML 明确指向最新 JSON 接口源', () => {
assert.match(read('PC.openapi.yaml'), /正式接口源为 PC\.openapi2\.json/);
assert.match(plan, /Apifox 的 PC 目录是 PC 前端接口的唯一正式契约源/);
assert.match(plan, /旧 OpenAPI 文件不再作为新增功能依据/);
});
test('pages do not load scripts for unavailable business APIs', () => {
const removedScripts = [
'album-pages.js', 'article-pages.js', 'ceremony-pages.js',
'feedback-pages.js', 'genealogy-pages.js', 'generation-pages.js', 'growth-pages.js',
'help-pages.js', 'join-apply-pages.js', 'lineage-pages.js', 'member-admin-pages.js',
'feedback-pages.js', 'genealogy-pages.js', 'growth-pages.js',
'help-pages.js', 'join-apply-pages.js', 'member-admin-pages.js',
'memo-pages.js', 'notification-pages.js', 'promotion-pages.js', 'vip-pages.js'
];
const retainedPages = fs.readdirSync(root).filter((entry) => entry.endsWith('.html'));
@@ -53,4 +46,6 @@ test('pages do not load scripts for unavailable business APIs', () => {
['profile-feed.html', 'profile-feed-edit.html'].forEach((page) => {
assert.equal(read(page).includes('src="public/js/feed-pages.js"'), true, `${page} must load feed-pages.js`);
});
assert.equal(read('profile-generation.html').includes('src="public/js/generation-pages.js"'), true, 'profile-generation.html must load generation-pages.js');
assert.equal(read('profile-tree.html').includes('src="public/js/lineage-pages.js"'), true, 'profile-tree.html must load lineage-pages.js');
});
+13 -4
View File
@@ -13,8 +13,6 @@ const familyPendingPages = [
'profile-join-review.html',
'profile-family-admin.html',
'profile-invite.html',
'profile-tree.html',
'profile-generation.html'
];
const contentPendingPages = [
@@ -71,10 +69,21 @@ test('待开发页面显式加载状态脚本', () => {
});
});
test('家族圈动态页面保持真实功能状态', () => {
['profile-feed.html', 'profile-feed-edit.html'].forEach((page) => {
test('已接入的家谱业务页面保持真实功能状态', () => {
['profile-feed.html', 'profile-feed-edit.html', 'profile-generation.html', 'profile-tree.html'].forEach((page) => {
assert.doesNotMatch(read(page), /data-feature-status="pending"/, `${page} 不应标记为待开发`);
});
assert.match(read('profile-generation.html'), /src="public\/js\/generation-pages\.js"/);
assert.match(read('profile-tree.html'), /src="public\/js\/lineage-pages\.js"/);
});
test('待开发的家谱管理页明确放行已接入的字辈入口并透传上下文', () => {
const familyAdmin = read('profile-family-admin.html');
const profileCommon = read('public/js/profile-common.js');
assert.match(familyAdmin, /href="profile-generation\.html" data-feature-link="available" data-genealogy-context-link/);
assert.match(profileCommon, /function syncGenealogyContextLinks\(\)/);
assert.match(profileCommon, /params\.set\('genealogyId', genealogyId\)/);
});
test('待开发页面仅放行显式标记的可用功能入口', () => {
+89
View File
@@ -0,0 +1,89 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const ProfilePages = require('../public/js/profile-pages.js');
const RegionPages = require('../public/js/region-pages.js');
const UploadPages = require('../public/js/upload-pages.js');
test('profile view reads only the documented profile fields', () => {
assert.deepEqual(ProfilePages.buildProfileView({
nickName: '小林',
phone: '13800000000',
birthday: '1990-01-01',
provinceCode: '11',
userName: '不应读取',
mobile: '不应读取'
}), {
displayName: '小林',
avatarText: '小',
phone: '13800000000',
sex: '待填写',
birthday: '1990-01-01',
regionText: '加载中...'
});
assert.deepEqual(ProfilePages.buildProfileView({
userName: '不应读取',
phonenumber: '不应读取',
mobile: '不应读取'
}), {
displayName: '未设置昵称',
avatarText: '家',
phone: '未绑定',
sex: '待填写',
birthday: '待填写',
regionText: '待填写'
});
});
test('profile update preserves int64 OSS IDs as text', () => {
assert.deepEqual(ProfilePages.buildProfileUpdateBody({
nickName: '小林',
avatarOssId: '2060000000000000000',
sex: '0',
birthday: '1990-01-01',
provinceCode: '11',
cityCode: '1101',
districtCode: '110101'
}), {
nickName: '小林',
avatarOssId: '2060000000000000000',
sex: '0',
birthday: '1990-01-01',
provinceCode: '11',
cityCode: '1101',
districtCode: '110101'
});
assert.equal(typeof ProfilePages.buildProfileUpdateBody({ avatarOssId: '2060000000000000000' }).avatarOssId, 'string');
assert.equal(UploadPages.normalizeUploadResult({ ossId: '2060000000000000000' }).ossId, '2060000000000000000');
});
test('region selector accepts only documented region fields and levels', () => {
assert.deepEqual(RegionPages.normalizeList({ data: [{ regionCode: '11' }] }), []);
assert.equal(RegionPages.getRegionCode({ value: '11' }), '');
assert.equal(RegionPages.getRegionName({ label: '北京市' }), '未命名地区');
assert.equal(RegionPages.getRegionLevel({ regionCode: '110101' }), 0);
assert.deepEqual(RegionPages.buildRegionSelection([
{ regionCode: '11', regionName: '北京市', regionLevel: 1 },
{ regionCode: '1101', regionName: '市辖区', regionLevel: 2 },
{ regionCode: '110101', regionName: '东城区', regionLevel: 3 }
]), {
provinceCode: '11',
cityCode: '1101',
districtCode: '110101'
});
});
test('profile data page contains PC profile, upload and region flow markers', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-data.html'), 'utf8');
assert.match(source, /data-profile-form/);
assert.match(source, /name="avatarOssId"/);
assert.match(source, /data-region-profile-form/);
assert.match(source, /data-region-profile-status/);
assert.match(source, /src="public\/js\/upload-pages\.js"/);
assert.match(source, /src="public\/js\/region-pages\.js"/);
});
+27 -5
View File
@@ -1,12 +1,14 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const SecurityPages = require('../public/js/security-pages.js');
const UploadPages = require('../public/js/upload-pages.js');
test('security forms use dedicated captcha scenes and carry the returned ticket', () => {
assert.equal(SecurityPages.getCaptchaScene('password'), 'WEB_H5_CHANGE_PASSWORD');
assert.equal(SecurityPages.getCaptchaScene('phone'), 'WEB_H5_CHANGE_PHONE');
test('security forms use SMS captcha scenes and submit only documented business fields', () => {
assert.equal(SecurityPages.getCaptchaScene('phone'), 'PC_PHONE_CHANGE');
assert.equal(SecurityPages.getCaptchaScene('deactivate'), 'PC_ACCOUNT_DEACTIVATE');
assert.deepEqual(
SecurityPages.buildPasswordChangeBody({
oldPassword: 'old',
@@ -15,10 +17,21 @@ test('security forms use dedicated captcha scenes and carry the returned ticket'
}, (value) => 'md5-' + value),
{
oldPassword: 'md5-old',
newPassword: 'md5-new',
validToken: 'captcha-ticket'
newPassword: 'md5-new'
}
);
assert.deepEqual(
SecurityPages.buildPhoneChangeBody({
phone: '13800000000',
smsCode: '123456',
validToken: 'captcha-ticket'
}),
{
phone: '13800000000',
smsCode: '123456'
}
);
assert.deepEqual(SecurityPages.buildDeactivateBody({ smsCode: '654321' }), { smsCode: '654321' });
});
test('upload helper exposes only single-file upload support', () => {
@@ -26,3 +39,12 @@ test('upload helper exposes only single-file upload support', () => {
assert.equal('buildResumableInitBody' in UploadPages, false);
assert.equal(UploadPages.getUploadMode(5 * 1024 * 1024), 'single');
});
test('security page field names match phone-change and account-deactivation DTOs', () => {
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-security.html'), 'utf8');
assert.match(source, /id="newPhone"\s+name="phone"/);
assert.match(source, /data-security-form="deactivate"/);
assert.match(source, /id="deactivateSmsCode"\s+name="smsCode"/);
assert.match(source, /data-security-send-deactivate-code/);
});