Files
dbmole-mcp/test/integration/tunnel.test.ts
T

105 lines
3.8 KiB
TypeScript

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\./)
})
})