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.
Neon
Local Development with Neon - Neon Guides
Learn how to develop applications locally with Neon
1 Reply
KinglyEndeavors
KinglyEndeavorsOP4w ago
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 };
}
}
Actually, it looks like I was able to figure it out, so for anyone else who also encounters this problem, I'll leave my solution here. Please feel free to provide feedback on how to make it better.
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;
}

Did you find this page helpful?