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) })