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', 'uploadFile', 'initResumableUpload', 'uploadResumableChunk', 'completeResumableUpload', 'createFileReference', 'deleteFileReference', 'captchaRequirement', 'captchaChallenge', 'captchaVerify', 'regionChildren', 'regionPath', 'regionSearch', 'regionDetail', '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' ].sort(); assert.deepEqual(Object.keys(client).sort(), allowed); }); test('password login stores only the returned token', 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, data: { accessToken: 'access-token' } } }); } } }); await client.login({ phone: '13800000000', password: 'hash' }); assert.equal(client.getToken(), 'access-token'); }); test('sending an SMS code uses the latest documented request fields', 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/code'); assert.deepEqual(config.data, { grantType: 'sms', tenantId: '000000', clientId: 'ced7e5f0498645c6ec642dcf450b036f', phone: '13800000000', sceneCode: 'PC_SMS_LOGIN', validToken: 'captcha-ticket' }); return Promise.resolve({ data: { code: 200, data: null } }); } } }); await client.sendSmsCode({ phone: '13800000000', sceneCode: 'PC_SMS_LOGIN', validToken: 'captcha-ticket' }); }); test('password reset adds the documented grant, tenant and client fields', async () => { const client = GenealogyApi.createClient({ baseUrl: 'https://api.example.test', tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} }, axiosInstance: { request(config) { assert.equal(config.method, 'put'); assert.equal(config.url, '/genealogy/pc/auth/password/reset'); assert.equal(config.headers.Authorization, undefined); assert.deepEqual(config.data, { grantType: 'password', tenantId: '000000', clientId: 'ced7e5f0498645c6ec642dcf450b036f', phone: '13800000000', smsCode: '123456', newPassword: 'password-md5' }); return Promise.resolve({ data: { code: 200, data: null } }); } } }); await client.resetPassword({ phone: '13800000000', smsCode: '123456', newPassword: 'password-md5' }); }); test('registration and SMS login use the PC auth endpoints with SMS fields', async () => { const calls = []; const client = GenealogyApi.createClient({ baseUrl: 'https://api.example.test', tokenStore: { getItem() { return null; }, setItem() {}, removeItem() {} }, axiosInstance: { request(config) { calls.push(config); if (config.url.endsWith('/login/sms')) { return Promise.resolve({ data: { code: 200, data: { access_token: 'sms-token' } } }); } return Promise.resolve({ data: { code: 200, data: null } }); } } }); await client.register({ phone: '13800000000', smsCode: '123456', nickName: '小李', password: 'password-md5' }); await client.loginBySms({ phone: '13800000000', smsCode: '123456' }); assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [ ['post', '/genealogy/pc/auth/register', { grantType: 'password', registerSource: 'PC', tenantId: '000000', clientId: 'ced7e5f0498645c6ec642dcf450b036f', phone: '13800000000', smsCode: '123456', nickName: '小李', password: 'password-md5' }], ['post', '/genealogy/pc/auth/login/sms', { grantType: 'sms', tenantId: '000000', clientId: 'ced7e5f0498645c6ec642dcf450b036f', phone: '13800000000', smsCode: '123456' }] ]); }); test('phone change and account deactivation retain their PC request shapes', async () => { let stored = 'access-token'; const calls = []; const client = GenealogyApi.createClient({ baseUrl: 'https://api.example.test', tokenStore: { getItem() { return stored; }, setItem(_, value) { stored = value; }, removeItem() { stored = ''; } }, axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: null } }); } } }); await client.changePhone({ phone: '13900000000', smsCode: '123456' }); await client.deactivateAccount({ smsCode: '654321' }); assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [ ['put', '/genealogy/pc/auth/phone', { phone: '13900000000', smsCode: '123456', clientId: 'ced7e5f0498645c6ec642dcf450b036f' }], ['post', '/genealogy/pc/auth/account/deactivate', { smsCode: '654321', clientId: 'ced7e5f0498645c6ec642dcf450b036f' }] ]); assert.equal(stored, ''); }); test('region methods use PC paths and retain the logged-in authorization header', async () => { const calls = []; const client = GenealogyApi.createClient({ baseUrl: 'https://api.example.test', tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} }, axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: [] } }); } } }); await client.regionChildren('0'); await client.regionPath('11'); await client.regionSearch({ keyword: '北京', limit: 20 }); await client.regionDetail('110101'); assert.deepEqual(calls.map((config) => [ config.method, config.url, config.params, config.headers.Authorization ]), [ ['get', '/genealogy/region/children', { parentCode: '0' }, 'Bearer access-token'], ['get', '/genealogy/region/path/11', undefined, 'Bearer access-token'], ['get', '/genealogy/region/search', { keyword: '北京', limit: 20 }, 'Bearer access-token'], ['get', '/genealogy/region/110101', undefined, 'Bearer access-token'] ]); }); test('remaining PC file methods use the documented paths, request forms and login header', async () => { const calls = []; const client = GenealogyApi.createClient({ baseUrl: 'https://api.example.test', tokenStore: { getItem() { return 'access-token'; }, setItem() {}, removeItem() {} }, axiosInstance: { request(config) { calls.push(config); return Promise.resolve({ data: { code: 200, data: {} } }); } } }); const chunk = new Blob(['part'], { type: 'application/octet-stream' }); await client.initResumableUpload({ fileName: 'video.mp4', fileSize: 8388608, fileMd5: 'file-md5', chunkSize: 4194304, totalChunks: 2, contentType: 'video/mp4', bizType: 'video', usageScene: 'family_video' }); await client.uploadResumableChunk({ uploadId: 'UPLOAD202607240001', chunkIndex: 0, chunkMd5: 'chunk-md5', file: chunk }); await client.completeResumableUpload({ uploadId: 'UPLOAD202607240001', fileMd5: 'file-md5', fileSize: 8388608 }); await client.createFileReference({ bizType: 'family_feed', bizTable: 'gen_family_feed', bizId: '900013001', bizField: 'media_oss_ids', ossId: '2060000000000000000' }); await client.deleteFileReference({ bizTable: 'gen_family_feed', bizId: '900013001', bizField: 'media_oss_ids' }); assert.deepEqual(calls.map((config) => [ config.method, config.url, config.params, config.headers.Authorization ]), [ ['post', '/genealogy/pc/files/resumable/init', undefined, 'Bearer access-token'], ['post', '/genealogy/pc/files/resumable/chunk', undefined, 'Bearer access-token'], ['post', '/genealogy/pc/files/resumable/complete', undefined, 'Bearer access-token'], ['post', '/genealogy/pc/files/reference', undefined, 'Bearer access-token'], ['delete', '/genealogy/pc/files/reference', { bizTable: 'gen_family_feed', bizId: '900013001', bizField: 'media_oss_ids' }, 'Bearer access-token'] ]); assert.deepEqual(calls[0].data, { fileName: 'video.mp4', fileSize: 8388608, fileMd5: 'file-md5', chunkSize: 4194304, totalChunks: 2, contentType: 'video/mp4', bizType: 'video', usageScene: 'family_video' }); assert.equal(calls[1].data.get('uploadId'), 'UPLOAD202607240001'); assert.equal(calls[1].data.get('chunkIndex'), '0'); assert.equal(calls[1].data.get('chunkMd5'), 'chunk-md5'); assert.equal(calls[1].data.get('file').name, 'blob'); assert.deepEqual(calls[2].data, { uploadId: 'UPLOAD202607240001', fileMd5: 'file-md5', fileSize: 8388608 }); assert.deepEqual(calls[3].data, { bizType: 'family_feed', bizTable: 'gen_family_feed', bizId: '900013001', bizField: 'media_oss_ids', ossId: '2060000000000000000' }); }); test('family feed methods use the documented paths and request bodies', async () => { const calls = []; const client = GenealogyApi.createClient({ 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'] ]); });