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' // Pinned by digest for wait-strategy stability: the wait below depends on this // image's '[ls.io-init] done.' log line. Bump deliberately if the log changes. const SSHD_IMAGE = 'lscr.io/linuxserver/openssh-server@sha256:5b8550a3b703eb4e5efb14a1f491370b7f765febfac5b0b2ed0321cdc74b1476' describe('ssh tunnel integration', () => { let network: StartedNetwork | undefined let postgres: StartedPostgreSqlContainer | undefined let sshd: StartedTestContainer | undefined let dir: string | undefined let manager: Manager | undefined beforeAll(async () => { network = await new Network().start() postgres = await new PostgreSqlContainer('postgres:17-alpine') .withNetwork(network) .withNetworkAliases('db') .start() sshd = await new GenericContainer(SSHD_IMAGE) .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().catch(() => {}) await sshd?.stop().catch(() => {}) await postgres?.stop().catch(() => {}) await network?.stop().catch(() => {}) if (dir) { rmSync(dir, { recursive: true, force: true }) } }) const db = (): Manager => { if (!manager) { throw new Error('setup failed: manager not initialized') } return manager } it('queries postgres through the ssh tunnel', async () => { const { driver } = await db().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 db().get('pg-tunneled') expect(await driver.serverVersion()).toMatch(/^17\./) }) })