Files
jiapu/tests/api-client-contract.test.js
T
fizzleaf ce4f05b60f test(api): 更新API客户端契约测试以符合YAML规范
- 更新登录响应模拟数据以匹配真实的AppLoginVo结构
- 添加认证客户端租户和授权字段验证
- 增加操作码枚举验证测试用例
- 移除对旧token别名的兼容性测试
- 修复测试用例中的短信验证码长度一致性问题
- 更新区域接口路径为PC专用路径
- 调整分片上传接口参数以符合新契约定义

refactor(api): 重构API客户端实现以严格遵循YAML契约

- 添加认证操作码和短信操作码枚举验证
- 实现严格的token响应解析只接受access_token字段
- 使用pickDefined函数过滤请求体中未定义的字段
- 重构认证接口参数映射以符合契约定义
- 更新区域接口路径为PC专用路径/genealogy/pc/region/*
- 优化分片上传接口参数结构与契约保持一致
- 添加操作码枚举验证函数toRequiredOperationCode
- 实现请求体字段选择性提取功能

feat(auth): 优化认证页面的验证码处理流程

- 添加takeCaptchaToken函数用于一次性获取验证码票据
- 更新短信验证码长度验证从4-6位改为精确4位
- 在登录和密码重置流程中集成验证码票据处理
- 修复验证码发送后票据清理逻辑
- 更新HTML模板中的验证码输入字段属性

chore(config): 提取常量配置并扩展配置对象结构

- 将客户端ID、租户ID和令牌键提取为常量
- 扩展配置对象返回客户端配置信息
- 更新配置测试用例以验证新增配置项

docs(planning): 更新PC接口对接规划文档

- 更新契约源说明以反映YAML冻结契约
- 添加YAML与在线Apifox复核对比内容
- 更新阻断项状态表格
- 修订登录响应token字段处理规范
- 更新文件上传和行政区划接口规范说明

style(profile): 优化相册管理页面的文件上传交互

- 将封面和照片OSS ID输入改为隐藏字段
- 添加文件选择标签以改善用户体验
- 移除手动输入OSS ID的选项保持界面简洁
2026-07-28 15:06:34 +08:00

862 lines
35 KiB
JavaScript

const assert = require('node:assert/strict');
const test = require('node:test');
const GenealogyApi = require('../utils/ApiClient.js');
function createClient() {
return GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return null; },
setItem() {},
removeItem() {}
},
axiosInstance: {
request() {
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
}
test('API client exposes latest document-defined PC operations', () => {
const client = createClient();
const allowed = [
'clientId', 'tenantId', 'tokenKey', 'getToken', 'setToken', 'clearToken', 'buildApiUrl',
'register', 'login', 'loginBySms', 'sendSmsCode',
'currentProfile', 'updateProfile', 'changePassword', 'resetPassword', 'changePhone',
'deactivateAccount', 'logout', 'initResumableUpload', 'uploadResumableChunk',
'completeResumableUpload',
'captchaRequirement', 'captchaChallenge', 'captchaVerify', 'captchaChallengeUrl', 'captchaVerifyUrl',
'regionChildren', 'regionPath', 'regionSearch', 'regionDetail',
'genealogyQuota',
'notifications', 'unreadNotificationCount', 'markNotificationRead', 'markAllNotificationsRead',
'feeds', 'feedsPage', 'feedDetail', 'createFeed', 'updateFeed', 'deleteFeed',
'likeFeed', 'unlikeFeed', 'feedComments', 'feedCommentsPage', 'createFeedComment',
'feedCommentReplies', 'feedCommentRepliesPage', 'deleteFeedComment',
'generationPoems', 'generationPoemsManagement', 'createGenerationPoem', 'updateGenerationPoem',
'previewGenerationPoems', 'saveGenerationPoems',
'lineagePersons', 'lineagePersonsPage', 'lineagePersonOptions', 'lineageTree',
'createLineagePerson', 'lineagePersonDetail', 'updateLineagePerson', 'disableLineagePerson',
'createLineageChild', 'createLineageParent', 'createLineageSibling', 'createLineageSpouse',
'growthRecords', 'createGrowthRecord', 'growthRecordDetail', 'updateGrowthRecord', 'deleteGrowthRecord',
'relativeRecords', 'createRelativeRecord', 'relativeRecordDetail', 'updateRelativeRecord', 'deleteRelativeRecord',
'memos', 'createMemo', 'memoDetail', 'updateMemo', 'deleteMemo', 'deleteMeritRecord',
'deleteArticle', 'deleteAlbum', 'deleteAlbumPhoto', 'deleteVideo', 'deleteCeremony', 'deleteCeremonyGift',
'replaceCeremonyInvitees', 'ceremonyInvitations', 'respondCeremonyInvitation', 'myCeremonyInvitations'
].sort();
assert.deepEqual(Object.keys(client).sort(), allowed);
});
test('password login stores access_token from the real AppLoginVo response', async () => {
let stored = '';
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: {
getItem() { return stored; },
setItem(_, value) { stored = value; },
removeItem() { stored = ''; }
},
axiosInstance: {
request(config) {
assert.equal(config.method, 'post');
assert.equal(config.url, '/genealogy/pc/auth/login');
assert.equal(config.headers.Authorization, undefined);
return Promise.resolve({
data: {
code: 200,
msg: '操作成功',
data: {
clientKey: 'web_pc',
deviceType: 'pc',
userType: 'app_user',
profile: {
userId: '2062179707935264769',
tenantId: '000000',
userNo: 'U2062179707910225920',
phone: '19181970173',
nickName: '叶子',
realName: '',
avatar: null,
sex: '2',
birthday: null,
email: '',
registerSource: 'h5',
loginIp: '112.45.165.24',
loginDate: '2026-07-28 11:06:09',
status: '0',
clientKey: 'web_pc',
deviceType: 'pc'
},
access_token: 'access-token',
expire_in: 604800,
client_id: 'ced7e5f0498645c6ec642dcf450b036f'
}
}
});
}
}
});
await client.login({ phone: '13800000000', password: 'hash' });
assert.equal(client.getToken(), 'access-token');
});
test('authentication client owns tenant and grant fields and drops fields outside YAML DTOs', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tenantId: 'tenant-from-config',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
if (config.url.endsWith('/login')) {
return Promise.resolve({ data: { code: 200, data: { access_token: 'login-token' } } });
}
return Promise.resolve({ data: { code: 200, data: {} } });
}
}
});
await client.register({
grantType: 'legacy',
tenantId: 'caller-tenant',
phone: '13800000000',
password: 'password-md5',
smsCode: '1234',
nickName: '小李',
registerSource: 'APP',
clientId: 'legacy-client',
sceneCode: 'legacy-scene',
validToken: 'legacy-ticket'
});
await client.login({
grantType: 'legacy',
tenantId: 'caller-tenant',
phone: '13800000000',
password: 'password-md5',
validToken: 'captcha-ticket',
clientId: 'legacy-client',
sceneCode: 'legacy-scene'
});
assert.deepEqual(calls[0].data, {
grantType: 'password',
tenantId: 'tenant-from-config',
phone: '13800000000',
password: 'password-md5',
nickName: '小李',
registerSource: 'PC',
smsCode: '1234'
});
assert.deepEqual(calls[1].data, {
grantType: 'password',
tenantId: 'tenant-from-config',
phone: '13800000000',
password: 'password-md5',
validToken: 'captcha-ticket'
});
});
test('authentication operationCode rejects values outside the YAML enum', async () => {
const client = createClient();
await assert.rejects(
async () => client.captchaRequirement('legacy-login', { subject: '13800000000' }),
/不支持的PC 认证动作/
);
await assert.rejects(
async () => client.sendSmsCode('password-login', { phone: '13800000000' }),
/不支持的PC 短信认证动作/
);
});
test('login rejects legacy token aliases outside AppLoginVo', async () => {
for (const legacyField of ['token', 'accessToken', 'tokenValue']) {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request() {
return Promise.resolve({ data: { code: 200, data: { [legacyField]: 'legacy-token' } } });
}
}
});
await assert.rejects(
client.login({ phone: '13800000000', password: 'password-md5' }),
/登录响应缺少 token/
);
}
});
test('sending an SMS code uses the PC operation path and documented request body', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.url, '/genealogy/pc/auth/sms/sms-login/code');
assert.deepEqual(config.data, {
grantType: 'sms',
tenantId: '000000',
phone: '13800000000',
validToken: 'captcha-ticket'
});
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.sendSmsCode('sms-login', {
phone: '13800000000',
validToken: 'captcha-ticket'
});
});
test('PC verification operations keep operationCode in the path and clientid out of query and body', async () => {
const calls = [];
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
calls.push(config);
return Promise.resolve({ data: { code: 200, data: { required: false } } });
}
}
});
const challenge = { tenantId: '000000', subject: '13800000000' };
const verification = {
tenantId: '000000',
subject: '13800000000',
challengeId: 'challenge-1',
providerCode: 'tianai',
captchaType: 'SLIDER',
payload: { track: { left: 120, top: 0, trackList: [] } }
};
await client.captchaRequirement('sms-login', { subject: '13800000000' });
await client.captchaChallenge('sms-login', challenge);
await client.captchaVerify('sms-login', verification);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
['get', '/genealogy/pc/auth/verification/sms-login/require', {
tenantId: '000000',
subject: '13800000000'
}, undefined],
['post', '/genealogy/pc/auth/verification/sms-login/challenge', undefined, challenge],
['post', '/genealogy/pc/auth/verification/sms-login/verify', undefined, verification]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal(config.headers.Authorization, undefined);
});
assert.equal(
client.captchaChallengeUrl('sms-login'),
'https://api.example.test/genealogy/pc/auth/verification/sms-login/challenge'
);
assert.equal(
client.captchaVerifyUrl('sms-login'),
'https://api.example.test/genealogy/pc/auth/verification/sms-login/verify'
);
});
test('password reset adds the documented grant and tenant fields without body clientId', 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',
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: '1234',
nickName: '小李',
password: 'password-md5'
});
await client.loginBySms({ phone: '13800000000', smsCode: '1234' });
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['post', '/genealogy/pc/auth/register', {
grantType: 'password',
registerSource: 'PC',
tenantId: '000000',
phone: '13800000000',
smsCode: '1234',
nickName: '小李',
password: 'password-md5'
}],
['post', '/genealogy/pc/auth/login/sms', {
grantType: 'sms',
tenantId: '000000',
phone: '13800000000',
smsCode: '1234'
}]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal('clientId' in config.data, false);
});
});
test('phone change and account deactivation retain PC bodies without clientId', 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'
}],
['post', '/genealogy/pc/auth/account/deactivate', {
smsCode: '654321'
}]
]);
calls.forEach((config) => {
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
assert.equal('clientId' in config.data, false);
});
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/pc/region/children', { parentCode: '0' }, 'Bearer access-token'],
['get', '/genealogy/pc/region/path/11', undefined, 'Bearer access-token'],
['get', '/genealogy/pc/region/search', { keyword: '北京', limit: 20 }, 'Bearer access-token'],
['get', '/genealogy/pc/region/110101', undefined, 'Bearer access-token']
]);
});
test('current PC file methods use the three documented resumable paths and request forms', 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({
uploadId: 'UPLOAD202607240001',
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2,
chunkSize: 4194304,
contentType: 'video/mp4'
});
await client.uploadResumableChunk({
uploadId: 'UPLOAD202607240001',
chunkIndex: 0,
chunkMd5: 'chunk-md5',
file: chunk
});
await client.completeResumableUpload({
uploadId: 'UPLOAD202607240001',
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2
});
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']
]);
assert.deepEqual(calls[0].data, {
uploadId: 'UPLOAD202607240001',
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2,
chunkSize: 4194304,
contentType: 'video/mp4'
});
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',
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2
});
});
test('genealogy quota uses the current PC path without manufacturing a genealogy ID', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.method, 'get');
assert.equal(config.url, '/genealogy/pc/genealogies/quota');
assert.equal(config.params, undefined);
assert.equal(config.data, undefined);
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: { canCreate: true, canJoin: true } } });
}
}
});
await client.genealogyQuota();
});
test('notification methods use the current PC list, unread and read paths', 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.notifications({ readStatus: '0' });
await client.unreadNotificationCount();
await client.markNotificationRead('2060000000000000001');
await client.markAllNotificationsRead();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
['get', '/genealogy/pc/notifications', { readStatus: '0' }, undefined],
['get', '/genealogy/pc/notifications/unread-count', undefined, undefined],
['post', '/genealogy/pc/notifications/2060000000000000001/read', undefined, undefined],
['post', '/genealogy/pc/notifications/read-all', undefined, undefined]
]);
calls.forEach((config) => {
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
});
});
test('family feed methods use the documented 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: {} } });
}
}
});
await client.createFeed(900001001, {
feedType: 'text',
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
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]), [
['post', '/genealogy/pc/genealogies/900001001/feeds', undefined, {
feedType: 'text',
feedContent: '今天上传一张老照片。',
mediaOssIds: '101,102',
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']
]);
});
test('growth record methods use the Apifox PC paths and GrowthRecordBody', 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 genealogyId = '2060000000000000001';
const recordId = '2060000000000000002';
const record = {
lineagePersonId: '2060000000000000003',
recordType: 'birth',
recordTitle: '出生记录',
recordContent: '平安出生',
recordDate: '2026-07-24',
remindTime: '2026-07-24T09:00:00+08:00',
mediaOssIds: '101,102',
sortOrder: 1,
status: 'enabled'
};
await client.growthRecords(genealogyId);
await client.createGrowthRecord(genealogyId, record);
await client.growthRecordDetail(genealogyId, recordId);
await client.updateGrowthRecord(genealogyId, recordId, record);
await client.deleteGrowthRecord(genealogyId, recordId);
assert.deepEqual(calls.map((config) => [
config.method,
config.url,
config.params,
config.data,
config.headers.Authorization,
config.headers.clientid
]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/growth-records', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['post', '/genealogy/pc/genealogies/2060000000000000001/growth-records', undefined, record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['get', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['put', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/growth-records/2060000000000000002', undefined, undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
test('relative record methods use the Apifox PC paths and RelativeRecordBody', 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 genealogyId = '2060000000000000001';
const relativeId = '2060000000000000002';
const record = { relativeName: '王叔', relationName: '长辈', eventName: '寿宴', eventTime: '2026-07-24T09:00:00+08:00', giftAmount: 500, recordContent: '出席', mediaOssIds: '101', sortOrder: 1, status: 'enabled' };
await client.relativeRecords(genealogyId);
await client.createRelativeRecord(genealogyId, record);
await client.relativeRecordDetail(genealogyId, relativeId);
await client.updateRelativeRecord(genealogyId, relativeId, record);
await client.deleteRelativeRecord(genealogyId, relativeId);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/relative-records', undefined],
['post', '/genealogy/pc/genealogies/2060000000000000001/relative-records', record],
['get', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', record],
['delete', '/genealogy/pc/genealogies/2060000000000000001/relative-records/2060000000000000002', undefined]
]);
});
test('memo methods use the Apifox PC paths and documented request fields', 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 genealogyId = '2060000000000000001';
const memoId = '2060000000000000002';
const memo = {
memoTitle: '祭祖提醒',
memoContent: '准备供品',
remindTime: '2026-07-24T09:00:00+08:00',
completed: '0',
mediaOssIds: '101,102',
sortOrder: 1,
status: 'enabled'
};
await client.memos(genealogyId);
await client.createMemo(genealogyId, memo);
await client.memoDetail(genealogyId, memoId);
await client.updateMemo(genealogyId, memoId, memo);
await client.deleteMemo(genealogyId, memoId);
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['get', '/genealogy/pc/genealogies/2060000000000000001/memos', undefined],
['post', '/genealogy/pc/genealogies/2060000000000000001/memos', memo],
['get', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', memo],
['delete', '/genealogy/pc/genealogies/2060000000000000001/memos/2060000000000000002', undefined]
]);
});
test('merit record deletion uses the only documented PC merit operation', async () => {
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
axiosInstance: {
request(config) {
assert.equal(config.method, 'delete');
assert.equal(config.url, '/genealogy/pc/genealogies/2060000000000000001/merit-records/2060000000000000002');
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
return Promise.resolve({ data: { code: 200, data: null } });
}
}
});
await client.deleteMeritRecord('2060000000000000001', '2060000000000000002');
});
test('remaining document-defined deletion operations use PC paths only', 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: null } }); } }
});
const genealogyId = '2060000000000000001';
await client.deleteArticle(genealogyId, '2060000000000000002');
await client.deleteAlbum(genealogyId, '2060000000000000003');
await client.deleteAlbumPhoto(genealogyId, '2060000000000000003', '2060000000000000004');
await client.deleteVideo(genealogyId, '2060000000000000005');
await client.deleteCeremony(genealogyId, '2060000000000000006');
await client.deleteCeremonyGift(genealogyId, '2060000000000000006', '2060000000000000007');
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data, config.headers.clientid]), [
['delete', '/genealogy/pc/genealogies/2060000000000000001/articles/2060000000000000002', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000003', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000003/photos/2060000000000000004', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/videos/2060000000000000005', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000006', undefined, 'ced7e5f0498645c6ec642dcf450b036f'],
['delete', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000006/gifts/2060000000000000007', undefined, 'ced7e5f0498645c6ec642dcf450b036f']
]);
});
test('ceremony invitation methods use the current PC invitation contracts', 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 genealogyId = '2060000000000000001';
const ceremonyId = '2060000000000000002';
await client.replaceCeremonyInvitees(genealogyId, ceremonyId, { inviteeUserIds: ['900000001', '900000002'] });
await client.ceremonyInvitations(genealogyId, ceremonyId);
await client.respondCeremonyInvitation(genealogyId, ceremonyId, { inviteStatus: 'ACCEPTED' });
await client.myCeremonyInvitations();
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['put', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitees', {
inviteeUserIds: ['900000001', '900000002']
}],
['get', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitations', undefined],
['put', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/invitations/me', {
inviteStatus: 'ACCEPTED'
}],
['get', '/genealogy/pc/genealogies/ceremony-invitations/mine', undefined]
]);
calls.forEach((config) => {
assert.equal(config.headers.Authorization, 'Bearer access-token');
assert.equal(config.headers.clientid, 'ced7e5f0498645c6ec642dcf450b036f');
});
});