Files
pj_h5_cloud/tests/file-upload.test.cjs
2026-10-29 15:46:36 +08:00

81 lines
6.1 KiB
JavaScript

const {test} = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const Module = require('node:module')
const ts = require('typescript')
const root = path.resolve(__dirname, '..')
const folder = fs.existsSync(path.join(root,'src/utils/file-access-core.ts')) ? 'src/utils' : 'src/sheep/utils'
const corePath = path.join(root,folder,'file-access-core.ts')
const compile = source => ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText
const core = new Module(corePath,module); core.filename=corePath; core.paths=Module._nodeModulePaths(path.dirname(corePath))
core._compile(compile(fs.readFileSync(corePath,'utf8')),corePath)
const vectors=JSON.parse(fs.readFileSync(path.join(root,'test-vectors/file-upload.json'),'utf8'))
for(const v of vectors) test('upload signature vector: '+v.target,()=>{
assert.equal(core.exports.fileUploadCanonical(v.target,v.appId,v.kid,v.ts,v.nonce),v.canonical)
assert.equal(core.exports.fileUploadSignature(v.target,v.appId,v.kid,v.ts,v.nonce,v.key),v.sign)
})
test('upload credentials bind the purpose and target, renew per request and never leak to other hosts or imports',()=>{
let time=1793232000000
const client=core.exports.createFileAccess({appId:'admin',kid:'v1',key:vectors[0].key,baseUrl:()=> 'https://files.example/gateway/api',now:()=>time,allowedOrigins:['https://legacy.example']})
for(const [source,target] of [['/api/admin/sys-file/upload','upload'],['/sys-file/inner-upload','inner-upload'],['https://legacy.example/admin/sys-file/qn/upload','qn-upload'],['https://files.example/gateway/api/admin/sys-file/upload','upload']]){
const headers=client.uploadHeaders(source,vectors[0].nonce)
assert.equal(headers['X-File-Signature'],core.exports.fileUploadSignature(target,'admin','v1',1793232000,vectors[0].nonce,vectors[0].key))
assert.equal(Object.values(headers).includes(vectors[0].key),false)
}
const first=client.uploadHeaders('/admin/sys-file/upload'), second=client.uploadHeaders('/admin/sys-file/upload')
assert.notEqual(first['X-File-Nonce'],second['X-File-Nonce'])
assert.notEqual(first['X-File-Signature'],second['X-File-Signature'])
assert.equal(first['X-File-Timestamp'],'1793232000')
client.synchronize(time+86400000,1800,0)
assert.equal(client.uploadHeaders('/admin/sys-file/upload')['X-File-Timestamp'],'1793318400')
for(const source of ['https://outside.example/admin/sys-file/upload','https://files.example.evil/admin/sys-file/upload','/admin/sys-file/oss/file?fileName=x','/admin/user/import','data:upload','blob:upload']){
assert.deepEqual(client.uploadHeaders(source),{})
}
assert.notEqual(core.exports.fileAccessSignature('a.jpg','admin','v1',1793232000,vectors[0].key),first['X-File-Signature'])
})
function runtime(){
const filename=path.join(root,folder,'file-access.ts')
const env={VITE_FILE_ACCESS_APP_ID:'admin',VITE_FILE_ACCESS_KEY_ID:'v1',VITE_FILE_ACCESS_KEY:vectors[0].key,VITE_IMAGE_BASE_PATH:'https://files.example/api',VITE_IMAGE_DEV_PATH:'https://files.example/api',VITE_APP_DEV_SERVICE:'https://files.example/api',VITE_APP_PROD_IMAGE:'https://files.example/api',VITE_APP_PROD_SERVICE:'https://files.example/api'}
const loaded=new Module(filename,module);loaded.filename=filename;loaded.paths=core.paths
loaded.require=name=>name==='./file-access-core'?core.exports:name==='./app'?{isDevEnv:()=>true}:core.require(name)
loaded._compile(compile(fs.readFileSync(filename,'utf8').replace(/import\.meta\.env/g,'('+JSON.stringify(env)+')')),filename)
return loaded.exports
}
if(folder==='src/utils') test('the real Axios interceptor signs each file upload and preserves login and tenant headers',()=>{
const api=runtime();let onRequest
const axios={create:()=>({interceptors:{request:{use(fn){onRequest=fn}},response:{use(){}}}})}
const filename=path.join(root,'src/utils/request.ts');const loaded=new Module(filename,module);loaded.filename=filename;loaded.paths=core.paths
loaded.require=name=>{
if(name==='axios')return {default:axios}
if(name==='./file-access')return api
if(name==='./other')return {default:{adaptationUrl:url=>url}}
if(name==='/@/utils/storage')return {Session:{getToken:()=> 'user-token',getTenant:()=> '42'}}
if(name==='/@/hooks/message')return {}
return core.require(name)
}
loaded._compile(compile(fs.readFileSync(filename,'utf8')),filename)
const first=onRequest({method:'post',url:'/admin/sys-file/upload',headers:{},data:{file:'unchanged'}})
const second=onRequest({method:'post',url:'/admin/sys-file/upload',headers:{},data:{file:'unchanged'}})
assert.equal(first.headers.Authorization,'Bearer user-token');assert.equal(first.headers['TENANT-ID'],'42')
assert.ok(first.headers['X-File-Signature']);assert.notEqual(first.headers['X-File-Nonce'],second.headers['X-File-Nonce'])
assert.deepEqual(first.data,{file:'unchanged'})
const other=onRequest({method:'post',url:'https://outside.example/admin/sys-file/upload',headers:{}})
assert.equal(other.headers['X-File-Signature'],undefined)
})
else test('the actual uni upload interceptor signs photo, video and rich text calls without changing form data',()=>{
const previous=global.uni;let interceptor
global.uni={addInterceptor(name,hook){assert.equal(name,'uploadFile');interceptor=hook},onNetworkStatusChange(){}}
try{
const api=runtime();api.installFileAccess({config:{globalProperties:{}},mixin(){}})
const first={url:'/api/admin/sys-file/upload',header:{Authorization:'Bearer user','TENANT-ID':'42'},formData:{dir:'photos'},filePath:'local-photo.png'}
const second={...first,header:{...first.header}}
interceptor.invoke(first);interceptor.invoke(second)
assert.ok(first.header['X-File-Signature']);assert.notEqual(first.header['X-File-Nonce'],second.header['X-File-Nonce'])
assert.equal(first.header.Authorization,'Bearer user');assert.equal(first.header['TENANT-ID'],'42')
assert.deepEqual(first.formData,{dir:'photos'});assert.equal(first.filePath,'local-photo.png')
const other={url:'https://outside.example/upload',header:{Authorization:'other'}};interceptor.invoke(other)
assert.deepEqual(other.header,{Authorization:'other'})
}finally{global.uni=previous}
})