1497 lines
59 KiB
JavaScript
1497 lines
59 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',
|
|
'genealogiesMine', 'genealogyOptions', 'genealogyDetail', 'genealogyOverview', 'genealogyQuota',
|
|
'createGenealogy', 'applyToGenealogy', 'myGenealogyJoinApplies',
|
|
'pendingGenealogyJoinApplies', 'auditGenealogyJoinApply', 'cancelGenealogyJoinApply',
|
|
'submitFeedback', 'myFeedback',
|
|
'helpArticles', 'helpArticleDetail', 'siteArticles',
|
|
'promotions',
|
|
'vipPackages', 'createVipOrder', 'vipOrders',
|
|
'notifications', 'notificationDetail', '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',
|
|
'meritRecords', 'createMeritRecord', 'meritRecordDetail', 'updateMeritRecord', 'deleteMeritRecord',
|
|
'articles', 'articleDetail', 'createArticle', 'updateArticle', 'deleteArticle',
|
|
'albums', 'createAlbum', 'updateAlbum', 'albumPhotos', 'createAlbumPhoto', 'deleteAlbum', 'deleteAlbumPhoto',
|
|
'videos', 'createVideo', 'videoDetail', 'updateVideo', 'deleteVideo',
|
|
'ceremonies', 'ceremonyDetail', 'createCeremony', 'updateCeremony',
|
|
'ceremonyGifts', 'createCeremonyGift', 'deleteCeremony', 'deleteCeremonyGift',
|
|
'replaceCeremonyInvitees', 'ceremonyInvitations', 'respondCeremonyInvitation', 'myCeremonyInvitations',
|
|
'genealogyMembers', 'genealogyMemberOptions', 'updateGenealogyMember',
|
|
'removeGenealogyMember', 'leaveGenealogy', 'transferGenealogyOwner',
|
|
'businessDictionary', 'saveGenealogyOrder', 'publicGenealogies', 'genealogyCompleteness',
|
|
'archiveGenealogy', 'restoreGenealogy', 'updateGenealogy', 'genealogyProfileReminders',
|
|
'issueGenealogyInvitation', 'previewGenealogyInvitation', 'redeemGenealogyInvitation',
|
|
'myGenealogyInvitations', 'revokeGenealogyInvitation', 'genealogyDeletionCapability',
|
|
'sendGenealogyDeletionCode', 'permanentlyDeleteGenealogy',
|
|
'vipCapability', 'vipPaymentStatus', 'closeVipPayment',
|
|
'earningSummary', 'earningLedger', 'earningWithdrawals',
|
|
'createEarningWithdrawal', 'cancelEarningWithdrawal',
|
|
'personDocuments', 'personDocument', 'createPersonDocument', 'updatePersonDocument',
|
|
'deletePersonDocument', 'addPersonDocumentResource', 'deletePersonDocumentResource',
|
|
'personDocumentResourceAccess', 'protectPersonDocument', 'unlockPersonDocument',
|
|
'unprotectPersonDocument'
|
|
].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: '9007199254740993001',
|
|
tenantId: '000000',
|
|
userNo: 'U9007199254740993001',
|
|
phone: '13800000000',
|
|
nickName: '测试用户',
|
|
realName: '',
|
|
avatar: null,
|
|
sex: '2',
|
|
birthday: null,
|
|
email: '',
|
|
registerSource: 'h5',
|
|
loginIp: '127.0.0.1',
|
|
loginDate: '2026-01-01 00:00:00',
|
|
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('help article methods use authenticated PC paths and whitelist the optional category', 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.helpArticles({ helpCategory: ' account ', keyword: 'must-drop' });
|
|
await client.helpArticleDetail('2062179707935264769');
|
|
|
|
assert.deepEqual(calls.map((config) => [
|
|
config.method,
|
|
config.url,
|
|
config.params,
|
|
config.headers.Authorization
|
|
]), [
|
|
['get', '/genealogy/pc/help-articles', { helpCategory: ' account ' }, 'Bearer access-token'],
|
|
['get', '/genealogy/pc/help-articles/2062179707935264769', undefined, 'Bearer access-token']
|
|
]);
|
|
});
|
|
|
|
test('promotion method uses the authenticated PC path and whitelists platform query', 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.promotions({
|
|
platform: 'pc',
|
|
keyword: 'must-drop'
|
|
});
|
|
|
|
assert.deepEqual(calls.map((config) => [
|
|
config.method,
|
|
config.url,
|
|
config.params,
|
|
config.headers.Authorization
|
|
]), [
|
|
['get', '/genealogy/pc/promotions', { platform: 'pc' }, 'Bearer access-token']
|
|
]);
|
|
assert.throws(
|
|
() => client.promotions({ platform: 'desktop' }),
|
|
/不支持的推广平台/
|
|
);
|
|
});
|
|
|
|
test('site article method uses the public PC path and validates SQL query values', 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.siteArticles({
|
|
articleType: 'notice',
|
|
limit: 100,
|
|
keyword: 'must-drop'
|
|
});
|
|
await client.siteArticles({ limit: 100 });
|
|
|
|
assert.deepEqual(calls.map((config) => [
|
|
config.method,
|
|
config.url,
|
|
config.params,
|
|
config.headers.Authorization
|
|
]), [
|
|
['get', '/genealogy/pc/site/articles', { articleType: 'notice', limit: 100 }, undefined],
|
|
['get', '/genealogy/pc/site/articles', { limit: 100 }, undefined]
|
|
]);
|
|
assert.throws(
|
|
() => client.siteArticles({ articleType: 'culture', limit: 100 }),
|
|
/不支持的资讯类型/
|
|
);
|
|
[0, -1, 1.5, 101, '100'].forEach((limit) => {
|
|
assert.throws(
|
|
() => client.siteArticles({ limit }),
|
|
/资讯数量限制/
|
|
);
|
|
});
|
|
});
|
|
|
|
test('genealogy context methods obtain IDs from the real PC list and detail 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.genealogiesMine();
|
|
await client.genealogyOptions({ keyword: '汤氏' });
|
|
await client.genealogyDetail('9007199254740993002');
|
|
await client.genealogyOverview('9007199254740993002');
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params]), [
|
|
['get', '/genealogy/pc/genealogies/mine', undefined],
|
|
['get', '/genealogy/pc/genealogies/options', { keyword: '汤氏' }],
|
|
['get', '/genealogy/pc/genealogies/9007199254740993002', undefined],
|
|
['get', '/genealogy/pc/genealogies/9007199254740993002/overview', undefined]
|
|
]);
|
|
});
|
|
|
|
test('genealogy lifecycle methods use PC paths and strict 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 genealogyId = '2062179707935264769';
|
|
const applyId = '2062179707935264770';
|
|
|
|
await client.createGenealogy({
|
|
genealogyName: '汤氏家谱',
|
|
surname: '汤',
|
|
regionCode: '511622',
|
|
ancestralHall: '汤氏宗祠',
|
|
originPlace: '四川',
|
|
addressDetail: '沿口镇',
|
|
coverOssId: '2062179707935264771',
|
|
intro: '家谱简介',
|
|
visibility: '1',
|
|
joinMode: '1',
|
|
legacyField: 'must-drop'
|
|
});
|
|
await client.applyToGenealogy(genealogyId, {
|
|
applicantName: '申请人',
|
|
phone: '19100000000',
|
|
relationDesc: '族亲',
|
|
applyReason: '申请加入',
|
|
inviterUserId: 'must-drop'
|
|
});
|
|
await client.myGenealogyJoinApplies();
|
|
await client.pendingGenealogyJoinApplies(genealogyId);
|
|
await client.auditGenealogyJoinApply(genealogyId, applyId, {
|
|
status: '1',
|
|
auditRemark: '资料一致',
|
|
legacyField: 'must-drop'
|
|
});
|
|
await client.cancelGenealogyJoinApply(applyId);
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['post', '/genealogy/pc/genealogies', {
|
|
genealogyName: '汤氏家谱',
|
|
surname: '汤',
|
|
regionCode: '511622',
|
|
ancestralHall: '汤氏宗祠',
|
|
originPlace: '四川',
|
|
addressDetail: '沿口镇',
|
|
coverOssId: '2062179707935264771',
|
|
intro: '家谱简介',
|
|
visibility: '1',
|
|
joinMode: '1'
|
|
}],
|
|
['post', '/genealogy/pc/genealogies/2062179707935264769/join-applies', {
|
|
applicantName: '申请人',
|
|
phone: '19100000000',
|
|
relationDesc: '族亲',
|
|
applyReason: '申请加入'
|
|
}],
|
|
['get', '/genealogy/pc/genealogies/join-applies/mine', undefined],
|
|
['get', '/genealogy/pc/genealogies/2062179707935264769/join-applies/pending', undefined],
|
|
['put', '/genealogy/pc/genealogies/2062179707935264769/join-applies/2062179707935264770/audit', {
|
|
status: '1',
|
|
auditRemark: '资料一致'
|
|
}],
|
|
['delete', '/genealogy/pc/genealogies/join-applies/2062179707935264770', undefined]
|
|
]);
|
|
assert.throws(
|
|
() => client.pendingGenealogyJoinApplies(Number.MAX_SAFE_INTEGER + 1),
|
|
/家谱编号/
|
|
);
|
|
assert.throws(
|
|
() => client.cancelGenealogyJoinApply(Number.MAX_SAFE_INTEGER + 1),
|
|
/申请编号/
|
|
);
|
|
});
|
|
|
|
test('feedback methods use the PC collection path and AppFeedbackBody whitelist', 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.submitFeedback({
|
|
feedbackType: 'bug',
|
|
feedbackContent: '页面按钮无响应',
|
|
contactInfo: '测试联系方式',
|
|
feedbackTitle: 'must-drop',
|
|
appUserId: 'must-drop'
|
|
});
|
|
await client.myFeedback();
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['post', '/genealogy/pc/feedback', {
|
|
feedbackType: 'bug',
|
|
feedbackContent: '页面按钮无响应',
|
|
contactInfo: '测试联系方式'
|
|
}],
|
|
['get', '/genealogy/pc/feedback', undefined]
|
|
]);
|
|
});
|
|
|
|
test('VIP methods use PC native-payment paths with PcVipOrderBody whitelist', 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.vipPackages();
|
|
await client.vipCapability();
|
|
await client.createVipOrder({
|
|
packageId: '2062179707935264769',
|
|
genealogyId: '2062179707935264770',
|
|
payType: 'wechat',
|
|
appUserId: 'must-drop',
|
|
payStatus: 'must-drop'
|
|
});
|
|
await client.vipOrders();
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['get', '/genealogy/pc/vip/packages', undefined],
|
|
['get', '/genealogy/pc/vip/capability', undefined],
|
|
['post', '/genealogy/pc/vip/orders', {
|
|
packageId: '2062179707935264769',
|
|
genealogyId: '2062179707935264770'
|
|
}],
|
|
['get', '/genealogy/pc/vip/orders', undefined]
|
|
]);
|
|
});
|
|
|
|
test('PC closure methods use invitation, reminder, lifecycle, document and earning 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 = '2062179707935264701';
|
|
|
|
await client.genealogyProfileReminders(genealogyId);
|
|
await client.issueGenealogyInvitation(genealogyId, { lineagePersonId: '2062179707935264702', ownerUserId: 'drop' });
|
|
await client.genealogyDeletionCapability(genealogyId);
|
|
await client.permanentlyDeleteGenealogy(genealogyId, { genealogyName: '叶氏家谱', smsCode: '123456', drop: true });
|
|
await client.personDocuments(genealogyId, { lineagePersonId: '2062179707935264702', pageNum: 2 });
|
|
await client.createPersonDocument(genealogyId, {
|
|
lineagePersonId: '2062179707935264702', documentType: 'id_card', documentTitle: '身份证', drop: true
|
|
});
|
|
await client.createEarningWithdrawal({
|
|
requestId: 'pc-request-1', amount: '88.50', payoutQrOssId: '2062179707935264703',
|
|
payoutAccountName: '叶一', accountNo: 'drop'
|
|
});
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
|
|
['get', `/genealogy/pc/genealogies/${genealogyId}/profile-reminders`, undefined, undefined],
|
|
['post', `/genealogy/pc/genealogies/${genealogyId}/invitations`, undefined, { lineagePersonId: '2062179707935264702' }],
|
|
['get', `/genealogy/pc/genealogies/${genealogyId}/permanent-deletion/capability`, undefined, undefined],
|
|
['post', `/genealogy/pc/genealogies/${genealogyId}/permanent-deletion`, undefined, { genealogyName: '叶氏家谱', smsCode: '123456' }],
|
|
['get', `/genealogy/pc/genealogies/${genealogyId}/person-documents`, { lineagePersonId: '2062179707935264702' }, undefined],
|
|
['post', `/genealogy/pc/genealogies/${genealogyId}/person-documents`, undefined, {
|
|
lineagePersonId: '2062179707935264702', documentType: 'id_card', documentTitle: '身份证'
|
|
}],
|
|
['post', '/genealogy/pc/earnings/withdrawals', undefined, {
|
|
requestId: 'pc-request-1', amount: '88.50', payoutQrOssId: '2062179707935264703', payoutAccountName: '叶一'
|
|
}]
|
|
]);
|
|
});
|
|
|
|
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', pageNum: 99 });
|
|
await client.notificationDetail('2060000000000000001');
|
|
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/2060000000000000001', undefined, 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('notification methods reject unsafe numeric path IDs before sending a request', async () => {
|
|
let requestCount = 0;
|
|
const client = GenealogyApi.createClient({
|
|
baseUrl: 'https://api.example.test',
|
|
tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} },
|
|
axiosInstance: {
|
|
request() {
|
|
requestCount += 1;
|
|
return Promise.resolve({ data: { code: 200, data: null } });
|
|
}
|
|
}
|
|
});
|
|
|
|
assert.throws(() => client.notificationDetail(Number.MAX_SAFE_INTEGER + 1));
|
|
assert.throws(() => client.markNotificationRead(Number.MAX_SAFE_INTEGER + 1));
|
|
assert.equal(requestCount, 0);
|
|
});
|
|
|
|
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 methods use the complete PC CRUD paths and MeritRecordBody', 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 meritId = '2060000000000000002';
|
|
const record = {
|
|
donorName: '叶明',
|
|
meritType: 'repair',
|
|
meritTitle: '修缮宗祠',
|
|
meritContent: '参与宗祠修缮',
|
|
amount: 500.5,
|
|
meritTime: '2026-07-26 10:00:00',
|
|
sortOrder: 1,
|
|
status: '0'
|
|
};
|
|
const recordWithExtras = Object.assign({}, record, {
|
|
appUserId: 'must-not-send',
|
|
mediaOssIds: 'must-not-send'
|
|
});
|
|
|
|
await client.meritRecords(genealogyId);
|
|
await client.createMeritRecord(genealogyId, recordWithExtras);
|
|
await client.meritRecordDetail(genealogyId, meritId);
|
|
await client.updateMeritRecord(genealogyId, meritId, recordWithExtras);
|
|
await client.deleteMeritRecord(genealogyId, meritId);
|
|
|
|
assert.deepEqual(calls.map((config) => [
|
|
config.method,
|
|
config.url,
|
|
config.data,
|
|
config.headers.Authorization,
|
|
config.headers.clientid
|
|
]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/merit-records', undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/merit-records', record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/merit-records/2060000000000000002', undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/merit-records/2060000000000000002', record, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f'],
|
|
['delete', '/genealogy/pc/genealogies/2060000000000000001/merit-records/2060000000000000002', undefined, 'Bearer access-token', 'ced7e5f0498645c6ec642dcf450b036f']
|
|
]);
|
|
});
|
|
|
|
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('article methods use the complete PC CRUD paths and ArticleBody whitelist', 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 articleId = '2060000000000000002';
|
|
const input = {
|
|
categoryId: '2060000000000000003',
|
|
articleTitle: '家族源流',
|
|
articleSummary: '摘要',
|
|
coverOssId: '2060000000000000004',
|
|
articleContent: '<p>正文</p>',
|
|
authorName: '宗亲',
|
|
sortOrder: 1,
|
|
status: '0',
|
|
appUserId: 'must-drop'
|
|
};
|
|
const body = {
|
|
categoryId: '2060000000000000003',
|
|
articleTitle: '家族源流',
|
|
articleSummary: '摘要',
|
|
coverOssId: '2060000000000000004',
|
|
articleContent: '<p>正文</p>',
|
|
authorName: '宗亲',
|
|
sortOrder: 1,
|
|
status: '0'
|
|
};
|
|
|
|
await client.articles(genealogyId);
|
|
await client.createArticle(genealogyId, input);
|
|
await client.articleDetail(genealogyId, articleId);
|
|
await client.updateArticle(genealogyId, articleId, input);
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/articles', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/articles', body],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/articles/2060000000000000002', undefined],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/articles/2060000000000000002', body]
|
|
]);
|
|
});
|
|
|
|
test('album methods use the complete PC album and photo 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 albumId = '2060000000000000002';
|
|
const albumInput = {
|
|
albumName: '祠堂旧影',
|
|
albumDesc: '历史照片',
|
|
coverOssId: '2060000000000000003',
|
|
sortOrder: 2,
|
|
status: '0',
|
|
photoCount: 99
|
|
};
|
|
const albumBody = {
|
|
albumName: '祠堂旧影',
|
|
albumDesc: '历史照片',
|
|
coverOssId: '2060000000000000003',
|
|
sortOrder: 2,
|
|
status: '0'
|
|
};
|
|
const photoInput = {
|
|
ossId: '2060000000000000004',
|
|
photoTitle: '合影',
|
|
photoDesc: '祭祖合影',
|
|
photographer: '宗亲',
|
|
shootTime: '2026-07-29 10:30:00',
|
|
sortOrder: 3,
|
|
status: '0',
|
|
albumId: 'must-drop'
|
|
};
|
|
const photoBody = {
|
|
ossId: '2060000000000000004',
|
|
photoTitle: '合影',
|
|
photoDesc: '祭祖合影',
|
|
photographer: '宗亲',
|
|
shootTime: '2026-07-29 10:30:00',
|
|
sortOrder: 3,
|
|
status: '0'
|
|
};
|
|
|
|
await client.albums(genealogyId);
|
|
await client.createAlbum(genealogyId, albumInput);
|
|
await client.updateAlbum(genealogyId, albumId, albumInput);
|
|
await client.albumPhotos(genealogyId, albumId);
|
|
await client.createAlbumPhoto(genealogyId, albumId, photoInput);
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/albums', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/albums', albumBody],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000002', albumBody],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000002/photos', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/albums/2060000000000000002/photos', photoBody]
|
|
]);
|
|
});
|
|
|
|
test('ceremony admin methods use PC activity and gift 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';
|
|
const ceremonyInput = {
|
|
ceremonyType: 'ancestor',
|
|
ceremonyTitle: '清明祭祖',
|
|
ceremonyDesc: '宗亲祭祖',
|
|
ceremonyTime: '2026-04-05 09:00:00',
|
|
location: '祠堂',
|
|
locationAddress: '宗祠路 1 号',
|
|
longitude: 104.1,
|
|
latitude: 30.6,
|
|
coverOssId: '2060000000000000003',
|
|
sortOrder: 1,
|
|
status: '0',
|
|
giftCount: 10
|
|
};
|
|
const ceremonyBody = {
|
|
ceremonyType: 'ancestor',
|
|
ceremonyTitle: '清明祭祖',
|
|
ceremonyDesc: '宗亲祭祖',
|
|
ceremonyTime: '2026-04-05 09:00:00',
|
|
location: '祠堂',
|
|
locationAddress: '宗祠路 1 号',
|
|
longitude: 104.1,
|
|
latitude: 30.6,
|
|
coverOssId: '2060000000000000003',
|
|
sortOrder: 1,
|
|
status: '0'
|
|
};
|
|
const giftInput = {
|
|
giverName: '宗亲',
|
|
giftAmount: 88.5,
|
|
giftMessage: '敬献',
|
|
status: 'must-drop'
|
|
};
|
|
const giftBody = { giverName: '宗亲', giftAmount: 88.5, giftMessage: '敬献' };
|
|
|
|
await client.ceremonies(genealogyId);
|
|
await client.createCeremony(genealogyId, ceremonyInput);
|
|
await client.ceremonyDetail(genealogyId, ceremonyId);
|
|
await client.updateCeremony(genealogyId, ceremonyId, ceremonyInput);
|
|
await client.ceremonyGifts(genealogyId, ceremonyId);
|
|
await client.createCeremonyGift(genealogyId, ceremonyId, giftInput);
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/ceremonies', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/ceremonies', ceremonyBody],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002', undefined],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002', ceremonyBody],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/gifts', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/ceremonies/2060000000000000002/gifts', giftBody]
|
|
]);
|
|
});
|
|
|
|
test('genealogy member methods use PC member paths and body whitelists', 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 memberId = '2060000000000000002';
|
|
const memberInput = {
|
|
memberName: '族员甲',
|
|
relationName: '侄',
|
|
roleType: 'editor',
|
|
lineagePersonId: '2060000000000000003',
|
|
appUserId: 'must-drop'
|
|
};
|
|
const memberBody = {
|
|
memberName: '族员甲',
|
|
relationName: '侄',
|
|
roleType: 'editor',
|
|
lineagePersonId: '2060000000000000003'
|
|
};
|
|
|
|
await client.genealogyMembers(genealogyId);
|
|
await client.genealogyMemberOptions(genealogyId);
|
|
await client.updateGenealogyMember(genealogyId, memberId, memberInput);
|
|
await client.removeGenealogyMember(genealogyId, memberId);
|
|
await client.leaveGenealogy(genealogyId);
|
|
await client.transferGenealogyOwner(genealogyId, { targetMemberId: memberId, roleType: 'must-drop' });
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.params, config.data]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/members', undefined, undefined],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/members/options', undefined, undefined],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/members/2060000000000000002', undefined, memberBody],
|
|
['delete', '/genealogy/pc/genealogies/2060000000000000001/members/2060000000000000002', undefined, undefined],
|
|
['delete', '/genealogy/pc/genealogies/2060000000000000001/members/me', undefined, undefined],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/members/owner-transfer', undefined, {
|
|
targetMemberId: memberId
|
|
}]
|
|
]);
|
|
});
|
|
|
|
test('video methods use the complete PC CRUD paths and VideoBody', 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 videoId = '2060000000000000002';
|
|
const body = {
|
|
videoTitle: '家族活动记录',
|
|
videoDesc: '清明祭祖活动视频',
|
|
coverOssId: '2060000000000000003',
|
|
videoOssId: '2060000000000000004',
|
|
durationSeconds: 180,
|
|
sortOrder: 1,
|
|
status: '0'
|
|
};
|
|
|
|
await client.videos(genealogyId);
|
|
await client.createVideo(genealogyId, body);
|
|
await client.videoDetail(genealogyId, videoId);
|
|
await client.updateVideo(genealogyId, videoId, body);
|
|
|
|
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/videos', undefined],
|
|
['post', '/genealogy/pc/genealogies/2060000000000000001/videos', body],
|
|
['get', '/genealogy/pc/genealogies/2060000000000000001/videos/2060000000000000002', undefined],
|
|
['put', '/genealogy/pc/genealogies/2060000000000000001/videos/2060000000000000002', body]
|
|
]);
|
|
});
|
|
|
|
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, ['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');
|
|
});
|
|
});
|