diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a266291d..6f0751c56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,28 +25,39 @@ jobs: needs: lint services: postgres: - image: ghcr.io/railwayapp-templates/postgres-ssl + image: ghcr.io/railwayapp-templates/postgres-ssl:${{ matrix.postgres }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_HOST_AUTH_METHOD: 'md5' POSTGRES_DB: ci_db_test + # PostgreSQL 18's official image defaults PGDATA to a versioned + # subdirectory (/var/lib/postgresql/18/docker), but the + # railwayapp-templates/postgres-ssl entrypoint requires PGDATA to + # start with /var/lib/postgresql/data so we pin it explicitly. This is + # also the default for the older images, so it is a no-op there. + PGDATA: /var/lib/postgresql/data ports: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: fail-fast: false matrix: - node: - - '16' - - '18' - - '20' - - '22' - - '24' - - '26' - os: - - ubuntu-latest - name: Node.js ${{ matrix.node }} + include: + # Historical Node.js versions tested against a single PostgreSQL version + - { node: '16', postgres: &stable_postgres '18' } + - { node: '18', postgres: *stable_postgres } + - { node: '20', postgres: *stable_postgres } + - { node: '22', postgres: *stable_postgres } + - { node: '24', postgres: *stable_postgres } + # Latest Node.js version tested against multiple PostgreSQL versions + - { node: &latest_node '26', postgres: '13' } + - { node: *latest_node, postgres: '14' } + - { node: *latest_node, postgres: '15' } + - { node: *latest_node, postgres: '16' } + - { node: *latest_node, postgres: '17' } + - { node: *latest_node, postgres: '18' } + name: Node.js ${{ matrix.node }} x PostgreSQL ${{ matrix.postgres }} runs-on: ubuntu-latest env: PGUSER: postgres diff --git a/packages/pg/test/integration/client/ssl-tests.js b/packages/pg/test/integration/client/ssl-tests.js index 33919cdf8..b5b32c5ba 100644 --- a/packages/pg/test/integration/client/ssl-tests.js +++ b/packages/pg/test/integration/client/ssl-tests.js @@ -22,3 +22,58 @@ suite.test('can connect with ssl', function (done) { }) ) }) + +async function getServerVersionNum() { + const client = new helper.pg.Client(helper.config) + await client.connect() + try { + const { + rows: [row], + } = await client.query('SHOW server_version_num') + return parseInt(row.server_version_num, 10) + } finally { + await client.end() + } +} + +// The native client forwards sslnegotiation=direct to libpq, whose support +// for direct SSL depends on the linked libpq version (17+) rather than on +// this library. It also does not expose the underlying TLS socket, so the +// direct-negotiation check below is impossible. So we only test the pure-JS client. +if (!helper.args.native) { + suite.test('can connect with direct SSL negotiation', async () => { + // Direct SSL negotiation (sslnegotiation=direct) is only supported by + // PostgreSQL 17 and newer servers. Probe the server version first and skip + // on older servers rather than failing the test. + const serverVersionNum = await getServerVersionNum() + if (serverVersionNum < 170000) { + console.log(`(skipped: direct SSL requires PostgreSQL 17+, server_version_num=${serverVersionNum}) `) + return + } + + const config = { + ...helper.config, + ssl: { rejectUnauthorized: false }, + sslnegotiation: 'direct', + } + const client = new helper.pg.Client(config) + await client.connect() + const { rows } = await client.query('SELECT NOW()') + assert.strictEqual(rows.length, 1) + + // Verify the connection actually used direct SSL negotiation rather than + // silently falling back to the traditional SSLRequest handshake. pg only + // sends the 'postgresql' ALPN protocol on a direct SSL handshake (see + // Connection#upgradeToSSL), and a PostgreSQL 17+ server echoes it back, so + // its presence on the negotiated TLS socket confirms direct negotiation. + const tlsSocket = client.connection.stream + assert.ok(tlsSocket.encrypted, 'expected the connection to be upgraded to a TLS socket') + assert.strictEqual( + tlsSocket.alpnProtocol, + 'postgresql', + 'expected direct SSL negotiation to select the "postgresql" ALPN protocol' + ) + + await client.end() + }) +}