Complete roadmap optionals: Holesail, Swarm UI, GitOps, virtualization.
CI / test (push) Successful in 9m58s
CI / test (push) Successful in 9m58s
Add tunnel persistence and container one-click tunnels with local Holesail client bind, Swarm services/nodes/tasks view, GitOps stack sync from git, deploy wizard step chrome, virtualized container lists, and structure regression tests. Mark Tracks A–D and optional future done.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import test from 'brittle'
|
||||
import { fetchComposeFromGit } from '../server/utils/gitops.js'
|
||||
|
||||
test('fetchComposeFromGit rejects bad URLs', async (t) => {
|
||||
await t.exception(() => fetchComposeFromGit({ repoUrl: '' }))
|
||||
await t.exception(() => fetchComposeFromGit({ repoUrl: 'ftp://evil' }))
|
||||
await t.exception(() =>
|
||||
fetchComposeFromGit({
|
||||
repoUrl: 'https://example.com/x.git',
|
||||
composePath: '../etc/passwd',
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,107 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
isHolesailEnabled,
|
||||
assertTunnelTarget,
|
||||
allowedTunnelHosts,
|
||||
holesailStatus,
|
||||
listTunnels,
|
||||
} from '../server/services/holesail-tunnels.js'
|
||||
|
||||
test('isHolesailEnabled respects ENABLE_HOLESAIL', (t) => {
|
||||
const prev = process.env.ENABLE_HOLESAIL
|
||||
process.env.ENABLE_HOLESAIL = '1'
|
||||
t.ok(isHolesailEnabled())
|
||||
process.env.ENABLE_HOLESAIL = '0'
|
||||
t.absent(isHolesailEnabled())
|
||||
if (prev === undefined) delete process.env.ENABLE_HOLESAIL
|
||||
else process.env.ENABLE_HOLESAIL = prev
|
||||
})
|
||||
|
||||
test('assertTunnelTarget allows loopback and rejects arbitrary hosts', (t) => {
|
||||
const ok = assertTunnelTarget('127.0.0.1', 8080)
|
||||
t.is(ok.port, 8080)
|
||||
|
||||
t.exception(() => assertTunnelTarget('127.0.0.1', 0))
|
||||
t.exception(() => assertTunnelTarget('10.0.0.5', 80))
|
||||
})
|
||||
|
||||
test('PEARDOCK_TUNNEL_HOSTS extends allowlist', (t) => {
|
||||
const prev = process.env.PEARDOCK_TUNNEL_HOSTS
|
||||
process.env.PEARDOCK_TUNNEL_HOSTS = '10.0.0.5, app.local'
|
||||
const hosts = allowedTunnelHosts()
|
||||
t.ok(hosts.has('10.0.0.5'))
|
||||
t.ok(hosts.has('app.local'))
|
||||
const resolved = assertTunnelTarget('10.0.0.5', 3000)
|
||||
t.is(resolved.port, 3000)
|
||||
if (prev === undefined) delete process.env.PEARDOCK_TUNNEL_HOSTS
|
||||
else process.env.PEARDOCK_TUNNEL_HOSTS = prev
|
||||
})
|
||||
|
||||
test('holesailStatus reports structure', (t) => {
|
||||
const st = holesailStatus()
|
||||
t.ok(typeof st.enabled === 'boolean')
|
||||
t.ok(typeof st.available === 'boolean')
|
||||
t.ok(typeof st.active === 'number')
|
||||
t.ok(Array.isArray(st.allowedHosts))
|
||||
})
|
||||
|
||||
test('listTunnels starts empty', (t) => {
|
||||
t.ok(Array.isArray(listTunnels()))
|
||||
})
|
||||
|
||||
test('createTunnel end-to-end when ENABLE_HOLESAIL=1', async (t) => {
|
||||
const prev = process.env.ENABLE_HOLESAIL
|
||||
const prevPath = process.env.PEARDOCK_TUNNELS_PATH
|
||||
const fs = await import('fs')
|
||||
const os = await import('os')
|
||||
const path = await import('path')
|
||||
const tmpFile = path.join(os.tmpdir(), `peardock-tunnels-test-${Date.now()}.json`)
|
||||
process.env.ENABLE_HOLESAIL = '1'
|
||||
process.env.PEARDOCK_TUNNELS_PATH = tmpFile
|
||||
t.teardown(async () => {
|
||||
if (prev === undefined) delete process.env.ENABLE_HOLESAIL
|
||||
else process.env.ENABLE_HOLESAIL = prev
|
||||
if (prevPath === undefined) delete process.env.PEARDOCK_TUNNELS_PATH
|
||||
else process.env.PEARDOCK_TUNNELS_PATH = prevPath
|
||||
try {
|
||||
fs.unlinkSync(tmpFile)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const { closeAllTunnels } = await import('../server/services/holesail-tunnels.js')
|
||||
await closeAllTunnels()
|
||||
})
|
||||
|
||||
// Free ephemeral local listener so tunnel has something to bind to
|
||||
const net = await import('net')
|
||||
const local = net.createServer((c) => c.end('ok'))
|
||||
await new Promise((resolve) => local.listen(0, '127.0.0.1', resolve))
|
||||
const port = local.address().port
|
||||
t.teardown(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
local.close(() => resolve())
|
||||
})
|
||||
)
|
||||
|
||||
const { createTunnel, closeTunnel, listTunnels: list, loadTunnelDefsFromDisk } = await import(
|
||||
'../server/services/holesail-tunnels.js'
|
||||
)
|
||||
const tunnel = await createTunnel({
|
||||
name: 'test-http',
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
secure: true,
|
||||
persist: true,
|
||||
})
|
||||
t.ok(tunnel.id)
|
||||
t.ok(tunnel.url.startsWith('hs://'))
|
||||
t.ok(tunnel.secure)
|
||||
t.is(list().length, 1)
|
||||
// Persisted to disk
|
||||
const defs = loadTunnelDefsFromDisk()
|
||||
t.ok(defs.some((d) => d.port === port))
|
||||
|
||||
t.ok(await closeTunnel(tunnel.id))
|
||||
t.is(list().length, 0)
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Lightweight visual/structure regression: ensure critical UI landmarks stay in index.html.
|
||||
* Complements full screenshot suites (optional future) without browser automation deps.
|
||||
*/
|
||||
import test from 'brittle'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8')
|
||||
|
||||
const REQUIRED_IDS = [
|
||||
'sidebar',
|
||||
'content',
|
||||
'job-drawer',
|
||||
'toast-stack',
|
||||
'containers-view',
|
||||
'deploy-view',
|
||||
'tunnels-view',
|
||||
'swarm-view',
|
||||
'host-view',
|
||||
'settings-view',
|
||||
'deploy-wizard-steps',
|
||||
'stack-git-url',
|
||||
'tunnel-create-btn',
|
||||
'swarm-services-body',
|
||||
]
|
||||
|
||||
const REQUIRED_SNIPPETS = [
|
||||
'data-view="tunnels"',
|
||||
'data-view="swarm"',
|
||||
'ENABLE_HOLESAIL',
|
||||
'GitOps',
|
||||
'collapse-sidebar-btn',
|
||||
]
|
||||
|
||||
test('index.html retains critical view landmarks', (t) => {
|
||||
for (const id of REQUIRED_IDS) {
|
||||
t.ok(html.includes(`id="${id}"`), `missing #${id}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('index.html retains critical feature snippets', (t) => {
|
||||
for (const s of REQUIRED_SNIPPETS) {
|
||||
t.ok(html.includes(s), `missing snippet: ${s}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('modern.css includes collapsed sidebar rail rules', (t) => {
|
||||
const css = fs.readFileSync(path.join(root, 'ui/modern.css'), 'utf8')
|
||||
t.ok(css.includes('#sidebar.collapsed'))
|
||||
t.ok(css.includes('icon') || css.includes('nav-link'))
|
||||
})
|
||||
Reference in New Issue
Block a user