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