70 lines
3.6 KiB
JavaScript
70 lines
3.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const factory = vm.runInNewContext(fs.readFileSync('src/features/opportunities/internal-sr-session.js', 'utf8') + '\ncreateInternalSrSession');
|
|
test('without a document, session recovery stops without polling', async () => {
|
|
for (const result of [{ok:false,httpStatus:401}, {ok:false,debugRequests:[{httpStatus:401}]}, {ok:false,error:'Request failed with status 401.'}]) {
|
|
let calls=0;
|
|
const service=factory({query:async()=>{calls++; return result;},environment:{document:{createElement(){assert.fail('No iframe');}},setTimeout(){assert.fail('No polling');}}});
|
|
await assert.rejects(service.request('123'), /Internal SR|status 401/);
|
|
await new Promise(resolve=>setImmediate(resolve));
|
|
assert.equal(calls,1);
|
|
}
|
|
});
|
|
|
|
function harness() {
|
|
const timers = [], listeners = new Map();
|
|
let appended = 0, removed = 0;
|
|
const frame = {style:{},setAttribute(){},addEventListener(k,v){listeners.set(k,v);},removeEventListener(k){listeners.delete(k);},remove(){removed++;}};
|
|
return { timers, frame, listeners, counts:()=>({appended,removed}), environment:{
|
|
document:{createElement:()=>frame,body:{append(){appended++;}}},
|
|
setTimeout(fn,delay){timers.push({fn,delay});}
|
|
}};
|
|
}
|
|
const flush = () => new Promise(resolve=>setImmediate(resolve));
|
|
test('hidden iframe remains alive after load and closes only after successful session query',async()=>{
|
|
const h=harness(); let calls=0;
|
|
const api=factory({environment:h.environment,query:async()=>++calls<3?{ok:false,httpStatus:401}:{ok:true,items:[{SrId:1}]}});
|
|
const first=api.request('123'); assert.equal(first,api.request(123));
|
|
await flush();
|
|
assert.equal(h.counts().appended,1);
|
|
h.listeners.get('load')?.();
|
|
assert.equal(h.counts().removed,0);
|
|
assert.equal(calls,1);
|
|
assert.equal(h.timers[0].delay,2000);
|
|
h.timers.shift().fn(); await flush();
|
|
assert.equal(h.counts().removed,0);
|
|
assert.equal(h.timers[0].delay,4000);
|
|
h.timers.shift().fn();
|
|
assert.equal((await first)[0].SrId,1);
|
|
assert.equal(h.counts().removed,1);
|
|
});
|
|
test('persistent 401 is bounded to five recovery checks and removes iframe',async()=>{
|
|
const h=harness(); let calls=0;
|
|
const api=factory({environment:h.environment,query:async()=>{calls++;return {ok:false,httpStatus:401};}});
|
|
const failed=assert.rejects(api.request('123'),/still unavailable/);
|
|
for(const delay of [2000,4000,8000,12000,16000]) {
|
|
await flush(); assert.equal(h.timers[0].delay,delay); h.timers.shift().fn();
|
|
}
|
|
await failed;
|
|
assert.equal(calls,6);
|
|
assert.equal(h.counts().removed,1);
|
|
assert.equal(h.timers.length,0);
|
|
});
|
|
|
|
test('concurrent requests share work and explicit retry can recover', async () => {
|
|
let calls=0, complete;
|
|
const service=factory({query:()=>{calls++; return new Promise(resolve=>{complete=resolve;});}});
|
|
const first=service.request('123'); const second=service.request(123);
|
|
assert.equal(first,second); await Promise.resolve();
|
|
complete({ok:false,httpStatus:401}); await assert.rejects(first);
|
|
const retry=service.request('123'); await Promise.resolve(); complete({ok:true,items:[{SrId:'456'}]});
|
|
assert.equal((await retry)[0].SrId,'456'); assert.equal(calls,2);
|
|
});
|
|
test('other errors do not retry and successful empty results are preserved', async()=>{
|
|
const failed=factory({query:async()=>({ok:false,error:'Forbidden',httpStatus:403})});
|
|
await assert.rejects(failed.request('123'),/Forbidden/);
|
|
const ok=factory({query:async()=>({ok:true,items:[]})}); assert.equal((await ok.request('123')).length,0);
|
|
});
|