refactor(test): use container-based postgres_tls for TLS SQL tests (#26518)

## Summary
- Refactors `tls-sql.test.ts` to use `describeWithContainer` with a
local Docker container instead of external Neon secrets
- Updates `postgres_tls` service to build from Dockerfile (fixes SSL key
permission issues)
- Fixes pg_hba.conf to allow local socket connections for init scripts

## Test plan
- [x] Verified tests pass locally with `bun bd test
test/js/sql/tls-sql.test.ts` (30 tests pass)
- [ ] CI passes on x64 Linux (arm64 Docker tests are currently disabled)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
robobun
2026-01-27 23:32:39 -08:00
committed by GitHub
parent c9ebb17921
commit a394063a7d
4 changed files with 243 additions and 280 deletions

View File

@@ -21,20 +21,17 @@ services:
start_period: 5s
postgres_tls:
image: postgres:15
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: postgres
volumes:
- ./init-scripts/postgres:/docker-entrypoint-initdb.d:ro
- ../js/sql/docker-tls/server.crt:/etc/postgresql/ssl/server.crt:ro
- ../js/sql/docker-tls/server.key:/etc/postgresql/ssl/server.key:ro
build:
context: ../js/sql/docker-tls
dockerfile: Dockerfile
image: bun-postgres-tls:local
ports:
- target: 5432
published: 0
protocol: tcp
command: >
postgres
-c hba_file=/etc/postgresql/pg_hba.conf
-c ssl=on
-c ssl_cert_file=/etc/postgresql/ssl/server.crt
-c ssl_key_file=/etc/postgresql/ssl/server.key
@@ -47,7 +44,8 @@ services:
interval: 1h # Effectively disable after startup
timeout: 5s
retries: 30
start_period: 5s
start_period: 60s
start_interval: 1s
postgres_auth:
image: postgres:15

View File

@@ -109,8 +109,8 @@ class DockerComposeHelper {
return;
}
// Build the service if needed (for services like mysql_tls that need building)
if (service === "mysql_tls" || service === "redis_unified") {
// Build the service if needed (for services like mysql_tls, postgres_tls that need building)
if (service === "mysql_tls" || service === "redis_unified" || service === "postgres_tls") {
const buildResult = await this.exec(["build", service]);
if (buildResult.exitCode !== 0) {
throw new Error(`Failed to build service ${service}: ${buildResult.stderr}`);

View File

@@ -51,16 +51,20 @@ RUN chmod +x /docker-entrypoint-initdb.d/init-users-db.sh
# Create pg_hba.conf with SSL requirements
RUN mkdir -p /etc/postgresql && touch /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 127.0.0.1/32 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram 127.0.0.1/32 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 ::1/128 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram ::1/128 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "# Allow local socket connections for init scripts" >> /etc/postgresql/pg_hba.conf && \
echo "local all postgres trust" >> /etc/postgresql/pg_hba.conf && \
echo "local all all trust" >> /etc/postgresql/pg_hba.conf && \
echo "# Remote TLS connections" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 0.0.0.0/0 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram 0.0.0.0/0 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 ::/0 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram ::/0 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "host all all all reject" >> /etc/postgresql/pg_hba.conf
# Configure PostgreSQL for SSL

View File

@@ -1,280 +1,241 @@
import { postgres, randomUUIDv7, SQL, sql } from "bun";
import { SQL, randomUUIDv7 } from "bun";
import { describe, expect, test } from "bun:test";
import { getSecret } from "harness";
import { describeWithContainer, isDockerEnabled } from "harness";
const TLS_POSTGRES_DATABASE_URL = getSecret("TLS_POSTGRES_DATABASE_URL");
const PG_TRANSACTION_POOL_SUPABASE_URL = getSecret("PG_TRANSACTION_POOL_SUPABASE_URL");
for (const options of [
{
url: TLS_POSTGRES_DATABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: true,
transactionPool: false,
},
{
url: PG_TRANSACTION_POOL_SUPABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: false,
transactionPool: true,
},
{
url: TLS_POSTGRES_DATABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: false,
transactionPool: false,
},
] satisfies (Bun.SQL.Options & { transactionPool?: boolean })[]) {
if (options.url === undefined) {
console.log("SKIPPING TEST", JSON.stringify(options), "BECAUSE MISSING THE URL SECRET");
continue;
}
describe.concurrent(
`${options.transactionPool ? "Transaction Pooling" : `Prepared Statements (${options.prepare ? "on" : "off"})`}`,
() => {
test("default sql", async () => {
expect(sql.reserve).toBeDefined();
expect(sql.options).toBeDefined();
expect(sql[Symbol.asyncDispose]).toBeDefined();
expect(sql.begin).toBeDefined();
expect(sql.beginDistributed).toBeDefined();
expect(sql.distributed).toBeDefined();
expect(sql.unsafe).toBeDefined();
expect(sql.end).toBeDefined();
expect(sql.close).toBeDefined();
expect(sql.transaction).toBeDefined();
expect(sql.distributed).toBeDefined();
expect(sql.unsafe).toBeDefined();
expect(sql.commitDistributed).toBeDefined();
expect(sql.rollbackDistributed).toBeDefined();
});
test("default postgres", async () => {
expect(postgres.reserve).toBeDefined();
expect(postgres.options).toBeDefined();
expect(postgres[Symbol.asyncDispose]).toBeDefined();
expect(postgres.begin).toBeDefined();
expect(postgres.beginDistributed).toBeDefined();
expect(postgres.distributed).toBeDefined();
expect(postgres.unsafe).toBeDefined();
expect(postgres.end).toBeDefined();
expect(postgres.close).toBeDefined();
expect(postgres.transaction).toBeDefined();
expect(postgres.distributed).toBeDefined();
expect(postgres.unsafe).toBeDefined();
expect(postgres.commitDistributed).toBeDefined();
expect(postgres.rollbackDistributed).toBeDefined();
});
test("tls (explicit)", async () => {
await using sql = new SQL(options);
const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`;
expect(one).toBe(1);
expect(two).toBe("2");
await sql.close();
});
test("Throws on illegal transactions", async () => {
await using sql = new SQL({ ...options, max: 2 });
const error = await sql`BEGIN`.catch(e => e);
expect(error).toBeInstanceOf(SQL.SQLError);
expect(error).toBeInstanceOf(SQL.PostgresError);
return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
});
test.skipIf(options.transactionPool)("Transaction throws", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(e => e.errno),
).toBe("22P02");
});
test.skipIf(options.transactionPool)("Transaction rolls back", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(() => {
/* ignore */
if (!isDockerEnabled()) {
test.skip("skipping TLS SQL tests - Docker is not available", () => {});
} else {
describeWithContainer(
"PostgreSQL TLS",
{
image: "postgres_tls",
},
container => {
// Test with prepared statements on and off
for (const prepare of [true, false]) {
describe(`prepared: ${prepare}`, () => {
const getOptions = (): Bun.SQL.Options => ({
url: `postgres://postgres@${container.host}:${container.port}/bun_sql_test`,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare,
});
expect((await sql`select a from ${sql(random_name)}`).count).toBe(0);
});
test("tls (explicit)", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`;
expect(one).toBe(1);
expect(two).toBe("2");
});
test.skipIf(options.transactionPool)("Transaction throws on uncaught savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(err => err.message),
).toBe("fail");
});
test("Throws on illegal transactions", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const error = await sql`BEGIN`.catch(e => e);
expect(error).toBeInstanceOf(SQL.SQLError);
expect(error).toBeInstanceOf(SQL.PostgresError);
return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
});
test.skipIf(options.transactionPool)("Transaction throws on uncaught named savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint("watpoint", async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(() => "fail"),
).toBe("fail");
});
test("Transaction throws", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(e => e.errno),
).toBe("22P02");
});
test("Transaction rolls back", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
test("Transaction succeeds on caught savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
try {
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
expect((await sql`select a from ${sql(random_name)}`).count).toBe(0);
});
expect((await sql`select count(1) from ${sql(random_name)}`)[0].count).toBe(2n);
} finally {
await sql`DROP TABLE IF EXISTS ${sql(random_name)}`;
}
});
test("Savepoint returns Result", async () => {
let result;
await using sql = new SQL(options);
await sql.begin(async t => {
result = await t.savepoint(s => s`select 1 as x`);
});
expect(result[0]?.x).toBe(1);
});
test("Transaction throws on uncaught savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(err => err.message),
).toBe("fail");
});
test("Transaction requests are executed implicitly", async () => {
await using sql = new SQL(options);
expect(
(
await sql.begin(sql => [
sql`select set_config('bun_sql.test', 'testing', true)`,
sql`select current_setting('bun_sql.test') as x`,
])
)[1][0].x,
).toBe("testing");
});
test("Transaction throws on uncaught named savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint("watpoint", async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(e => e.message),
).toBe("fail");
});
test("Uncaught transaction request errors bubbles to transaction", async () => {
await using sql = new SQL(options);
expect(
await sql
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.catch(e => e.errno),
).toBe("42703");
});
test("Transaction rejects with rethrown error", async () => {
await using sql = new SQL(options);
expect(
await sql
.begin(async sql => {
try {
await sql`select exception`;
} catch (ex) {
throw new Error("WAT");
}
})
.catch(e => e.message),
).toBe("WAT");
});
test("Parallel transactions", async () => {
await using sql = new SQL({ ...options, max: 2 });
expect(
(await Promise.all([sql.begin(sql => sql`select 1 as count`), sql.begin(sql => sql`select 1 as count`)]))
.map(x => x[0].count)
.join(""),
).toBe("11");
});
test("Many transactions at beginning of connection", async () => {
await using sql = new SQL({ ...options, max: 2 });
const xs = await Promise.all(Array.from({ length: 30 }, () => sql.begin(sql => sql`select 1`)));
return expect(xs.length).toBe(30);
});
test("Transactions array", async () => {
await using sql = new SQL(options);
expect(
(await sql.begin(sql => [sql`select 1 as count`, sql`select 1 as count`])).map(x => x[0].count).join(""),
).toBe("11");
});
test.skipIf(options.transactionPool)("Transaction waits", async () => {
await using sql = new SQL({ ...options, max: 2 });
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
test("Transaction succeeds on caught savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
});
await sql`insert into ${sql(random_name)} values(3)`;
expect((await sql`select count(1) from ${sql(random_name)}`)[0].count).toBe(2n);
});
test("Savepoint returns Result", async () => {
await container.ready;
let result;
await using sql = new SQL(getOptions());
await sql.begin(async t => {
result = await t.savepoint(s => s`select 1 as x`);
});
expect(result[0]?.x).toBe(1);
});
test("Transaction requests are executed implicitly", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
(
await sql.begin(sql => [
sql`select set_config('bun_sql.test', 'testing', true)`,
sql`select current_setting('bun_sql.test') as x`,
])
)[1][0].x,
).toBe("testing");
});
test("Uncaught transaction request errors bubbles to transaction", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
await sql
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.catch(e => e.errno),
).toBe("42703");
});
test("Transaction rejects with rethrown error", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
await sql
.begin(async sql => {
try {
await sql`select exception`;
} catch (ex) {
throw new Error("WAT");
}
})
.catch(e => e.message),
).toBe("WAT");
});
test("Parallel transactions", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
expect(
(await Promise.all([sql.begin(sql => sql`select 1 as count`), sql.begin(sql => sql`select 1 as count`)]))
.map(x => x[0].count)
.join(""),
).toBe("11");
});
test("Many transactions at beginning of connection", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const xs = await Promise.all(Array.from({ length: 30 }, () => sql.begin(sql => sql`select 1`)));
return expect(xs.length).toBe(30);
});
test("Transactions array", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
(await sql.begin(sql => [sql`select 1 as count`, sql`select 1 as count`])).map(x => x[0].count).join(""),
).toBe("11");
});
test("Transaction waits", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
});
expect(
(
await Promise.all([
sql.begin(async sql => await sql`select 1 as count`),
sql.begin(async sql => await sql`select 1 as count`),
])
)
.map(x => x[0].count)
.join(""),
).toBe("11");
});
});
expect(
(
await Promise.all([
sql.begin(async sql => await sql`select 1 as count`),
sql.begin(async sql => await sql`select 1 as count`),
])
)
.map(x => x[0].count)
.join(""),
).toBe("11");
});
}
},
);
}