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

View File

@@ -109,8 +109,8 @@ class DockerComposeHelper {
return; return;
} }
// Build the service if needed (for services like mysql_tls that need building) // Build the service if needed (for services like mysql_tls, postgres_tls that need building)
if (service === "mysql_tls" || service === "redis_unified") { if (service === "mysql_tls" || service === "redis_unified" || service === "postgres_tls") {
const buildResult = await this.exec(["build", service]); const buildResult = await this.exec(["build", service]);
if (buildResult.exitCode !== 0) { if (buildResult.exitCode !== 0) {
throw new Error(`Failed to build service ${service}: ${buildResult.stderr}`); 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 # Create pg_hba.conf with SSL requirements
RUN mkdir -p /etc/postgresql && touch /etc/postgresql/pg_hba.conf && \ 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 "# Allow local socket connections for init scripts" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \ echo "local all postgres 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 "local all all trust" >> /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 "# Remote TLS connections" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres ::1/128 trust" >> /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 ::1/128 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 ::1/128 md5" >> /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 ::1/128 scram-sha-256" >> /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 replication all 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \ echo "hostssl all postgres ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all ::1/128 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 echo "host all all all reject" >> /etc/postgresql/pg_hba.conf
# Configure PostgreSQL for SSL # 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 { 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"); if (!isDockerEnabled()) {
const PG_TRANSACTION_POOL_SUPABASE_URL = getSecret("PG_TRANSACTION_POOL_SUPABASE_URL"); test.skip("skipping TLS SQL tests - Docker is not available", () => {});
} else {
for (const options of [ describeWithContainer(
{ "PostgreSQL TLS",
url: TLS_POSTGRES_DATABASE_URL, {
tls: true, image: "postgres_tls",
adapter: "postgres", },
max: 1, container => {
bigint: true, // Test with prepared statements on and off
prepare: true, for (const prepare of [true, false]) {
transactionPool: false, describe(`prepared: ${prepare}`, () => {
}, const getOptions = (): Bun.SQL.Options => ({
{ url: `postgres://postgres@${container.host}:${container.port}/bun_sql_test`,
url: PG_TRANSACTION_POOL_SUPABASE_URL, tls: true,
tls: true, adapter: "postgres",
adapter: "postgres", max: 1,
max: 1, bigint: true,
bigint: true, prepare,
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 */
}); });
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 () => { test("Throws on illegal transactions", async () => {
await using sql = new SQL(options); await container.ready;
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase(); await using sql = new SQL({ ...getOptions(), max: 2 });
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`; const error = await sql`BEGIN`.catch(e => e);
expect( expect(error).toBeInstanceOf(SQL.SQLError);
await sql expect(error).toBeInstanceOf(SQL.PostgresError);
.begin(async sql => { return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
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.skipIf(options.transactionPool)("Transaction throws on uncaught named savepoint", async () => { test("Transaction throws", async () => {
await using sql = new SQL(options); await container.ready;
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase(); await using sql = new SQL(getOptions());
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`; const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
expect(
await sql await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
.begin(async sql => { expect(
await sql`insert into ${sql(random_name)} values(1)`; await sql
await sql.savepoint("watpoint", async sql => { .begin(async sql => {
await sql`insert into ${sql(random_name)} values(2)`; await sql`insert into ${sql(random_name)} values(1)`;
throw new Error("fail"); await sql`insert into ${sql(random_name)} values('hej')`;
}); })
}) .catch(e => e.errno),
.catch(() => "fail"), ).toBe("22P02");
).toBe("fail"); });
});
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 await sql
.savepoint(async sql => { .begin(async sql => {
await sql`insert into ${sql(random_name)} values(2)`; await sql`insert into ${sql(random_name)} values(1)`;
throw new Error("please rollback"); await sql`insert into ${sql(random_name)} values('hej')`;
}) })
.catch(() => { .catch(() => {
/* ignore */ /* 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 () => { test("Transaction throws on uncaught savepoint", async () => {
let result; await container.ready;
await using sql = new SQL(options); await using sql = new SQL(getOptions());
await sql.begin(async t => { const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
result = await t.savepoint(s => s`select 1 as x`); await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
}); expect(
expect(result[0]?.x).toBe(1); 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 () => { test("Transaction throws on uncaught named savepoint", async () => {
await using sql = new SQL(options); await container.ready;
expect( await using sql = new SQL(getOptions());
( const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql.begin(sql => [ await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
sql`select set_config('bun_sql.test', 'testing', true)`, expect(
sql`select current_setting('bun_sql.test') as x`, await sql
]) .begin(async sql => {
)[1][0].x, await sql`insert into ${sql(random_name)} values(1)`;
).toBe("testing"); 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 () => { test("Transaction succeeds on caught savepoint", async () => {
await using sql = new SQL(options); await container.ready;
expect( await using sql = new SQL(getOptions());
await sql const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`]) await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
.catch(e => e.errno), await sql.begin(async sql => {
).toBe("42703"); await sql`insert into ${sql(random_name)} values(1)`;
}); await sql
.savepoint(async sql => {
test("Transaction rejects with rethrown error", async () => { await sql`insert into ${sql(random_name)} values(2)`;
await using sql = new SQL(options); throw new Error("please rollback");
expect( })
await sql .catch(() => {
.begin(async sql => { /* ignore */
try { });
await sql`select exception`; await sql`insert into ${sql(random_name)} values(3)`;
} 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 */
}); });
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");
});
}, },
); );
} }