test: postgres integration via testcontainers

This commit is contained in:
smartass
2026-06-12 00:25:30 +05:00
parent 858ed17d6b
commit d5f9eafb35
+172
View File
@@ -0,0 +1,172 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { Registry } from '../../src/config/registry.js'
import { createRegistry } from '../../src/config/registry.js'
import type { Manager } from '../../src/db/manager.js'
import { createManager } from '../../src/db/manager.js'
describe('postgres integration', () => {
let container: StartedPostgreSqlContainer
let dir: string
let registry: Registry
let manager: Manager
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:17-alpine').start()
dir = mkdtempSync(join(tmpdir(), 'dbmole-int-pg-'))
registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} })
manager = createManager(registry)
registry.add({
name: 'pg',
type: 'postgres',
host: container.getHost(),
port: container.getPort(),
user: container.getUsername(),
password: container.getPassword(),
database: container.getDatabase(),
readonly: false
})
registry.add({
name: 'pg-ro',
type: 'postgres',
host: container.getHost(),
port: container.getPort(),
user: container.getUsername(),
password: container.getPassword(),
database: container.getDatabase(),
readonly: true
})
})
afterAll(async () => {
await manager.disposeAll()
await container.stop()
rmSync(dir, { recursive: true, force: true })
})
it('runs DDL, DML and SELECT with params', async () => {
const { driver } = await manager.get('pg')
await driver.query({
sql: 'CREATE TABLE users (id serial PRIMARY KEY, name text NOT NULL)',
rowLimit: 10
})
const insert = await driver.query({
sql: "INSERT INTO users (name) VALUES ('ada'), ('bob')",
rowLimit: 10
})
expect(insert.rowCount).toBe(2)
const select = await driver.query({
sql: 'SELECT name FROM users WHERE id = $1',
params: [1],
rowLimit: 10
})
expect(select.columns).toEqual(['name'])
expect(select.rows).toEqual([['ada']])
})
it('truncates SELECT results at rowLimit', async () => {
const { driver } = await manager.get('pg')
const result = await driver.query({
sql: 'SELECT generate_series(1, 10)',
rowLimit: 3
})
expect(result.rows).toHaveLength(3)
expect(result.truncated).toBe(true)
})
it('rejects multi-statement and session-level sql', async () => {
const { driver } = await manager.get('pg')
await expect(driver.query({ sql: 'SELECT 1; SELECT 2', rowLimit: 10 })).rejects.toThrow(
/one SQL statement/
)
await expect(driver.query({ sql: 'BEGIN', rowLimit: 10 })).rejects.toThrow(/session-level/)
})
it('lists databases, tables and describes a table', async () => {
const { driver } = await manager.get('pg')
const databases = await driver.listDatabases()
expect(databases.map((d) => d.name)).toContain(container.getDatabase())
const tables = await driver.listTables({})
expect(tables).toContainEqual(expect.objectContaining({ schema: 'public', name: 'users' }))
await driver.query({
sql: `CREATE TABLE orders (
id serial PRIMARY KEY,
user_id integer NOT NULL REFERENCES users(id),
note text
)`,
rowLimit: 10
})
await driver.query({
sql: 'CREATE INDEX orders_user_idx ON orders (user_id)',
rowLimit: 10
})
const description = await driver.describeTable({ table: 'orders' })
expect(description.primaryKey).toEqual(['id'])
expect(description.columns.map((c) => c.name)).toEqual(['id', 'user_id', 'note'])
expect(description.columns[1].nullable).toBe(false)
expect(description.indexes).toContainEqual(
expect.objectContaining({
name: 'orders_user_idx',
columns: ['user_id'],
unique: false
})
)
expect(description.foreignKeys).toContainEqual(
expect.objectContaining({ columns: ['user_id'], referencedTable: 'users' })
)
})
it('describes composite foreign keys with paired columns', async () => {
const { driver } = await manager.get('pg')
await driver.query({
sql: `CREATE TABLE parents (
a integer NOT NULL,
b integer NOT NULL,
PRIMARY KEY (a, b)
)`,
rowLimit: 10
})
await driver.query({
sql: `CREATE TABLE children (
id serial PRIMARY KEY,
pa integer NOT NULL,
pb integer NOT NULL,
FOREIGN KEY (pa, pb) REFERENCES parents (a, b)
)`,
rowLimit: 10
})
const description = await driver.describeTable({ table: 'children' })
expect(description.foreignKeys).toHaveLength(1)
expect(description.foreignKeys[0].columns).toEqual(['pa', 'pb'])
expect(description.foreignKeys[0].referencedTable).toBe('parents')
expect(description.foreignKeys[0].referencedColumns).toEqual(['a', 'b'])
})
it('describe_table of a missing table fails clearly', async () => {
const { driver } = await manager.get('pg')
await expect(driver.describeTable({ table: 'ghost' })).rejects.toThrow(/not found/)
})
it('enforces readonly: INSERT and DDL fail, SELECT works', async () => {
const { driver } = await manager.get('pg-ro')
const select = await driver.query({ sql: 'SELECT count(*) FROM users', rowLimit: 10 })
expect(select.rows[0][0]).toBe('2')
await expect(
driver.query({ sql: "INSERT INTO users (name) VALUES ('mallory')", rowLimit: 10 })
).rejects.toThrow(/read-only/)
await expect(
driver.query({ sql: 'CREATE TABLE hacked (id int)', rowLimit: 10 })
).rejects.toThrow(/read-only/)
})
it('reports server version', async () => {
const { driver } = await manager.get('pg')
expect(await driver.serverVersion()).toMatch(/^17\./)
})
})