80 lines
4.2 KiB
JavaScript
80 lines
4.2 KiB
JavaScript
const test=require('node:test');
|
|
const assert=require('node:assert/strict');
|
|
const fs=require('node:fs');
|
|
const path=require('node:path');
|
|
const vm=require('node:vm');
|
|
const {webcrypto}=require('node:crypto');
|
|
const flush=()=>new Promise(setImmediate);
|
|
|
|
function sessionPage(options={}){
|
|
const storage=options.storage || new Map(),calls=[],timers=new Map();let timerId=0;
|
|
const response=(data,status=200)=>({ok:status>=200&&status<300,status,json:async()=>data});
|
|
const context=vm.createContext({console,AbortController,Uint8Array,
|
|
window:{crypto:webcrypto},
|
|
localStorage:{getItem:key=>{if(options.storageBlocked) throw Error();return storage.get(key);},
|
|
setItem:(key,value)=>{if(options.storageBlocked) throw Error();storage.set(key,value);}},
|
|
setTimeout:(callback,delay)=>{const id=++timerId;timers.set(id,{callback,delay});return id;},clearTimeout:id=>timers.delete(id),
|
|
fetch:async(url,init)=>{calls.push({url,init});
|
|
if(options.fetch) return options.fetch(url,init,response);
|
|
return url==='/api/visitor/session' ? response({ok:true,key:JSON.parse(init.body).key || 'f'.repeat(64)}) : response([]);
|
|
},
|
|
});
|
|
vm.runInContext(fs.readFileSync(path.join(__dirname,'../../static/js/visitor.js'),'utf8'),context);
|
|
return {fetch:context.VisitorSession.fetch,calls,storage,
|
|
expire:async()=>{for(const timer of [...timers.values()])timer.callback();await flush();}};
|
|
}
|
|
|
|
test('a local random key is restored across reloads and differs in another browser',async()=>{
|
|
const a=sessionPage();await a.fetch('/api/get_history');
|
|
const key=JSON.parse(a.calls[0].init.body).key;
|
|
assert.match(key,/^[0-9a-f]{64}$/);
|
|
const restored=sessionPage({storage:a.storage});await restored.fetch('/api/get_history');
|
|
assert.equal(JSON.parse(restored.calls[0].init.body).key,key);
|
|
const b=sessionPage();await b.fetch('/api/get_history');
|
|
assert.notEqual(JSON.parse(b.calls[0].init.body).key,key);
|
|
assert.equal(a.calls[1].init.credentials,'same-origin');
|
|
assert.equal(a.calls.some(call=>call.url.includes(key)),false);
|
|
});
|
|
|
|
test('concurrent requests wait for one bootstrap before sending customer data',async()=>{
|
|
let release;
|
|
const app=sessionPage({fetch:(url,init,response)=>url==='/api/visitor/session'
|
|
? new Promise(resolve=>{release=()=>resolve(response({ok:true,key:JSON.parse(init.body).key}));}) : response([])});
|
|
const a=app.fetch('/api/get_history'),b=app.fetch('/api/new_chat',{method:'POST'});
|
|
assert.equal(app.calls.length,1);
|
|
release();await Promise.all([a,b]);
|
|
assert.equal(app.calls.filter(call=>call.url==='/api/visitor/session').length,1);
|
|
assert.equal(app.calls.length,3);
|
|
});
|
|
|
|
test('blocked local storage uses a server cookie without falling back to global history',async()=>{
|
|
const app=sessionPage({storageBlocked:true});await app.fetch('/api/get_history');
|
|
assert.equal(JSON.parse(app.calls[0].init.body).key,null);
|
|
assert.equal(app.calls[1].init.credentials,'same-origin');
|
|
const failed=sessionPage({fetch:(url,init,response)=>response({ok:false},503)});
|
|
await assert.rejects(failed.fetch('/api/get_history'));
|
|
assert.equal(failed.calls.length,1);
|
|
});
|
|
|
|
test('expired sessions rebootstrap once and retry the original request once',async()=>{
|
|
let attempts=0;
|
|
const app=sessionPage({fetch:(url,init,response)=>url==='/api/visitor/session'
|
|
? response({ok:true,key:JSON.parse(init.body).key}) : response([],++attempts===1 ? 401 : 200)});
|
|
const result=await app.fetch('/api/get_history');
|
|
assert.equal(result.status,200);
|
|
assert.equal(app.calls.filter(call=>call.url==='/api/visitor/session').length,2);
|
|
assert.equal(attempts,2);
|
|
});
|
|
|
|
test('a cancelled request waiting for bootstrap does not upload its body',async()=>{
|
|
let release;
|
|
const app=sessionPage({fetch:(url,init,response)=>new Promise(resolve=>{
|
|
release=()=>resolve(response({ok:true,key:JSON.parse(init.body).key}));
|
|
})});
|
|
const controller=new AbortController();
|
|
const pending=app.fetch('/api/upload',{method:'POST',body:'private file',signal:controller.signal});
|
|
controller.abort();release();
|
|
await assert.rejects(pending,{name:'AbortError'});
|
|
assert.equal(app.calls.length,1);
|
|
});
|