diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index 716b531e88..27197ebb54 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -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 diff --git a/test/docker/index.ts b/test/docker/index.ts index 0920d42432..5c87bf07be 100644 --- a/test/docker/index.ts +++ b/test/docker/index.ts @@ -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}`); diff --git a/test/js/sql/docker-tls/Dockerfile b/test/js/sql/docker-tls/Dockerfile index b24507c910..f9b3216335 100644 --- a/test/js/sql/docker-tls/Dockerfile +++ b/test/js/sql/docker-tls/Dockerfile @@ -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 diff --git a/test/js/sql/tls-sql.test.ts b/test/js/sql/tls-sql.test.ts index 39f30e5cd0..155e632a91 100644 --- a/test/js/sql/tls-sql.test.ts +++ b/test/js/sql/tls-sql.test.ts @@ -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"); - }); + } }, ); }