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 source = fs.readFileSync(path.join(__dirname,'../internal/web/static/app.js'),'utf8'); function functionSource(name,next) { return source.slice(source.indexOf('function '+name+'('),source.indexOf('\nfunction '+next+'(')); } test('bandwidth cursor follows elapsed time across a rolling history and a new session',()=>{ const seen=[]; const context={bwtestState:{lastSampleElapsed:-1},handleBwTestSample:s=>seen.push(s.elapsed_sec),clearInterval(){},setBwTestRunningUI(){},handleBwTestFinish(){}}; vm.createContext(context); vm.runInContext(functionSource('processBwTestStatus','startBwTestTelemetry'),context); const window=offset=>Array.from({length:200},(_,i)=>({elapsed_sec:i+offset})); context.processBwTestStatus({session_id:'one',running:true,recent_samples:window(1)}); context.processBwTestStatus({session_id:'one',running:true,recent_samples:window(2)}); assert.equal(seen.length,201); assert.equal(seen.at(-1),201); context.processBwTestStatus({session_id:'two',running:true,recent_samples:[{elapsed_sec:0.5}]}); assert.equal(seen.at(-1),0.5); }); test('fetch coalesces API reads, copies responses, and keeps bearer tokens on this origin',async()=>{ const requests=[]; const localStorage={getItem:()=> 'test-token',removeItem(){}}; const context={URL,Headers,Request,Response,AbortController,setTimeout,clearTimeout,console,localStorage,document:{getElementById:()=>null}}; context.window={location:{href:'http://router/',origin:'http://router'},fetch:async(resource,options)=>{ requests.push({resource,options}); await new Promise(resolve=>setTimeout(resolve,10)); return new Response('{"ok":true}',{headers:{'content-type':'application/json'}}); }}; vm.createContext(context); vm.runInContext(source.slice(source.indexOf('const originalFetch'),source.indexOf('\nlet state ='))+'\nlet state = {currentUser:null};',context); const responses=await Promise.all([context.window.fetch('/api/status'),context.window.fetch('/api/status')]); assert.equal(requests.length,1); assert.deepEqual(await responses[0].json(),{ok:true}); assert.deepEqual(await responses[1].json(),{ok:true}); assert.equal(requests[0].options.headers.get('Authorization'),'Bearer test-token'); await context.window.fetch('https://another-origin.example/path'); assert.equal(requests[1].options.headers,undefined); }); test('HTML contains no duplicate IDs',()=>{ const html=fs.readFileSync(path.join(__dirname,'../internal/web/static/index.html'),'utf8'); const ids=[...html.matchAll(/\bid="([^"]+)"/g)].map(match=>match[1]); assert.equal(new Set(ids).size,ids.length); }); test('routing delete keeps selection and reports a rejected change',async()=>{ const messages=[]; let refreshes=0; const context={API_BASE:'/api',state:{selectedRipRow:{instances:'r1'}},confirm:()=>true, fetch:async()=>new Response('{"message":"Cannot delete active instance"}',{status:409}), showToast:(...args)=>messages.push(args),loadDynamicRouting:async()=>refreshes++,Response}; vm.createContext(context); vm.runInContext(source.slice(source.indexOf('async function responseError('),source.indexOf('\n}',source.indexOf('async function responseError('))+2),context); vm.runInContext(source.slice(source.indexOf('async function routingMutation('),source.indexOf('\n}',source.indexOf('async function routingMutation('))+2),context); vm.runInContext(source.slice(source.indexOf('async function removeSelectedRipInstance('),source.indexOf('\n}',source.indexOf('async function removeSelectedRipInstance('))+2),context); await context.removeSelectedRipInstance(); assert.equal(context.state.selectedRipRow.instances,'r1'); assert.equal(refreshes,0); assert.match(messages[0][1],/Cannot delete active instance/); }); test('view health filters unrelated requests and reports failures',()=>{ const element={}; const context={document:{getElementById:()=>element},state:{activeTab:'eigrp'},Date, telemetryHealth:{failures:new Map([['/api/routing/eigrp/neighbors','HTTP 500'],['/api/firewall/filter','HTTP 500']]),successes:new Map([['/api/routing/eigrp/instances',Date.now()]])}}; vm.createContext(context); vm.runInContext(source.slice(source.indexOf('function viewUsesEndpoint('),source.indexOf('function renderTelemetryHealth(')),context); context.renderViewDataHealth(); assert.match(element.textContent,/incomplete/); assert.match(element.title,/eigrp/); assert.doesNotMatch(element.title,/firewall/); context.telemetryHealth.failures.delete('/api/routing/eigrp/neighbors'); context.renderViewDataHealth(); assert.match(element.textContent,/oldest update/); }); test('network overview displays reported state without treating it as reachability',()=>{ const context={};vm.createContext(context); vm.runInContext(source.slice(source.indexOf('function formatNetworkOverview('),source.indexOf('\nfunction loadToolsView(')),context); assert.match(context.formatNetworkOverview('Interfaces',[{name:'eth0',operstate:'up',ipv4:['192.0.2.1/24'],ipv6:[]}]),/eth0: up.*192\.0\.2\.1/); assert.match(context.formatNetworkOverview('WireGuard peers',[{interface:'wg0',name:'site',disabled:false}]),/last handshake not reported/); assert.match(context.formatNetworkOverview('AeroTUN',[{name:'a0',running:true,connected:false}]),/not connected/); assert.equal(context.formatNetworkOverview('Routes',{}),'Unexpected response'); }); test('OSPF rejected delete keeps modal open and reports API error',async()=>{ const events=[]; const context={API_BASE:'/api',document:{getElementById:()=>({value:'r1'})},confirm:()=>true, fetch:async()=>new Response('{"message":"In use"}',{status:409}), closeOspfAreaRangeModal:()=>events.push('closed'),showToast:(...x)=>events.push(x),loadDynamicRouting:()=>events.push('refreshed'),Response}; vm.createContext(context); for(const name of ['responseError','routingMutation','deleteCurrentOspfAreaRange']) { const start=source.indexOf('async function '+name+'('); const end=source.indexOf('\n}',start)+2; vm.runInContext(source.slice(start,end),context); } await context.deleteCurrentOspfAreaRange(); assert.equal(events.some(e=>e==='closed' || e==='refreshed'),false); assert.ok(events.some(e=>Array.isArray(e) && e[1]==='In use' && e[2]==='error')); }); test('OSPF rejected toggle does not alter cached range',async()=>{ const range={id:'r1',disabled:true};const messages=[]; const context={API_BASE:'/api',state:{selectedOspfRow:{'area-ranges':'r1'},ospfAreaRanges:[range]}, fetch:async()=>new Response('Rejected',{status:400}),showToast:(...x)=>messages.push(x),loadDynamicRouting:()=>{throw Error('unexpected refresh')},Response}; vm.createContext(context); for(const name of ['responseError','routingMutation','enableSelectedOspfAreaRange']) { const start=source.indexOf('async function '+name+'('); const end=source.indexOf('\n}',start)+2; vm.runInContext(source.slice(start,end),context); } await context.enableSelectedOspfAreaRange(); assert.equal(range.disabled,true);assert.equal(messages[0][1],'Rejected'); });