From d7d2709f8838dafbb00d36a81d1d36920fae2562 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Aug 2026 15:40:11 +0200 Subject: [PATCH] Migrated the Driver and TLS enumerations in the Persistence package to use PostgreSQL instead. --- .../Sources/Public/Enumerations/Driver.swift | 4 +- .../Sources/Public/Enumerations/TLS.swift | 28 ++--- .../Cases/Public/Enumerations/TLSTests.swift | 102 ++++++++++++------ 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/Packages/Persistence/Sources/Public/Enumerations/Driver.swift b/Packages/Persistence/Sources/Public/Enumerations/Driver.swift index 60f053b..a86c51b 100644 --- a/Packages/Persistence/Sources/Public/Enumerations/Driver.swift +++ b/Packages/Persistence/Sources/Public/Enumerations/Driver.swift @@ -4,10 +4,10 @@ /// that default and stay agnostic of which backend is in use. public enum Driver: Sendable { - /// A MySQL/MariaDB server, reached with the given connection parameters. + /// A PostgreSQL server, reached with the given connection parameters. /// /// - Parameter configuration: the host, credentials, TLS posture, and pooling limits the connection is opened with. - case mysql(Configuration) + case postgres(Configuration) /// An ephemeral, in-process SQLite database held entirely in memory. /// diff --git a/Packages/Persistence/Sources/Public/Enumerations/TLS.swift b/Packages/Persistence/Sources/Public/Enumerations/TLS.swift index be6f492..faf5777 100644 --- a/Packages/Persistence/Sources/Public/Enumerations/TLS.swift +++ b/Packages/Persistence/Sources/Public/Enumerations/TLS.swift @@ -1,9 +1,10 @@ import NIOSSL +import PostgresNIO /// The TLS posture used when connecting to the database. /// -/// The executable derives a posture from its `database.tls` configuration and passes it along as part of ``Configuration``; the MySQL driver -/// receives the resulting `TLSConfiguration` through ``tlsConfiguration``. +/// The executable derives a posture from its `database.tls` configuration and passes it along as part of ``Configuration``; the PostgreSQL driver +/// receives the resulting connection TLS mode through ``postgresTLS()``. public enum TLS: Sendable { /// Connect without TLS, in plaintext. @@ -13,28 +14,27 @@ public enum TLS: Sendable { case prefer /// Connect only over TLS, refusing the connection when the server offers none. - /// - /// - Important: the refusal is not yet enforced — until it is, `require` behaves like ``prefer`` and silently falls back to plaintext when the - /// server offers no TLS. case require } -// MARK: - Properties +// MARK: - Methods extension TLS { - /// The NIO TLS configuration passed to the MySQL driver for this posture. + /// The connection TLS mode passed to the PostgreSQL driver for this posture. /// - /// Returns `nil` for ``off`` (connect in plaintext) and the default client configuration for ``prefer`` and ``require``. + /// Returns `.disable` for ``off`` (connect in plaintext) and the default client configuration for ``prefer`` and ``require``. The driver enforces + /// both semantics natively: `prefer` upgrades to TLS only when the server advertises support and continues in plaintext otherwise, while `require` + /// refuses the connection when the server offers no TLS. /// - /// - Note: the driver gives a supplied configuration ``prefer`` semantics natively — it upgrades to TLS only when the server advertises support, - /// and continues in plaintext otherwise — so `prefer` is fully enforced. `require` maps to the same configuration and therefore currently - /// behaves like ``prefer``: the refusal when the server offers no TLS is not yet enforced. - var tlsConfiguration: TLSConfiguration? { + /// - Throws: an error when the TLS context cannot be built from the default client configuration. + /// - Returns: the connection TLS mode for this posture. + func postgresTLS() throws -> PostgresConnection.Configuration.TLS { switch self { - case .off: nil - default: .makeClientConfiguration() + case .off: .disable + case .prefer: .prefer(try NIOSSLContext(configuration: .makeClientConfiguration())) + case .require: .require(try NIOSSLContext(configuration: .makeClientConfiguration())) } } diff --git a/Packages/Persistence/Tests/Cases/Public/Enumerations/TLSTests.swift b/Packages/Persistence/Tests/Cases/Public/Enumerations/TLSTests.swift index 0cd7d4e..37a20c6 100644 --- a/Packages/Persistence/Tests/Cases/Public/Enumerations/TLSTests.swift +++ b/Packages/Persistence/Tests/Cases/Public/Enumerations/TLSTests.swift @@ -1,8 +1,7 @@ import Logging -import MySQLNIO import NIOCore import NIOPosix -import NIOSSL +import PostgresNIO import Testing @testable import Persistence @@ -13,46 +12,81 @@ import Testing ) struct TLSTests { - // MARK: Properties tests + // MARK: Methods tests @Test - func `off has no TLS configuration`() { - #expect(TLS.off.tlsConfiguration == nil) - } - - @Test(arguments: [ - TLS.prefer, - TLS.require - ]) - func `maps to the default client configuration`( - for tls: TLS - ) throws { - let configuration = try #require(tls.tlsConfiguration) - - #expect(configuration.bestEffortEquals(.makeClientConfiguration())) - } - - @Test - func `prefer falls back to plaintext when the server offers no TLS`() async throws { - // The fake server never advertises `CLIENT_SSL`, so this connection can only succeed by downgrading to - // plaintext — pinning the driver behavior the `prefer` posture relies on. - let server = try await PlaintextMySQLServer.start() - let tlsConfiguration = try #require(TLS.prefer.tlsConfiguration) - let connection = try await MySQLConnection.connect( - to: .init(ipAddress: "127.0.0.1", port: server.port), - username: "loud", - database: "loud", - tlsConfiguration: tlsConfiguration, - logger: Logger(label: "test"), - on: MultiThreadedEventLoopGroup.singleton.any() - ).get() + func `off connects in plaintext`() async throws { + // With TLS disabled the client skips the `SSLRequest` and sends its startup message directly, + // which the fake server answers in plaintext. + let server = try await PlaintextPostgresServer.start() + let connection = try await connect(to: server, tls: .off) let isConnected = !connection.isClosed - try await connection.close().get() + try await connection.close() try await server.stop() #expect(isConnected) } + @Test + func `prefer falls back to plaintext when the server offers no TLS`() async throws { + // The fake server refuses the `SSLRequest`, so this connection can only succeed by downgrading + // to plaintext — pinning the driver behavior the `prefer` posture relies on. + let server = try await PlaintextPostgresServer.start() + let connection = try await connect(to: server, tls: .prefer) + + let isConnected = !connection.isClosed + + try await connection.close() + try await server.stop() + + #expect(isConnected) + } + + @Test + func `require refuses the connection when the server offers no TLS`() async throws { + // The fake server refuses the `SSLRequest`, so the driver must fail the connection instead of + // downgrading — pinning the refusal the `require` posture promises. + let server = try await PlaintextPostgresServer.start() + + let error = await #expect(throws: PSQLError.self) { + _ = try await connect(to: server, tls: .require) + } + + try await server.stop() + + #expect(error?.code == .sslUnsupported) + } + +} + +// MARK: - Helpers + +private extension TLSTests { + + /// Opens a connection to the given fake server with the given TLS posture. + /// - Parameters: + /// - server: the fake server to connect to. + /// - tls: the TLS posture to connect with. + /// - Returns: the open connection, to be closed by the caller. + func connect( + to server: PlaintextPostgresServer, + tls: TLS + ) async throws -> PostgresConnection { + try await PostgresConnection.connect( + on: MultiThreadedEventLoopGroup.singleton.any(), + configuration: .init( + host: "127.0.0.1", + port: server.port, + username: "loud", + password: "loud", + database: "loud", + tls: tls.postgresTLS() + ), + id: 1, + logger: Logger(label: "test") + ) + } + }