Add admin firmware upload support
Implement authenticated HTTPS OTA uploads with bounded streaming, image validation, reboot coordination, and lifecycle exclusion. Add the admin UI, regression tests, and Phase 10 acceptance documentation.
This commit is contained in:
@@ -14,7 +14,15 @@ const tick = async () => { for (let i = 0; i < 6; ++i) await new Promise(r => se
|
||||
function browser({onlyLoader = false, withLoader = false, role = 'user', username = '<img>'} = {}) {
|
||||
const nodes = {}, events = {}, calls = [], redirects = [], timers = new Map(), sockets = [], terminals = [];
|
||||
const queues = {'/api/session': [], '/api/status': [], '/api/ws-ticket': [], '/api/admin/ws-ticket': [], '/api/logout': [], '/api/settings/serial': [], '/api/settings/serial-operation': [], '/api/settings/accounts': [], '/api/settings/account-operation': [], '/api/settings/accounts/generate-password': [], '/api/settings/accounts/keys': [], '/api/settings/network': [], '/api/settings/network-operation': [], '/api/settings/display': [], '/api/settings/display-operation': [], '/api/settings/broker': [], '/api/settings/broker-operation': [], '/api/settings/ssh': [], '/api/settings/ssh-operation': [], '/api/settings/lifecycle': [], '/api/settings/lifecycle-operation': []};
|
||||
const fits = [];
|
||||
const fits = [], uploads = [];
|
||||
class Upload {
|
||||
constructor() { this.upload = {}; this.headers = {}; uploads.push(this); }
|
||||
open(method, url) { this.method = method; this.url = url; }
|
||||
setRequestHeader(key, value) { this.headers[key] = value; }
|
||||
send(file) { this.file = file; }
|
||||
abort() { this.aborted = true; this.onabort?.(); }
|
||||
reply(status, value) { this.status = status; this.responseText = typeof value === 'string' ? value : JSON.stringify(value); this.onload?.(); }
|
||||
}
|
||||
let serial = 0, now = Date.now();
|
||||
class Clock extends Date { static now() { return now; } }
|
||||
const on = (key, fn) => { if (!(events[key] ||= []).includes(fn)) events[key].push(fn); };
|
||||
@@ -60,7 +68,7 @@ function browser({onlyLoader = false, withLoader = false, role = 'user', usernam
|
||||
constructor() { this.measurements = []; this.calls = 0; fits.push(this); }
|
||||
proposeDimensions() { ++this.calls; return this.measurements.length ? this.measurements.shift() : {cols: 80, rows: 24}; }
|
||||
}},
|
||||
TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, AbortController, URL, Date: Clock, performance: {now: () => now}, WebSocket: Socket,
|
||||
TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, AbortController, URL, Date: Clock, performance: {now: () => now}, WebSocket: Socket, XMLHttpRequest: Upload,
|
||||
fetch: async (url, options) => {
|
||||
// Apply the Origin regression guard to every mutation, including logout.
|
||||
assert.ok(Object.hasOwn(queues, url));
|
||||
@@ -83,7 +91,7 @@ function browser({onlyLoader = false, withLoader = false, role = 'user', usernam
|
||||
const match = [...timers].find(([, t]) => t.ms === ms); assert.ok(match, `missing timer ${ms}`);
|
||||
const [id, t] = match; if (!t.interval) timers.delete(id); t.fn();
|
||||
};
|
||||
return {nodes, document: context.document, calls, redirects, timers, sockets, terminals, queues, fits, events, emit, start, fire,
|
||||
return {nodes, document: context.document, calls, redirects, timers, sockets, terminals, queues, fits, events, emit, start, fire, uploads,
|
||||
click: id => nodes[id].click(), elapse: ms => { now += ms; }, window};
|
||||
}
|
||||
async function connected() { const b = browser(); b.start(); await tick(); assert.equal(b.sockets.length, 1); return b; }
|
||||
@@ -1399,5 +1407,6 @@ async function test(name, fn) { await fn(); ++passed; console.log('PASS JS:', na
|
||||
await require('./broker.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
await require('./ssh.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
await require('./lifecycle.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
await require('./firmware.cjs')({test, browser, adminBrowser, tick, json, session, failure, deferred, token, html});
|
||||
console.log(`PASS ${passed} browser behavior groups (production C-rendered JS)`);
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
|
||||
Reference in New Issue
Block a user