'use strict'; const assert = require('node:assert/strict'); module.exports = async ({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html}) => { const snapshot = {generation:7,running:true,transitioning:false,controllable:true,identity_generation:11,fingerprint:'ab'.repeat(32),rotatable:true}; const file = (extra={}) => ({name:'firmware.bin',size:4194304,type:'',...extra}); const n = (b,id) => b.document.getElementById('firmware-'+id); async function open() { const b=await adminBrowser(); b.click('select-settings'); await tick(); b.queues['/api/settings/lifecycle'].push(json(snapshot)); b.click('settings-lifecycle'); await tick(); n(b,'file').files=[file()]; return b; } async function upload(b) { b.click('firmware-upload'); await tick(); return b.uploads.at(-1); } await test('Firmware admin-only card validates filename hint/size, not unreliable browser MIME, and requires explicit confirmation', async()=>{ assert.match(html,/accept="\.bin"/); assert.match(html,/firmware\.bin built for this device/); assert.match(html,/Saved settings are kept/); const u=browser(); u.start(); await tick(); n(u,'file').files=[file()]; await upload(u); assert.equal(u.uploads.length,0); for(const f of [null,file({name:'flash.txt'}),file({size:0}),file({size:4194305}),file({size:NaN}),file({size:1.5})]) { const b=await open(); n(b,'file').files=f?[f]:[]; await upload(b); assert.equal(b.uploads.length,0); assert.match(n(b,'detail').textContent,/nonempty .bin/); } const b=await open(); let warning=''; b.window.confirm=s=>{warning=s;return false;}; await upload(b); assert.equal(b.uploads.length,0); assert.equal(warning,'Upload firmware and reboot? All connections will close. Saved settings are kept; unsaved changes will be lost.'); b.window.confirm=()=>true; n(b,'file').files=[file({name:'APP.BIN',type:'text/plain'})]; const x=await upload(b); assert.ok(x.file); }); await test('Firmware sends original File raw with current CSRF and browser-managed headers; one flight and lifecycle controls gated',async()=>{ const b=await open(), f=n(b,'file').files[0], x=await upload(b); assert.equal(x.method,'POST'); assert.equal(x.url,'/api/firmware'); assert.equal(x.file,f); assert.deepEqual(x.headers,{'Content-Type':'application/octet-stream','X-CSRF-Token':token}); assert.equal(x.timeout,180000); assert.ok(n(b,'file').disabled && n(b,'upload').disabled); for(const action of ['stop','restart','rotate','reboot']) {assert.ok(b.nodes['lifecycle-'+action].disabled); b.click('lifecycle-'+action);} await upload(b); assert.equal(b.uploads.length,1); assert.ok(!b.calls.some(c=>c.url.endsWith('lifecycle-operation')&&c.method==='POST')); x.upload.onprogress({lengthComputable:true,loaded:25,total:100}); assert.equal(n(b,'progress').value,25); x.upload.onprogress({lengthComputable:true,loaded:100,total:100}); assert.match(n(b,'detail').textContent,/Validating firmware/); for(const e of [{lengthComputable:false},{lengthComputable:true,loaded:NaN,total:100},{lengthComputable:true,loaded:1,total:0}]) { x.upload.onprogress(e); assert.match(n(b,'detail').textContent,/unavailable/); } x.reply(200,{ok:true,rebooting:true}); assert.match(n(b,'detail').textContent,/accepted; rebooting/); assert.match(n(b,'detail').textContent,/Reconnect and sign in/); await upload(b); assert.equal(b.uploads.length,1); assert.ok(n(b,'upload').disabled); x.onerror(); assert.match(n(b,'detail').textContent,/accepted; rebooting/); }); await test('Firmware fences delayed session validation, changed identity/role/CSRF and logout before sending',async()=>{ for(const response of [failure(401),session({role:'user'}),session({role:'admin',username:'changed'}),session({role:'admin',csrf:'b'.repeat(64)})]) { const b=await open(); b.queues['/api/session'].push(response); await upload(b); assert.equal(b.uploads.length,0); assert.ok(b.redirects.length); } const b=await open(), d=deferred(); b.queues['/api/session'].push(d.promise); await upload(b); await upload(b); assert.equal(b.uploads.length,0); b.click('sign-out'); await tick(); d.resolve(session({role:'admin'})); await tick(); assert.equal(b.uploads.length,0); }); await test('Firmware session loss/pagehide/logout abort once and fence all late progress/success/error/401 callbacks',async()=>{ for(const end of ['logout','pagehide','expiry','identity']) { const b=await open(), x=await upload(b); if(end==='logout') b.click('sign-out'); else if(end==='pagehide') b.emit('pagehide'); else if(end==='expiry') x.reply(401,{error:'authentication_required'}); else { b.queues['/api/session'].push(session({role:'admin',username:'changed'})); b.click('select-serial'); b.click('connection-toggle'); await tick(); b.click('connection-toggle'); } await tick(); assert.ok(x.aborted,end); const detail=n(b,'detail').textContent, redirects=b.redirects.length; assert.match(detail,/may already be installed/); x.upload.onprogress({lengthComputable:true,loaded:50,total:100}); x.reply(200,{ok:true,rebooting:true}); x.reply(401,{}); x.onerror(); assert.equal(n(b,'detail').textContent,detail); assert.equal(b.redirects.length,redirects); assert.equal(b.uploads.length,1); } }); await test('Firmware navigation retains single upload and lifecycle gate without resending',async()=>{ const b=await open(), x=await upload(b); b.click('settings-serial'); await tick(); b.click('settings-lifecycle'); await tick(); assert.ok(!x.aborted); assert.ok(b.nodes['lifecycle-reboot'].disabled); await upload(b); assert.equal(b.uploads.length,1); x.reply(200,{ok:true,rebooting:true}); assert.match(n(b,'detail').textContent,/rebooting/); }); await test('Firmware maps every backend error safely, never displays arbitrary response text or retries',async()=>{ const groups={400:['invalid_request','invalid_firmware','firmware_incomplete'],403:['origin','csrf','admin_required'],408:['firmware_timeout'],413:['firmware_too_large'],415:['firmware_content_type'],500:['firmware_write_failed','firmware_commit_failed'],503:['unavailable','busy','firmware_unavailable','firmware_resources']}; for(const [status,codes] of Object.entries(groups)) for(const error of codes) { const b=await open(), x=await upload(b); x.reply(Number(status),{error}); assert.doesNotMatch(n(b,'detail').textContent,/Update status unknown/); assert.ok(n(b,'detail').textContent.length > 0 && n(b,'detail').textContent.length < 100); assert.equal(b.uploads.length,1); } for(const response of ['SECRET ERROR BODY','x'.repeat(129),{ok:true},{ok:true,rebooting:true,secret:'bad'},{error:'