132 lines
8.7 KiB
JavaScript
132 lines
8.7 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 corePath = fs.existsSync(path.join(root, 'src/utils/file-access-core.ts'))
|
|
? path.join(root, 'src/utils/file-access-core.ts') : path.join(root, 'src/sheep/utils/file-access-core.ts');
|
|
const loaded = new Module(corePath, module);
|
|
loaded.filename = corePath; loaded.paths = Module._nodeModulePaths(path.dirname(corePath));
|
|
loaded._compile(ts.transpileModule(fs.readFileSync(corePath, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText, corePath);
|
|
const { fileAccessSignature, fileAccessCanonical, createFileAccess, rewriteFileHtml, rewriteFileNodes } = loaded.exports;
|
|
const vectors = JSON.parse(fs.readFileSync(path.join(root, 'test-vectors/file-access.json'), 'utf8'));
|
|
for (const v of vectors) test('cross-language vector: ' + v.fileName, () => {
|
|
assert.equal(fileAccessCanonical(v.fileName,v.appId,v.kid,v.ts),v.canonical);
|
|
assert.equal(fileAccessSignature(v.fileName,v.appId,v.kid,v.ts,v.key),v.sign);
|
|
});
|
|
let time;
|
|
function client(baseUrl='https://files.example/api') {
|
|
time = 1793232000000;
|
|
return createFileAccess({appId:'mini',kid:'v1',key:vectors[0].key,baseUrl:()=>baseUrl,now:()=>time,allowedOrigins:['https://legacy.example']});
|
|
}
|
|
test('prefixes, absolute URLs, and bare names address the same file',()=>{
|
|
const c=client();
|
|
for(const value of ['/admin/sys-file/oss/file?fileName=a.jpg','/api/admin/sys-file/oss/file?fileName=a.jpg','https://files.example/api/admin/sys-file/oss/file?fileName=a.jpg','a.jpg']) {
|
|
const url=c.url(value); assert.match(url,/^https:\/\/files.example\/api\/admin\/sys-file\/oss\/file\?/);
|
|
assert.equal(new URL(url).searchParams.get('sign'),vectors[0].sign);
|
|
assert.ok(url.endsWith('fileName=a.jpg'));
|
|
}
|
|
});
|
|
test('relative API base, no duplicate prefixes',()=>{
|
|
const c=client('/api'); assert.match(c.url('/api/admin/sys-file/oss/file?fileName=a.jpg'),/^\/api\/admin/);
|
|
});
|
|
test('foreign URLs and local resources never receive credentials',()=>{
|
|
const c=client();
|
|
for(const value of ['https://other.example/admin/sys-file/oss/file?fileName=a.jpg','https://files.example.evil/api/admin/sys-file/oss/file?fileName=a.jpg','data:image/png;base64,AA==','blob:https://files.example/abc','/static/logo.png','https://files.example/qiniu/a.jpg']) assert.equal(c.url(value),value);
|
|
});
|
|
test('one decode, unusual names, no duplicated signature and cache renewal',()=>{
|
|
const c=client(), name='照片 +&%.png', source='/admin/sys-file/oss/file?fileName='+encodeURIComponent(name);
|
|
const first=c.url(source), params=new URL(first).searchParams;
|
|
assert.equal(params.get('fileName'),name); assert.equal(params.get('sign'),fileAccessSignature(name,'mini','v1',1793232000,vectors[0].key));
|
|
assert.equal(c.url(first),first); time+=10*60000; assert.equal(c.url(source),first);
|
|
time+=16*60000; const renewed=c.url(source); assert.notEqual(renewed,first);
|
|
assert.equal(c.strip(first),c.strip(renewed)); assert.equal(c.strip(source),source);
|
|
});
|
|
test('reject ambiguous file names and never silently use an empty key',()=>{
|
|
const c=client(); assert.throws(()=>c.url('/admin/sys-file/oss/file?fileName=a&fileName=b'));
|
|
assert.throws(()=>c.url('/admin/sys-file/oss/file?fileName='));
|
|
assert.throws(()=>fileAccessSignature('a','mini','v1',1,''));
|
|
});
|
|
test('clock correction renews a skewed signature',()=>{
|
|
const c=client();time+=86400000;c.synchronize(1793232000000,1800,0);
|
|
assert.equal(new URL(c.url('a.jpg')).searchParams.get('ts'),'1793232000');
|
|
});
|
|
test('routine clock synchronization preserves usable signatures and corrects large clock changes', () => {
|
|
const c = client(), first = c.url('a.jpg');
|
|
time += 2000; c.synchronize(time + 4000, 1800, 0);
|
|
assert.equal(c.url('a.jpg'), first);
|
|
c.synchronize(time + 86400000, 1800, 0);
|
|
assert.notEqual(c.url('a.jpg'), first);
|
|
});
|
|
test('HTML and rich-text nodes sign for presentation without mutating business values',()=>{
|
|
const c=client(), html='<p>hello</p><img src="/api/admin/sys-file/oss/file?fileName=a.jpg"><a href="https://other.example/a">x</a>';
|
|
const transform=s=>c.protected(s)?c.url(s):s;
|
|
const rendered=rewriteFileHtml(html,transform);assert.match(rendered,/&sign=/);assert.match(rendered,/href="https:\/\/other.example\/a"/);
|
|
const saved=rewriteFileHtml(rendered,s=>c.strip(s));assert.doesNotMatch(saved,/[?&](?:amp;)?(?:sign|ts|kid|appId)=/);assert.match(saved,/fileName=a.jpg/);
|
|
const nodes=[{name:'img',attrs:{src:'/api/admin/sys-file/oss/file?fileName=a.jpg'}}], before=JSON.stringify(nodes);
|
|
assert.match(rewriteFileNodes(nodes,transform)[0].attrs.src,/sign=/);assert.equal(JSON.stringify(nodes),before);
|
|
});
|
|
|
|
test('configured deployment prefixes and encoded parameter names remain unambiguous', () => {
|
|
const c = client('https://files.example/gateway/api');
|
|
const relative = '/gateway/api/admin/sys-file/oss/file?%66ileName=a.jpg';
|
|
const url = c.url(relative);
|
|
assert.equal(new URL(url).pathname, '/gateway/api/admin/sys-file/oss/file');
|
|
assert.equal(new URL(url).searchParams.get('sign'), vectors[0].sign);
|
|
assert.equal(c.url(url), url);
|
|
assert.throws(() => c.url(relative + '&fileName=b.jpg'));
|
|
const special = c.url('/admin/sys-file/oss/file?fileName=a%252Bb.png');
|
|
assert.equal(new URL(special).searchParams.get('fileName'), 'a%2Bb.png');
|
|
});
|
|
|
|
test('real runtime wrapper initializes, calibrates without login, and renews reactively', async () => {
|
|
const { computed } = require('vue');
|
|
const filename = path.join(path.dirname(corePath), 'file-access.ts');
|
|
const env = {
|
|
VITE_FILE_ACCESS_APP_ID: 'mini', 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', VITE_FILE_ACCESS_ALLOWED_ORIGINS: 'https://legacy.example',
|
|
};
|
|
const runtime = new Module(filename, module); runtime.filename = filename; runtime.paths = loaded.paths;
|
|
runtime.require = (name) => name === './file-access-core' ? loaded.exports : name === './app' ? { isDevEnv: () => true } : loaded.require(name);
|
|
const source = fs.readFileSync(filename, 'utf8').replace(/import\.meta\.env/g, '(' + JSON.stringify(env) + ')');
|
|
const originalFetch = global.fetch, originalUni = global.uni, realNow = Date.now;
|
|
const serverTime = realNow() + 86400000;
|
|
const info = { data: { serverTime, ttlSeconds: 1800 } };
|
|
global.fetch = async (url, options) => {
|
|
assert.equal(url, 'https://files.example/api/admin/sys-file/oss/access-info');
|
|
assert.equal(options.credentials, 'omit'); return { ok: true, json: async () => info };
|
|
};
|
|
global.uni = { request: options => { assert.match(options.url, /oss\/access-info$/); options.success({ statusCode: 200, data: info }); } };
|
|
let elapsed = 0;
|
|
Date.now = () => realNow() + elapsed;
|
|
try {
|
|
runtime._compile(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText, filename);
|
|
const api = runtime.exports, raw = '/admin/sys-file/oss/file?fileName=a.jpg';
|
|
const shown = computed(() => api.accessFileUrl(raw));
|
|
const first = shown.value;
|
|
assert.match(first, /sign=/);
|
|
assert.match(api.accessFileUrl('https://legacy.example/api/admin/sys-file/oss/file?fileName=a.jpg'), /sign=/);
|
|
await api.refreshFileAccess(true);
|
|
assert.equal(Number(new URL(shown.value).searchParams.get('ts')), Math.floor(serverTime / 1000));
|
|
const calibrated = shown.value;
|
|
elapsed = 26 * 60000;
|
|
api.fileAccessRevision.value++;
|
|
assert.notEqual(shown.value, calibrated);
|
|
assert.equal(api.originalFileUrl(shown.value), api.originalFileUrl(first));
|
|
assert.equal(raw, '/admin/sys-file/oss/file?fileName=a.jpg');
|
|
assert.equal(api.accessFileUrl('/static/logo.png'), '/static/logo.png');
|
|
assert.equal(api.accessFileUrl('blob:https://files.example/test'), 'blob:https://files.example/test');
|
|
} finally { global.fetch = originalFetch; global.uni = originalUni; Date.now = realNow; }
|
|
});
|
|
|
|
test('frontend proxy /api is normalized to the configured file-server prefix', () => {
|
|
const c = client('https://files.example');
|
|
assert.equal(new URL(c.url('/api/admin/sys-file/oss/file?fileName=a.jpg')).pathname, '/admin/sys-file/oss/file');
|
|
assert.equal(c.resolve('/api/admin/sys-file/oss/access-info'), 'https://files.example/admin/sys-file/oss/access-info');
|
|
assert.equal(new URL(c.url('https://legacy.example/api/admin/sys-file/oss/file?fileName=a.jpg')).pathname, '/api/admin/sys-file/oss/file');
|
|
});
|