forked from snxraven/peardock
Return the active tunnel for the same host:port:protocol instead of starting a second Holesail instance; coalesce concurrent creates. UI disables the create button and shows “already exists” feedback.
172 lines
5.6 KiB
JavaScript
172 lines
5.6 KiB
JavaScript
import test from 'brittle'
|
|
import {
|
|
isHolesailEnabled,
|
|
assertTunnelTarget,
|
|
allowedTunnelHosts,
|
|
holesailStatus,
|
|
listTunnels,
|
|
} from '../server/services/holesail-tunnels.js'
|
|
|
|
test('isHolesailEnabled defaults on; ENABLE_HOLESAIL=0 disables', (t) => {
|
|
const prev = process.env.ENABLE_HOLESAIL
|
|
delete process.env.ENABLE_HOLESAIL
|
|
t.ok(isHolesailEnabled(), 'default enabled')
|
|
process.env.ENABLE_HOLESAIL = '1'
|
|
t.ok(isHolesailEnabled())
|
|
process.env.ENABLE_HOLESAIL = 'true'
|
|
t.ok(isHolesailEnabled())
|
|
process.env.ENABLE_HOLESAIL = '0'
|
|
t.absent(isHolesailEnabled())
|
|
process.env.ENABLE_HOLESAIL = 'false'
|
|
t.absent(isHolesailEnabled())
|
|
process.env.ENABLE_HOLESAIL = 'off'
|
|
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 holesail enabled (default)', 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)
|
|
})
|
|
|
|
test('createTunnel is idempotent for same host:port (no duplicate)', 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-dup-${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()
|
|
})
|
|
|
|
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 } = await import(
|
|
'../server/services/holesail-tunnels.js'
|
|
)
|
|
const a = await createTunnel({ name: 'dup-a', host: '127.0.0.1', port, secure: true })
|
|
t.absent(a.existing)
|
|
const b = await createTunnel({ name: 'dup-b', host: '127.0.0.1', port, secure: true })
|
|
t.ok(b.existing, 'second create reuses existing')
|
|
t.is(b.id, a.id)
|
|
t.is(b.url, a.url)
|
|
t.is(list().length, 1, 'only one live tunnel')
|
|
|
|
// Concurrent creates coalesce
|
|
const c1 = createTunnel({ name: 'race-1', host: 'localhost', port, secure: true })
|
|
const c2 = createTunnel({ name: 'race-2', host: '127.0.0.1', port, secure: true })
|
|
const [r1, r2] = await Promise.all([c1, c2])
|
|
t.is(r1.id, a.id)
|
|
t.is(r2.id, a.id)
|
|
t.is(list().length, 1)
|
|
|
|
await closeTunnel(a.id)
|
|
})
|