#!/usr/bin/env node // Runs the real Docker VHosts dashboard module against a minimal DOM + API fixture. // This is a JS-only regression, not a compiled RustProxy or live agent test. import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; const dockerSource = readFileSync('src/admin/web/docker.js', 'utf8'); const coreSource = readFileSync('src/admin/web/app/core.js', 'utf8'); const english = JSON.parse(readFileSync('langs/en_US.json', 'utf8')).translations; const polish = JSON.parse(readFileSync('langs/pl_pl.json', 'utf8')).translations; for (const translations of [english, polish]) { assert.ok(translations.dockerNoObservation); assert.ok(translations.dockerAgentConnectionFailed); assert.ok(translations.dockerUnknownPollError); assert.doesNotMatch(translations.dockerNoObservation.toLowerCase(), /refresh|odśwież/); } class FakeNode { constructor(tag, text = '', cls = '') { this.tagName = tag; this.content = String(text ?? ''); this.className = cls; this.children = []; this.dataset = {}; this.attrs = {}; this.events = {}; this.hidden = false; this.open = false; this.classList = { toggle: (name, value) => { const parts = new Set(this.className.split(' ').filter(Boolean)); if (value) parts.add(name); else parts.delete(name); this.className = [...parts].join(' '); }, add: name => { this.className += ` ${name}`; }, remove: name => { this.className = this.className.split(' ').filter(part => part !== name).join(' '); }, }; } get textContent() { return this.content + this.children.map(child => child.textContent || '').join(''); } set textContent(value) { this.content = String(value ?? ''); this.children = []; } append(...nodes) { this.children.push(...nodes.filter(Boolean)); } replaceChildren(...nodes) { this.children = nodes.filter(Boolean); } setAttribute(name, value) { this.attrs[name] = value; } addEventListener(name, fn) { this.events[name] = fn; } descendants() { return this.children.flatMap(child => [child, ...child.descendants()]); } querySelectorAll(selector) { const part = selector.split(' ').at(-1); if (part.startsWith('.')) return this.descendants().filter(child => child.className.split(' ').includes(part.slice(1))); return []; } querySelector(selector) { return this.querySelectorAll(selector)[0] || null; } } const node = (tag, text, cls) => new FakeNode(tag, text, cls); const destination = node('div'); const elements = new Map([ ['docker-discovery-list', destination], ['overview-refresh', node('button')], ['page-label', node('span')], ['page-title', node('h1')], ['page-description', node('p')] ]); const agent = { id: 'docker1', name: 'Docker One', enabled: true, base_domain: 'apps.example.test', endpoint: 'https://192.0.2.2:9876', tls_fingerprint_sha256: 'A'.repeat(64), poll_interval_seconds: 30, assignments: [] }; const config = { docker_agents: { agents: [agent] }, listeners: { endpoints: [] }, tls: { certificates: [] } }; const storage = new Map(); const requests = []; let nextResult = { observations: [], pools: [], pool_routes: [] }; let refreshSpec; let syncs = 0, manuals = 0; const coordinator = { register(id, spec) { assert.equal(id, 'docker'); refreshSpec = spec; }, sync() { syncs++; }, manual(id) { assert.equal(id, 'docker'); manuals++; return Promise.resolve(); }, setPanel(name) { this.panel = name; }, }; const ctx = { el: node, $: name => elements.get(name) || null, t: name => english[name] || name, document: { activeElement: null, querySelectorAll(selector) { if (selector === '#docker-discovery-list .docker-agent-card') return destination.children; return []; } }, window: { rustProxyRefresh: coordinator, rustProxySettings: {} }, rustProxyRefresh: coordinator, localStorage: { getItem: key => storage.get(key) || null, setItem: (key, value) => storage.set(key, value) }, authenticated: true, authEpoch: 1, currentPanel: 'docker-vhosts', envelope: { config }, settingsDirty: new Set(), busy: false, api: async (path, args = {}) => { requests.push({ path, args }); return nextResult; }, toast(message) { throw new Error(`Unexpected toast: ${message}`); }, console, }; vm.createContext(ctx); vm.runInContext(dockerSource, ctx, { filename: 'src/admin/web/docker.js' }); assert(refreshSpec, 'register Docker refresh with central scheduler'); assert.equal(refreshSpec.interval(), 5); ctx.window.rustProxyDocker.render(); assert.equal(manuals, 1, 'deep-link bootstrap fetches a snapshot after configuration loads'); assert.equal(syncs, 1, 'deep-link bootstrap syncs waiting-state scheduler'); assert.equal(refreshSpec.active(), true, 'bootstrap retries cached status without requiring local Live view'); assert.match(destination.textContent, /WAITING/); assert.match(destination.textContent, /Waiting for the first agent check/); assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/); assert.doesNotMatch(destination.textContent, /Agent connection failed/); assert.equal(destination.querySelector('.docker-agent-error'), null, 'pending must not be styled as a failure'); let tlsPill = destination.querySelectorAll('.pill').find(item => item.textContent === 'TLS PINNED'); assert.ok(tlsPill); assert.ok(!tlsPill.className.split(' ').includes('live'), 'configured fingerprint does not prove successful handshake'); await ctx.window.rustProxyDocker.refresh(false); assert.equal(requests.at(-1).path, '/api/docker'); assert.notEqual(requests.at(-1).args.method, 'POST', 'automatic status fetch never polls the agent'); assert.equal(refreshSpec.active(), true, 'retry while the initial server-side poll is not complete'); nextResult = { observations: [{ agent_id: 'docker1', ok: false, error: 'connection refused', checked_at_unix_ms: 1, inventory: null }], pools: [], pool_routes: [] }; await ctx.window.rustProxyDocker.refresh(false); assert.match(destination.textContent, /Last Docker agent check failed/); assert.match(destination.textContent, /connection refused/); assert.doesNotMatch(destination.textContent, /TLS certificate SHA-256:/, 'pinned fingerprint is configuration, not the reason for an arbitrary failure'); assert.ok(destination.querySelector('.docker-agent-error'), 'a genuine failed observation must be visible'); assert.equal(refreshSpec.active(), false, 'local Live-off stops automatic refresh after first status, even if it is a failure'); nextResult = { observations: [{ agent_id: 'docker1', ok: true, error: null, checked_at_unix_ms: Date.now(), inventory: { agent_version: '0.22.3', hostname: 'docker1', addresses: [], services: [], public_ip: '192.0.2.2', certificate_sha256: 'A'.repeat(64) } }], pools: [], pool_routes: [] }; await ctx.window.rustProxyDocker.refresh(false); assert.match(destination.textContent, /CONNECTED/); assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/); assert.equal(refreshSpec.active(), false, 'local Live-off remains off with a successful observation'); tlsPill = destination.querySelectorAll('.pill').find(item => item.textContent === 'TLS PINNED'); assert.ok(tlsPill.className.split(' ').includes('live'), 'pin may be marked verified after a successful poll'); storage.set('rustproxy.docker.live-view', '1'); assert.equal(refreshSpec.active(), true, 'Live view still polls normally'); storage.delete('rustproxy.docker.live-view'); // Execute the real panel-navigation function with side effects disabled. const from = coreSource.indexOf('function setPanel(name, navigate = true) {'); const to = coreSource.indexOf('function bytes(value)', from); assert(from >= 0 && to > from); ctx.PANEL_PATHS = { overview: '/', 'docker-vhosts': '/docker-vhosts' }; vm.runInContext(coreSource.slice(from, to), ctx); ctx.currentPanel = 'overview'; ctx.setPanel('docker-vhosts', false); assert.equal(manuals, 2, 'opening Docker VHosts requests a new cached snapshot even with Live view off'); ctx.setPanel('docker-vhosts', false); assert.equal(manuals, 2, 'rendering the same panel does not duplicate one-shot requests'); ctx.setPanel('overview', false); ctx.setPanel('docker-vhosts', false); assert.equal(manuals, 3, 're-entering the page reads the newest cached status'); ctx.currentPanel = 'overview'; assert.equal(refreshSpec.active(), false, 'no bootstrap activity outside Docker VHosts'); ctx.currentPanel = 'docker-vhosts'; config.docker_agents.agents = [{ ...agent, enabled: false }]; ctx.window.rustProxyDocker.render(); assert.equal(refreshSpec.active(), false, 'disabled agents do not cause polling'); assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/); console.log('PASS: 0.31.9 Docker startup awaiting/poll failure/success, pinned TLS semantics, Live-off bootstrap and return-navigation refresh (real JS, simulated DOM/API).');