Testcontainers w/ Neon Proxy
Does anyone know how to set up a 100% local integration tests using
'ghcr.io/timowilhelm/local-neon-http-proxy:main'?
While I do have a means of running integration tests without it, I'd like to have it as close to the real thing as possible for my tests so to get reliable test results.
However, I seem unable to set it up, and I have no idea what I'm doing wrong.
Here's a Neon aarticle I've been using for reference.1 Reply
import { neonConfig, Pool } from '@neondatabase/serverless';
import {
PostgreSqlContainer,
type StartedPostgreSqlContainer as RunningContainer,
StartedPostgreSqlContainer,
} from '@testcontainers/postgresql';
import { pushSchema } from 'drizzle-kit/api';
import { drizzle as drizzleNeon } from 'drizzle-orm/neon-serverless';
import { drizzle } from 'drizzle-orm/node-postgres';
import { PgDatabase } from 'drizzle-orm/pg-core';
import { Client } from 'pg';
import { GenericContainer, type StartedTestContainer, Wait } from 'testcontainers';
import * as ws from 'ws';
import type { DatabaseWsClient } from '../neon-drizzle-engine.database';
const SECONDS = 1_000;
type CreatePostgresTestDatabaseReturn =
| {
container: RunningContainer;
error: null;
}
| {
container: null;
error: unknown;
};
type CreateNeonHttpProxyReturn =
| {
container: StartedTestContainer;
error: null;
}
| {
container: null;
error: unknown;
};
export async function createNeonHttpProxy(
postgresConnectionURL: string
): Promise<CreateNeonHttpProxyReturn> {
try {
const neonHttpProxyContainer = await new GenericContainer(
'ghcr.io/timowilhelm/local-neon-http-proxy:main'
)
.withEnvironment({ PG_CONNECTION_STRING: postgresConnectionURL })
.withWaitStrategy(Wait.forLogMessage('Starting wss on'))
.start();
return { container: neonHttpProxyContainer, error: null };
} catch (error) {
return { container: null, error };
}
}
import { neonConfig, Pool } from '@neondatabase/serverless';
import {
PostgreSqlContainer,
type StartedPostgreSqlContainer as RunningContainer,
StartedPostgreSqlContainer,
} from '@testcontainers/postgresql';
import { pushSchema } from 'drizzle-kit/api';
import { drizzle as drizzleNeon } from 'drizzle-orm/neon-serverless';
import { drizzle } from 'drizzle-orm/node-postgres';
import { PgDatabase } from 'drizzle-orm/pg-core';
import { Client } from 'pg';
import { GenericContainer, type StartedTestContainer, Wait } from 'testcontainers';
import * as ws from 'ws';
import type { DatabaseWsClient } from '../neon-drizzle-engine.database';
const SECONDS = 1_000;
type CreatePostgresTestDatabaseReturn =
| {
container: RunningContainer;
error: null;
}
| {
container: null;
error: unknown;
};
type CreateNeonHttpProxyReturn =
| {
container: StartedTestContainer;
error: null;
}
| {
container: null;
error: unknown;
};
export async function createNeonHttpProxy(
postgresConnectionURL: string
): Promise<CreateNeonHttpProxyReturn> {
try {
const neonHttpProxyContainer = await new GenericContainer(
'ghcr.io/timowilhelm/local-neon-http-proxy:main'
)
.withEnvironment({ PG_CONNECTION_STRING: postgresConnectionURL })
.withWaitStrategy(Wait.forLogMessage('Starting wss on'))
.start();
return { container: neonHttpProxyContainer, error: null };
} catch (error) {
return { container: null, error };
}
}
export async function createPostgresTestDatabase(): Promise<CreatePostgresTestDatabaseReturn> {
try {
const postgres = await new PostgreSqlContainer('postgres:17-alpine')
.withHealthCheck({
retries: 5,
timeout: 5 * SECONDS,
interval: 10 * SECONDS,
test: ['CMD-SHELL', 'pg_isready -U postgres'],
})
.withWaitStrategy(Wait.forHealthCheck())
.start();
return { container: postgres, error: null };
} catch (error) {
return { container: null, error };
}
}
export async function createPostgresTestDatabase(): Promise<CreatePostgresTestDatabaseReturn> {
try {
const postgres = await new PostgreSqlContainer('postgres:17-alpine')
.withHealthCheck({
retries: 5,
timeout: 5 * SECONDS,
interval: 10 * SECONDS,
test: ['CMD-SHELL', 'pg_isready -U postgres'],
})
.withWaitStrategy(Wait.forHealthCheck())
.start();
return { container: postgres, error: null };
} catch (error) {
return { container: null, error };
}
}
export function useTestNeonDatabase(schema: Record<string, unknown>) {
const MAX_TIMEOUT_MS = 60 * 1000;
const PROXY_HOST = 'db.localtest.me' as const;
let drizzleClient: DatabaseHttpClient;
let proxyContainer: StartedTestContainer;
let databaseContainer: StartedPostgreSqlContainer;
beforeAll(async () => {
const { container, error } = await createPostgresTestDatabase();
if (!container) throw error;
databaseContainer = container;
const postgresConnectionURI = `postgres://${databaseContainer.getUsername()}:${databaseContainer.getPassword()}@${databaseContainer.getIpAddress(
databaseContainer.getNetworkNames()[0]
)}:5432/${databaseContainer.getDatabase()}`;
const { container: httpProxyContainer, error: httpProxyError } = await createNeonHttpProxy(postgresConnectionURI);
if (!httpProxyContainer) throw httpProxyError;
proxyContainer = httpProxyContainer;
const proxyPort = proxyContainer.getMappedPort(4444);
export function useTestNeonDatabase(schema: Record<string, unknown>) {
const MAX_TIMEOUT_MS = 60 * 1000;
const PROXY_HOST = 'db.localtest.me' as const;
let drizzleClient: DatabaseHttpClient;
let proxyContainer: StartedTestContainer;
let databaseContainer: StartedPostgreSqlContainer;
beforeAll(async () => {
const { container, error } = await createPostgresTestDatabase();
if (!container) throw error;
databaseContainer = container;
const postgresConnectionURI = `postgres://${databaseContainer.getUsername()}:${databaseContainer.getPassword()}@${databaseContainer.getIpAddress(
databaseContainer.getNetworkNames()[0]
)}:5432/${databaseContainer.getDatabase()}`;
const { container: httpProxyContainer, error: httpProxyError } = await createNeonHttpProxy(postgresConnectionURI);
if (!httpProxyContainer) throw httpProxyError;
proxyContainer = httpProxyContainer;
const proxyPort = proxyContainer.getMappedPort(4444);
neonConfig.pipelineTLS = false;
neonConfig.pipelineConnect = false;
neonConfig.useSecureWebSocket = false;
neonConfig.webSocketConstructor = ws;
neonConfig.wsProxy = (host) => `${host}:${proxyPort}/v2`;
neonConfig.fetchEndpoint = (host) => `http://${host}:${proxyPort}/sql`;
const proxyConnectionString = `postgres://${databaseContainer.getUsername()}:${databaseContainer.getPassword()}@${proxyHost}:${proxyPort}/${databaseContainer.getDatabase()}`;
const sql = neon(proxyConnectionString);
drizzleClient = drizzleNeon(sql);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await pushSchema(schema, drizzleClient as unknown as PgDatabase<any>);
await result.apply();
}, MAX_TIMEOUT_MS);
afterAll(async () => {
await proxyContainer?.stop();
await databaseContainer?.stop();
});
return () => drizzleClient;
}
neonConfig.pipelineTLS = false;
neonConfig.pipelineConnect = false;
neonConfig.useSecureWebSocket = false;
neonConfig.webSocketConstructor = ws;
neonConfig.wsProxy = (host) => `${host}:${proxyPort}/v2`;
neonConfig.fetchEndpoint = (host) => `http://${host}:${proxyPort}/sql`;
const proxyConnectionString = `postgres://${databaseContainer.getUsername()}:${databaseContainer.getPassword()}@${proxyHost}:${proxyPort}/${databaseContainer.getDatabase()}`;
const sql = neon(proxyConnectionString);
drizzleClient = drizzleNeon(sql);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await pushSchema(schema, drizzleClient as unknown as PgDatabase<any>);
await result.apply();
}, MAX_TIMEOUT_MS);
afterAll(async () => {
await proxyContainer?.stop();
await databaseContainer?.stop();
});
return () => drizzleClient;
}