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-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'); 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/); });