From 90c8860ea74c452e12f9e7f89732eefaec0597ec Mon Sep 17 00:00:00 2001 From: smartass Date: Fri, 12 Jun 2026 00:34:02 +0500 Subject: [PATCH] test: ssh tunnel e2e via sshd container --- test/integration/tunnel.test.ts | 90 +++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/integration/tunnel.test.ts diff --git a/test/integration/tunnel.test.ts b/test/integration/tunnel.test.ts new file mode 100644 index 0000000..f316dc1 --- /dev/null +++ b/test/integration/tunnel.test.ts @@ -0,0 +1,90 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql' +import { + GenericContainer, + Network, + type StartedNetwork, + type StartedTestContainer, + Wait +} from 'testcontainers' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { createRegistry } from '../../src/config/registry.js' +import type { Manager } from '../../src/db/manager.js' +import { createManager } from '../../src/db/manager.js' + +describe('ssh tunnel integration', () => { + let network: StartedNetwork + let postgres: StartedPostgreSqlContainer + let sshd: StartedTestContainer + let dir: string + let manager: Manager + + beforeAll(async () => { + network = await new Network().start() + + postgres = await new PostgreSqlContainer('postgres:17-alpine') + .withNetwork(network) + .withNetworkAliases('db') + .start() + + sshd = await new GenericContainer('lscr.io/linuxserver/openssh-server:latest') + .withNetwork(network) + .withEnvironment({ + PUID: '1000', + PGID: '1000', + USER_NAME: 'tunnel', + USER_PASSWORD: 'tunnelpass', + PASSWORD_ACCESS: 'true', + DOCKER_MODS: 'linuxserver/mods:openssh-server-ssh-tunnel' + }) + .withExposedPorts(2222) + // The image logs 'sshd is listening on port 2222' from its FIRST + // sshd start, BEFORE the init scripts enable password auth + TCP + // forwarding and restart sshd. Connecting in that window yields + // ECONNRESET before the SSH banner. '[ls.io-init] done.' is printed + // only after the full init (and restart) completes, so wait on that. + .withWaitStrategy(Wait.forLogMessage(/\[ls\.io-init\] done\./)) + .start() + + dir = mkdtempSync(join(tmpdir(), 'dbmole-int-ssh-')) + const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) + manager = createManager(registry) + registry.add({ + name: 'pg-tunneled', + type: 'postgres', + host: 'db', + port: 5432, + user: postgres.getUsername(), + password: postgres.getPassword(), + database: postgres.getDatabase(), + readonly: false, + ssh: { + host: sshd.getHost(), + port: sshd.getMappedPort(2222), + user: 'tunnel', + password: 'tunnelpass' + } + }) + }) + + afterAll(async () => { + await manager.disposeAll() + await sshd.stop() + await postgres.stop() + await network.stop() + rmSync(dir, { recursive: true, force: true }) + }) + + it('queries postgres through the ssh tunnel', async () => { + const { driver } = await manager.get('pg-tunneled') + const result = await driver.query({ sql: 'SELECT 1 + 1', rowLimit: 10 }) + expect(result.rows).toEqual([[2]]) + }) + + it('reports server version through the tunnel', async () => { + const { driver } = await manager.get('pg-tunneled') + expect(await driver.serverVersion()).toMatch(/^17\./) + }) +})