Skip to main content
Module

x/postgres/tests/connection_test.ts

PostgreSQL driver for Deno
Extremely Popular
Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
import { assertEquals, assertRejects, copyStream, joinPath,} from "./test_deps.ts";import { getClearConfiguration, getClearSocketConfiguration, getMainConfiguration, getMd5Configuration, getMd5SocketConfiguration, getScramConfiguration, getScramSocketConfiguration, getTlsOnlyConfiguration,} from "./config.ts";import { Client, ConnectionError, PostgresError } from "../mod.ts";import { getSocketName } from "../utils/utils.ts";
function createProxy( target: Deno.Listener, source: { hostname: string; port: number },): { aborter: AbortController; proxy: Promise<void> } { const aborter = new AbortController();
const proxy = (async () => { for await (const conn of target) { let aborted = false;
const outbound = await Deno.connect({ hostname: source.hostname, port: source.port, }); aborter.signal.addEventListener("abort", () => { conn.close(); outbound.close(); aborted = true; }); await Promise.all([ copyStream(conn, outbound), copyStream(outbound, conn), ]).catch(() => {});
if (!aborted) { conn.close(); outbound.close(); } } })();
return { aborter, proxy };}
function getRandomString() { return Math.random().toString(36).substring(7);}
Deno.test("Clear password authentication (unencrypted)", async () => { const client = new Client(getClearConfiguration(false)); await client.connect();
try { assertEquals(client.session.tls, false); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("Clear password authentication (tls)", async () => { const client = new Client(getClearConfiguration(true)); await client.connect();
try { assertEquals(client.session.tls, true); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("Clear password authentication (socket)", async () => { const client = new Client(getClearSocketConfiguration()); await client.connect();
try { assertEquals(client.session.tls, undefined); assertEquals(client.session.transport, "socket"); } finally { await client.end(); }});
Deno.test("MD5 authentication (unencrypted)", async () => { const client = new Client(getMd5Configuration(false)); await client.connect();
try { assertEquals(client.session.tls, false); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("MD5 authentication (tls)", async () => { const client = new Client(getMd5Configuration(true)); await client.connect();
try { assertEquals(client.session.tls, true); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("MD5 authentication (socket)", async () => { const client = new Client(getMd5SocketConfiguration()); await client.connect();
try { assertEquals(client.session.tls, undefined); assertEquals(client.session.transport, "socket"); } finally { await client.end(); }});
Deno.test("SCRAM-SHA-256 authentication (unencrypted)", async () => { const client = new Client(getScramConfiguration(false)); await client.connect();
try { assertEquals(client.session.tls, false); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("SCRAM-SHA-256 authentication (tls)", async () => { const client = new Client(getScramConfiguration(true)); await client.connect();
try { assertEquals(client.session.tls, true); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("SCRAM-SHA-256 authentication (socket)", async () => { const client = new Client(getScramSocketConfiguration()); await client.connect();
try { assertEquals(client.session.tls, undefined); assertEquals(client.session.transport, "socket"); } finally { await client.end(); }});
Deno.test("Skips TLS connection when TLS disabled", async () => { const client = new Client({ ...getTlsOnlyConfiguration(), tls: { enabled: false }, });
// Connection will fail due to TLS only user try { await assertRejects( () => client.connect(), PostgresError, "no pg_hba.conf", ); } finally { try { assertEquals(client.session.tls, undefined); assertEquals(client.session.transport, undefined); } finally { await client.end(); } }});
Deno.test("Aborts TLS connection when certificate is untrusted", async () => { // Force TLS but don't provide CA const client = new Client({ ...getTlsOnlyConfiguration(), tls: { enabled: true, enforce: true, }, });
try { await assertRejects( async (): Promise<void> => { await client.connect(); }, Error, "The certificate used to secure the TLS connection is invalid", ); } finally { try { assertEquals(client.session.tls, undefined); assertEquals(client.session.transport, undefined); } finally { await client.end(); } }});
Deno.test("Defaults to unencrypted when certificate is invalid and TLS is not enforced", async () => { // Remove CA, request tls and disable enforce const client = new Client({ ...getMainConfiguration(), tls: { enabled: true, enforce: false }, });
await client.connect();
// Connection will fail due to TLS only user try { assertEquals(client.session.tls, false); assertEquals(client.session.transport, "tcp"); } finally { await client.end(); }});
Deno.test("Handles bad authentication correctly", async function () { const badConnectionData = getMainConfiguration(); badConnectionData.password += getRandomString(); const client = new Client(badConnectionData);
try { await assertRejects( async (): Promise<void> => { await client.connect(); }, PostgresError, "password authentication failed for user", ); } finally { await client.end(); }});
// This test requires current user database connection permissions// on "pg_hba.conf" set to "all"Deno.test("Startup error when database does not exist", async function () { const badConnectionData = getMainConfiguration(); badConnectionData.database += getRandomString(); const client = new Client(badConnectionData);
try { await assertRejects( async (): Promise<void> => { await client.connect(); }, PostgresError, "does not exist", ); } finally { await client.end(); }});
Deno.test("Exposes session PID", async () => { const client = new Client(getMainConfiguration()); await client.connect();
try { const { rows } = await client.queryObject<{ pid: number }>( "SELECT PG_BACKEND_PID() AS PID", ); assertEquals(client.session.pid, rows[0].pid); } finally { await client.end();
assertEquals( client.session.pid, undefined, "PID was not cleared after disconnection", ); }});
Deno.test("Exposes session encryption", async () => { const client = new Client(getMainConfiguration()); await client.connect();
try { assertEquals(client.session.tls, true); } finally { await client.end();
assertEquals( client.session.tls, undefined, "TLS was not cleared after disconnection", ); }});
Deno.test("Exposes session transport", async () => { const client = new Client(getMainConfiguration()); await client.connect();
try { assertEquals(client.session.transport, "tcp"); } finally { await client.end();
assertEquals( client.session.transport, undefined, "Transport was not cleared after disconnection", ); }});
Deno.test("Attempts to guess socket route", async () => { await assertRejects( async () => { const mock_socket = await Deno.makeTempFile({ prefix: ".postgres_socket.", });
const client = new Client({ database: "some_database", hostname: mock_socket, host_type: "socket", user: "some_user", }); await client.connect(); }, Deno.errors.ConnectionRefused, undefined, "It doesn't use exact file name when real file provided", );
const path = await Deno.makeTempDir({ prefix: "postgres_socket" }); const port = 1234;
await assertRejects( async () => { const client = new Client({ database: "some_database", hostname: path, host_type: "socket", user: "some_user", port, }); await client.connect(); }, ConnectionError, `Could not open socket in path "${joinPath(path, getSocketName(port))}"`, "It doesn't guess socket location based on port", );});
Deno.test("Closes connection on bad TLS availability verification", async function () { const server = new Worker( new URL("./workers/postgres_server.ts", import.meta.url).href, { type: "module", }, );
// Await for server initialization const initialized = Promise.withResolvers(); server.onmessage = ({ data }) => { if (data !== "initialized") { initialized.reject(`Unexpected message "${data}" received from worker`); } initialized.resolve(null); }; server.postMessage("initialize"); await initialized.promise;
const client = new Client({ database: "none", hostname: "127.0.0.1", port: "8080", user: "none", });
// The server will try to emit a message everytime it receives a connection // For this test we don't need them, so we just discard them server.onmessage = () => {};
let bad_tls_availability_message = false; try { await client.connect(); } catch (e) { if ( e instanceof Error || e.message.startsWith("Could not check if server accepts SSL connections") ) { bad_tls_availability_message = true; } else { // Early fail, if the connection fails for an unexpected error server.terminate(); throw e; } } finally { await client.end(); }
const closed = Promise.withResolvers(); server.onmessage = ({ data }) => { if (data !== "closed") { closed.reject( `Unexpected message "${data}" received from worker`, ); } closed.resolve(null); }; server.postMessage("close"); await closed.promise; server.terminate();
assertEquals(bad_tls_availability_message, true);});
async function mockReconnection(attempts: number) { const server = new Worker( new URL("./workers/postgres_server.ts", import.meta.url).href, { type: "module", }, );
// Await for server initialization const initialized = Promise.withResolvers(); server.onmessage = ({ data }) => { if (data !== "initialized") { initialized.reject(`Unexpected message "${data}" received from worker`); } initialized.resolve(null); }; server.postMessage("initialize"); await initialized.promise;
const client = new Client({ connection: { attempts, }, database: "none", hostname: "127.0.0.1", port: "8080", user: "none", });
let connection_attempts = 0; server.onmessage = ({ data }) => { if (data !== "connection") { closed.reject( `Unexpected message "${data}" received from worker`, ); } connection_attempts++; };
try { await client.connect(); } catch (e) { if ( !(e instanceof Error) || !e.message.startsWith("Could not check if server accepts SSL connections") ) { // Early fail, if the connection fails for an unexpected error server.terminate(); throw e; } } finally { await client.end(); }
const closed = Promise.withResolvers(); server.onmessage = ({ data }) => { if (data !== "closed") { closed.reject( `Unexpected message "${data}" received from worker`, ); } closed.resolve(null); }; server.postMessage("close"); await closed.promise; server.terminate();
// If reconnections are set to zero, it will attempt to connect at least once, but won't // attempt to reconnect assertEquals( connection_attempts, attempts === 0 ? 1 : attempts, `Attempted "${connection_attempts}" reconnections, "${attempts}" expected`, );}
Deno.test("Attempts reconnection on connection startup", async function () { await mockReconnection(5); await mockReconnection(0);});
// This test ensures a failed query that is disconnected after execution but before// status report is only executed one (regression test)Deno.test("Attempts reconnection on disconnection", async function () { const client = new Client({ ...getMainConfiguration(), connection: { attempts: 1, }, }); await client.connect();
try { const test_table = "TEST_DENO_RECONNECTION_1"; const test_value = 1;
await client.queryArray(`DROP TABLE IF EXISTS ${test_table}`); await client.queryArray(`CREATE TABLE ${test_table} (X INT)`);
await assertRejects( () => client.queryArray( `INSERT INTO ${test_table} VALUES (${test_value}); COMMIT; SELECT PG_TERMINATE_BACKEND(${client.session.pid})`, ), ConnectionError, "The session was terminated unexpectedly", ); assertEquals(client.connected, false);
const { rows: result_1 } = await client.queryObject<{ pid: number }>({ text: "SELECT PG_BACKEND_PID() AS PID", fields: ["pid"], }); assertEquals( client.session.pid, result_1[0].pid, "The PID is not reseted after reconnection", );
const { rows: result_2 } = await client.queryObject<{ x: number }>({ text: `SELECT X FROM ${test_table}`, fields: ["x"], }); assertEquals( result_2.length, 1, ); assertEquals( result_2[0].x, test_value, ); } finally { await client.end(); }});
Deno.test("Attempts reconnection on socket disconnection", async () => { const client = new Client(getMd5SocketConfiguration()); await client.connect();
try { await assertRejects( () => client.queryArray`SELECT PG_TERMINATE_BACKEND(${client.session.pid})`, ConnectionError, "The session was terminated unexpectedly", );
const { rows: query_1 } = await client.queryArray`SELECT 1`; assertEquals(query_1, [[1]]); } finally { await client.end(); }});
// TODO// Find a way to unlink the socket to simulate unexpected socket disconnection
Deno.test("Attempts reconnection when connection is lost", async function () { const cfg = getMainConfiguration(); const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const { aborter, proxy } = createProxy(listener, { hostname: cfg.hostname, port: Number(cfg.port), });
const client = new Client({ ...cfg, hostname: "127.0.0.1", port: (listener.addr as Deno.NetAddr).port, tls: { enabled: false, }, });
await client.queryObject("SELECT 1");
// This closes ongoing connections. The original connection is now dead, so // a new connection should be established. aborter.abort();
await assertRejects( () => client.queryObject("SELECT 1"), ConnectionError, "The session was terminated unexpectedly", );
// Make sure the connection was reestablished once the server comes back online await client.queryObject("SELECT 1"); await client.end();
listener.close(); await proxy;});
Deno.test("Doesn't attempt reconnection when attempts are set to zero", async function () { const client = new Client({ ...getMainConfiguration(), connection: { attempts: 0 }, }); await client.connect();
try { await assertRejects(() => client.queryArray`SELECT PG_TERMINATE_BACKEND(${client.session.pid})` ); assertEquals(client.connected, false);
await assertRejects( () => client.queryArray`SELECT 1`, Error, "The client has been disconnected from the database", ); } finally { // End the connection in case the previous assertions failed await client.end(); }});
Deno.test("Options are passed to the database on connection", async () => { // Test for both cases cause we don't know what the default value of geqo is gonna be { const client = new Client({ ...getMainConfiguration(), options: { "geqo": "off", }, });
await client.connect();
try { const { rows: result } = await client.queryObject< { setting: string } >`SELECT SETTING FROM PG_SETTINGS WHERE NAME = 'geqo'`;
assertEquals(result.length, 1); assertEquals(result[0].setting, "off"); } finally { await client.end(); } }
{ const client = new Client({ ...getMainConfiguration(), options: { geqo: "on", }, });
await client.connect();
try { const { rows: result } = await client.queryObject< { setting: string } >`SELECT SETTING FROM PG_SETTINGS WHERE NAME = 'geqo'`;
assertEquals(result.length, 1); assertEquals(result[0].setting, "on"); } finally { await client.end(); } }});