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:
@@ -34,14 +34,151 @@ test('security forms use PC verification operation codes and submit only documen
|
||||
assert.deepEqual(SecurityPages.buildDeactivateBody({ smsCode: '654321' }), { smsCode: '654321' });
|
||||
});
|
||||
|
||||
test('upload helper keeps the avatar flow blocked until the current PC resumable response is defined', async () => {
|
||||
assert.equal(UploadPages.getUploadMode(5 * 1024 * 1024), 'resumable');
|
||||
await assert.rejects(
|
||||
UploadPages.uploadFileForPage({ initResumableUpload() {} }, { name: 'avatar.png' }),
|
||||
/当前 PC 文件上传响应未定义头像回填字段/
|
||||
test('security forms accept only the four-digit SMS code defined by YAML', () => {
|
||||
assert.equal(SecurityPages.isSmsCode('1234'), true);
|
||||
assert.equal(SecurityPages.isSmsCode('123456'), false);
|
||||
});
|
||||
|
||||
test('security captcha ticket is removed from the form when consumed', () => {
|
||||
const field = { value: 'one-time-ticket' };
|
||||
const form = {
|
||||
querySelector(selector) {
|
||||
return selector === 'input[name="validToken"]' ? field : null;
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(SecurityPages.takeCaptchaToken(form), 'one-time-ticket');
|
||||
assert.equal(field.value, '');
|
||||
assert.equal(SecurityPages.takeCaptchaToken(form), '');
|
||||
});
|
||||
|
||||
test('resumable upload returns the instant-upload OSS result without sending chunks', async () => {
|
||||
let chunkCalls = 0;
|
||||
const file = { name: 'avatar.png', size: 8, type: 'image/png', lastModified: 1 };
|
||||
const api = {
|
||||
async initResumableUpload(body) {
|
||||
assert.deepEqual(body, {
|
||||
uploadId: 'upload-fixed',
|
||||
fileName: 'avatar.png',
|
||||
fileMd5: 'file-md5',
|
||||
totalSize: 8,
|
||||
totalChunks: 2,
|
||||
chunkSize: 4,
|
||||
contentType: 'image/png'
|
||||
});
|
||||
return { instant: true, ossId: '2062179707935264769', url: '/avatar.png', fileName: 'avatar.png', uploadedChunks: [] };
|
||||
},
|
||||
async uploadResumableChunk() { chunkCalls += 1; },
|
||||
async completeResumableUpload() { throw new Error('秒传不应调用完成接口'); }
|
||||
};
|
||||
|
||||
const result = await UploadPages.uploadFileForPage(api, file, {
|
||||
uploadId: 'upload-fixed',
|
||||
chunkSize: 4,
|
||||
hashBlob: async () => 'file-md5'
|
||||
});
|
||||
|
||||
assert.equal(result.ossId, '2062179707935264769');
|
||||
assert.equal(chunkCalls, 0);
|
||||
});
|
||||
|
||||
test('resumable upload skips uploaded chunks, retries failures and completes with the init contract', async () => {
|
||||
const uploadedIndexes = [];
|
||||
const progress = [];
|
||||
let secondChunkAttempts = 0;
|
||||
const file = {
|
||||
name: 'video.mp4',
|
||||
size: 10,
|
||||
type: 'video/mp4',
|
||||
lastModified: 2,
|
||||
slice(start, end) {
|
||||
return { size: end - start, start: start };
|
||||
}
|
||||
};
|
||||
const api = {
|
||||
async initResumableUpload() {
|
||||
return { uploadId: 'server-upload', instant: false, uploadedChunks: [0] };
|
||||
},
|
||||
async uploadResumableChunk(body) {
|
||||
uploadedIndexes.push(body.chunkIndex);
|
||||
if (body.chunkIndex === 1 && secondChunkAttempts++ === 0) throw new Error('temporary');
|
||||
},
|
||||
async completeResumableUpload(body) {
|
||||
assert.deepEqual(body, {
|
||||
uploadId: 'server-upload',
|
||||
fileName: 'video.mp4',
|
||||
fileMd5: 'hash-full',
|
||||
totalSize: 10,
|
||||
totalChunks: 3
|
||||
});
|
||||
return { ossId: '2062179707935264770', fileName: 'video.mp4', url: '/video.mp4' };
|
||||
}
|
||||
};
|
||||
|
||||
const result = await UploadPages.uploadFileForPage(api, file, {
|
||||
uploadId: 'client-upload',
|
||||
chunkSize: 4,
|
||||
maxRetries: 1,
|
||||
hashBlob: async (blob) => blob === file ? 'hash-full' : 'hash-' + blob.start,
|
||||
onProgress: (value) => progress.push(value.uploadedChunks)
|
||||
});
|
||||
|
||||
assert.deepEqual(uploadedIndexes, [1, 1, 2]);
|
||||
assert.deepEqual(progress, [1, 2, 3]);
|
||||
assert.equal(result.ossId, '2062179707935264770');
|
||||
});
|
||||
|
||||
test('upload target values preserve string IDs and append multiple attachments', () => {
|
||||
assert.equal(UploadPages.mergeUploadTargetValue('', '2062179707935264769', false), '2062179707935264769');
|
||||
assert.equal(
|
||||
UploadPages.mergeUploadTargetValue('2062179707935264769', '2062179707935264770', true),
|
||||
'2062179707935264769,2062179707935264770'
|
||||
);
|
||||
});
|
||||
|
||||
test('duplicate file submissions share one active upload', async () => {
|
||||
let initCalls = 0;
|
||||
let releaseInit;
|
||||
const pendingInit = new Promise((resolve) => { releaseInit = resolve; });
|
||||
const file = { name: 'same.png', size: 4, type: 'image/png', lastModified: 3 };
|
||||
const api = {
|
||||
async initResumableUpload() {
|
||||
initCalls += 1;
|
||||
await pendingInit;
|
||||
return { instant: true, ossId: '2062179707935264771', uploadedChunks: [] };
|
||||
}
|
||||
};
|
||||
const options = { uploadId: 'same-upload', chunkSize: 4, hashBlob: async () => 'same-hash' };
|
||||
const first = UploadPages.uploadFileForPage(api, file, options);
|
||||
const second = UploadPages.uploadFileForPage(api, file, options);
|
||||
|
||||
assert.strictEqual(first, second);
|
||||
releaseInit();
|
||||
await first;
|
||||
assert.equal(initCalls, 1);
|
||||
});
|
||||
|
||||
test('business upload pages never expose editable OSS ID fields', () => {
|
||||
const pages = [
|
||||
'profile-data.html',
|
||||
'profile-article-edit.html',
|
||||
'profile-album.html',
|
||||
'profile-gift-edit.html',
|
||||
'profile-growth-edit.html',
|
||||
'profile-memo-edit.html',
|
||||
'profile-relative-edit.html'
|
||||
];
|
||||
|
||||
pages.forEach((file) => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
|
||||
|
||||
assert.doesNotMatch(source, /name="(?:avatar|coverOssId|ossId|mediaOssIds)"[^>]*type="text"/);
|
||||
assert.match(source, /data-upload-target=/);
|
||||
assert.match(source, /public\/js\/md5\.js/);
|
||||
assert.match(source, /public\/js\/upload-pages\.js/);
|
||||
});
|
||||
});
|
||||
|
||||
test('security page field names match phone-change and account-deactivation DTOs', () => {
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'profile-security.html'), 'utf8');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user