const {test} = require('node:test'); const assert = require('node:assert/strict'); const vm = require('node:vm'); const fs = require('node:fs'); const source = fs.readFileSync('src/main/frontend/account-session.js','utf8'); test('only trusted visible interaction renews, throttles successes, and retries failures', async () => { let now = 100000; let calls = 0; let fail = false; const listeners = {}; const window = {}; const document = {visibilityState:'visible',addEventListener:(name,fn) => listeners[name]=fn}; vm.runInNewContext(source,{window,document,AbortController,Date:{now:()=>now},fetch:async (url,options) => { calls++; assert.equal(url,'/account/activity'); assert.equal(options.method,'POST'); assert.equal(options.headers['X-CSRF-TOKEN'],'test-csrf'); if (fail) throw Error('offline'); return {status:204}; }}); window.werkjournalActivity('/account/activity','X-CSRF-TOKEN','test-csrf'); assert.equal(calls,0); await listeners.pointerdown({isTrusted:false}); document.visibilityState='hidden'; await listeners.keydown({isTrusted:true}); assert.equal(calls,0); document.visibilityState='visible'; await listeners.pointerdown({isTrusted:true}); await listeners.keydown({isTrusted:true}); assert.equal(calls,1); now += 60000; fail=true; await listeners.keydown({isTrusted:true}); assert.equal(calls,2); fail=false; await listeners.keydown({isTrusted:true}); assert.equal(calls,3); }); test('unavailable local storage does not prevent the CSRF-protected logout submission', () => { const window = {}; let submitted = false; let form; const document = {createElement: tag => tag === 'form' ? (form={append(input){this.input=input;},submit(){submitted=true;}}) : {},body:{append(){}}}; vm.runInNewContext(source,{window,document,get localStorage(){throw Error('storage blocked');}}); window.werkjournalLogout('/account/logout-all','_csrf','test-token','owner'); assert.equal(submitted,true); assert.equal(form.method,'POST'); assert.equal(form.action,'/account/logout-all'); assert.equal(form.input.name,'_csrf'); assert.equal(form.input.value,'test-token'); }); test('revocation clears only owner drafts, notifies other tabs and returns to login', async () => { const listeners = {}; let destination; const store = {'werkjournal.work.alice.new-2026-09-09.tab':'draft', 'werkjournal.work.bob.new-2026-09-09.tab':'other draft', 'werkjournal.theme':'dark'}; Object.defineProperties(store, { removeItem:{value:key=>delete store[key]}, setItem:{value:(key,value)=>store[key]=value} }); const window = {location:{assign:url=>destination=url}}; const document = {visibilityState:'visible',addEventListener:(name,fn)=>listeners[name]=fn}; vm.runInNewContext(source,{window,document,localStorage:store,AbortController, crypto:{randomUUID:()=> 'logout-event'},Date:{now:()=>100000},fetch:async()=>({status:401})}); window.werkjournalActivity('/app/account/activity','X-CSRF-TOKEN','test-token','alice'); await listeners.pointerdown({isTrusted:true}); assert.equal(store['werkjournal.work.alice.new-2026-09-09.tab'],undefined); assert.equal(store['werkjournal.work.bob.new-2026-09-09.tab'],'other draft'); assert.equal(store['werkjournal.theme'],'dark'); assert.equal(store['werkjournal.logout.alice'],'logout-event'); assert.equal(destination,'/app/login'); });