220 lines
8.1 KiB
JavaScript
220 lines
8.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const SecurityPages = require('../public/js/security-pages.js');
|
|
const UploadPages = require('../public/js/upload-pages.js');
|
|
|
|
test('security forms use PC verification operation codes and submit only documented business fields', () => {
|
|
assert.equal(SecurityPages.getCaptchaOperation('phone'), 'phone-change');
|
|
assert.equal(SecurityPages.getCaptchaOperation('deactivate'), 'account-deactivate');
|
|
assert.deepEqual(
|
|
SecurityPages.buildPasswordChangeBody({
|
|
oldPassword: 'old',
|
|
newPassword: 'new',
|
|
validToken: 'captcha-ticket'
|
|
}, (value) => 'md5-' + value),
|
|
{
|
|
oldPassword: 'md5-old',
|
|
newPassword: 'md5-new'
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
SecurityPages.buildPhoneChangeBody({
|
|
phone: '13800000000',
|
|
smsCode: '123456',
|
|
validToken: 'captcha-ticket'
|
|
}),
|
|
{
|
|
phone: '13800000000',
|
|
smsCode: '123456'
|
|
}
|
|
);
|
|
assert.deepEqual(SecurityPages.buildDeactivateBody({ smsCode: '654321' }), { smsCode: '654321' });
|
|
});
|
|
|
|
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-create-family.html',
|
|
'profile-article-edit.html',
|
|
'profile-album-edit.html',
|
|
'profile-album-detail.html',
|
|
'profile-video-edit.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');
|
|
|
|
assert.match(source, /id="newPhone"\s+name="phone"/);
|
|
assert.match(source, /data-security-form="deactivate"/);
|
|
assert.match(source, /id="deactivateSmsCode"\s+name="smsCode"/);
|
|
assert.match(source, /data-security-send-deactivate-code/);
|
|
});
|
|
|
|
test('上传状态复用统一组件并区分无权限', () => {
|
|
const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'upload-pages.js'), 'utf8');
|
|
|
|
assert.match(source, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
|
assert.match(source, /setUploadStatus\(status, 'loading', files\[index\]\.name \+ ' 正在上传…'\)/);
|
|
assert.match(source, /当前账号无权上传文件。/);
|
|
assert.match(source, /status\.setAttribute\('role', 'status'\)/);
|
|
assert.equal(UploadPages.isForbidden({ status: 403 }), true);
|
|
assert.equal(UploadPages.isForbidden({ status: 401 }), false);
|
|
});
|
|
|
|
test('security forms provide visible required labels and unified validation, loading and failure feedback', () => {
|
|
const page = fs.readFileSync(path.join(__dirname, '..', 'profile-security.html'), 'utf8');
|
|
const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'security-pages.js'), 'utf8');
|
|
|
|
assert.match(page, /data-security-form="password" novalidate/);
|
|
assert.match(page, /data-security-form="phone" novalidate/);
|
|
assert.match(page, /data-security-form="deactivate" novalidate/);
|
|
assert.match(page, /field-required/);
|
|
assert.match(page, /data-security-status[^>]*role="status"/);
|
|
assert.match(page, /data-security-bound-phone[^>]*role="status"/);
|
|
assert.doesNotMatch(page, /微信快捷登录|账号安全提醒/);
|
|
assert.match(script, /root\.ProfileUI && root\.ProfileUI\.setApiState/);
|
|
assert.match(script, /function setBoundPhoneStatus\(type, message\)/);
|
|
assert.match(script, /setStatus\(form, 'error', '验证码发送失败,请稍后重试。'\)/);
|
|
});
|