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: '
正文
', authorName: '宗亲', sortOrder: 1, status: '0', appUserId: 'must-drop' }; const body = { categoryId: '2060000000000000003', articleTitle: '家族源流', articleSummary: '摘要', coverOssId: '2060000000000000004', articleContent: '正文
', 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'); }); });