test(api): 更新API客户端契约测试以符合YAML规范

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

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

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

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

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

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

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

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

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

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

- 将封面和照片OSS ID输入改为隐藏字段
- 添加文件选择标签以改善用户体验
- 移除手动输入OSS ID的选项保持界面简洁
This commit is contained in:
fizzleaf
2026-07-28 15:06:34 +08:00
parent 735a06e330
commit ce4f05b60f
26 changed files with 886 additions and 196 deletions
+148 -26
View File
@@ -49,7 +49,7 @@ test('API client exposes latest document-defined PC operations', () => {
assert.deepEqual(Object.keys(client).sort(), allowed);
});
test('password login stores only the returned token', async () => {
test('password login stores access_token from the real AppLoginVo response', async () => {
let stored = '';
const client = GenealogyApi.createClient({
baseUrl: 'https://api.example.test',
@@ -63,7 +63,38 @@ test('password login stores only the returned token', async () => {
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' } } });
return Promise.resolve({
data: {
code: 200,
msg: '操作成功',
data: {
clientKey: 'web_pc',
deviceType: 'pc',
userType: 'app_user',
profile: {
userId: '2062179707935264769',
tenantId: '000000',
userNo: 'U2062179707910225920',
phone: '19181970173',
nickName: '叶子',
realName: '',
avatar: null,
sex: '2',
birthday: null,
email: '',
registerSource: 'h5',
loginIp: '112.45.165.24',
loginDate: '2026-07-28 11:06:09',
status: '0',
clientKey: 'web_pc',
deviceType: 'pc'
},
access_token: 'access-token',
expire_in: 604800,
client_id: 'ced7e5f0498645c6ec642dcf450b036f'
}
}
});
}
}
});
@@ -73,6 +104,95 @@ test('password login stores only the returned token', async () => {
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',
@@ -192,11 +312,11 @@ test('registration and SMS login use the PC auth endpoints with SMS fields', asy
await client.register({
phone: '13800000000',
smsCode: '123456',
smsCode: '1234',
nickName: '小李',
password: 'password-md5'
});
await client.loginBySms({ phone: '13800000000', smsCode: '123456' });
await client.loginBySms({ phone: '13800000000', smsCode: '1234' });
assert.deepEqual(calls.map((config) => [config.method, config.url, config.data]), [
['post', '/genealogy/pc/auth/register', {
@@ -204,7 +324,7 @@ test('registration and SMS login use the PC auth endpoints with SMS fields', asy
registerSource: 'PC',
tenantId: '000000',
phone: '13800000000',
smsCode: '123456',
smsCode: '1234',
nickName: '小李',
password: 'password-md5'
}],
@@ -212,7 +332,7 @@ test('registration and SMS login use the PC auth endpoints with SMS fields', asy
grantType: 'sms',
tenantId: '000000',
phone: '13800000000',
smsCode: '123456'
smsCode: '1234'
}]
]);
calls.forEach((config) => {
@@ -282,10 +402,10 @@ test('region methods use PC paths and retain the logged-in authorization header'
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']
['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']
]);
});
@@ -304,14 +424,13 @@ test('current PC file methods use the three documented resumable paths and reque
const chunk = new Blob(['part'], { type: 'application/octet-stream' });
await client.initResumableUpload({
uploadId: 'UPLOAD202607240001',
fileName: 'video.mp4',
fileSize: 8388608,
fileMd5: 'file-md5',
chunkSize: 4194304,
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2,
contentType: 'video/mp4',
bizType: 'video',
usageScene: 'family_video'
chunkSize: 4194304,
contentType: 'video/mp4'
});
await client.uploadResumableChunk({
uploadId: 'UPLOAD202607240001',
@@ -321,8 +440,10 @@ test('current PC file methods use the three documented resumable paths and reque
});
await client.completeResumableUpload({
uploadId: 'UPLOAD202607240001',
fileMd5: 'file-md5',
fileSize: 8388608
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2
});
assert.deepEqual(calls.map((config) => [
config.method,
@@ -335,14 +456,13 @@ test('current PC file methods use the three documented resumable paths and reque
['post', '/genealogy/pc/files/resumable/complete', undefined, 'Bearer access-token']
]);
assert.deepEqual(calls[0].data, {
uploadId: 'UPLOAD202607240001',
fileName: 'video.mp4',
fileSize: 8388608,
fileMd5: 'file-md5',
chunkSize: 4194304,
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2,
contentType: 'video/mp4',
bizType: 'video',
usageScene: 'family_video'
chunkSize: 4194304,
contentType: 'video/mp4'
});
assert.equal(calls[1].data.get('uploadId'), 'UPLOAD202607240001');
assert.equal(calls[1].data.get('chunkIndex'), '0');
@@ -350,8 +470,10 @@ test('current PC file methods use the three documented resumable paths and reque
assert.equal(calls[1].data.get('file').name, 'blob');
assert.deepEqual(calls[2].data, {
uploadId: 'UPLOAD202607240001',
fileMd5: 'file-md5',
fileSize: 8388608
fileName: 'video.mp4',
fileMd5: '0123456789abcdef0123456789abcdef',
totalSize: 8388608,
totalChunks: 2
});
});