Initial commit.

This commit is contained in:
2026-08-19 23:19:08 +02:00
commit 22e737d9c2
153 changed files with 11680 additions and 0 deletions
@@ -0,0 +1,92 @@
import Logging
import NIOCore
import NIOPosix
import PostgresNIO
import Testing
@testable import Persistence
@Suite(
"TLS enumeration",
.tags(.enumeration)
)
struct TLSTests {
// MARK: Methods tests
@Test
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()
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")
)
}
}
@@ -0,0 +1,121 @@
import FluentKit
import HummingbirdFluent
import Logging
import Testing
@testable import Persistence
@Suite(
"Probe method",
.tags(.method)
)
struct ProbeTests {
// MARK: Methods tests
@Test
func `reports a reachable database`() async throws {
let service = try Service(
driver: .inMemory,
logger: Logger(label: "test")
)
let fluent = service()
let probe = Probe(fluent: fluent)
let isReachable = await probe()
try await fluent.shutdown()
#expect(isReachable)
}
@Test
func `reports an unreachable database`() async throws {
// Port 1 on the loopback interface has nothing listening, so the connection is refused
// immediately instead of timing out.
let service = try Service(
driver: .postgres(
.init(
host: "127.0.0.1",
port: 1,
name: "unreachable",
username: "nobody",
password: "nothing",
tls: .off,
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
)
let fluent = service()
let probe = Probe(fluent: fluent)
let isReachable = await probe()
try await fluent.shutdown()
#expect(!isReachable)
}
@Test
func `reports a hanging database as unreachable within its timeout`() async throws {
// The silent server accepts the TCP connection and never answers, so the probe's query can only
// ever be resolved by its deadline without one, it would wait out the driver's own connect
// timeout (10 seconds) instead.
let server = try await SilentPostgresServer.start()
let service = try Service(
driver: .postgres(
.init(
host: "127.0.0.1",
port: server.port,
name: "hanging",
username: "nobody",
password: "nothing",
tls: .off,
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
)
let fluent = service()
let probe = Probe(
fluent: fluent,
timeout: .milliseconds(100)
)
let clock = ContinuousClock()
let start = clock.now
let isReachable = await probe()
let elapsed = clock.now - start
try await fluent.shutdown()
try await server.stop()
#expect(!isReachable)
// Well past the 100-millisecond deadline to absorb scheduling noise, yet far below the driver's
// 10-second connect timeout only the deadline can answer this fast.
#expect(elapsed < .seconds(5))
}
@Test
func `reports a default database that is not an SQL database`() async throws {
let fluent = Fluent(logger: Logger(label: "test"))
fluent.databases.use(
.init(make: { NotSQLConfiguration() }),
as: .init(string: "not-sql"),
isDefault: true
)
let probe = Probe(fluent: fluent)
let isReachable = await probe()
try await fluent.shutdown()
#expect(!isReachable)
}
}
@@ -0,0 +1,173 @@
import Foundation
import Logging
import SQLKit
import Testing
@testable import Persistence
@Suite(
"Service method",
.tags(.method)
)
struct ServiceTests {
// MARK: Methods tests
@Test
func `registers an SQLite database as the default for the in-memory driver`() async throws {
let service = try Service(
driver: .inMemory,
logger: Logger(label: "test")
)
let fluent = service()
let database = fluent.db() as? any SQLDatabase
try await fluent.shutdown()
let dialect = try #require(database?.dialect)
#expect(dialect.name == "sqlite")
}
@Test
func `registers a PostgreSQL database as the default for the postgres driver`() async throws {
// Resolving the default database opens no connection pooling is lazy so no server
// needs to be listening on the configured host and port.
let service = try Service(
driver: .postgres(
.init(
host: "127.0.0.1",
port: 5432,
name: "site",
username: "site",
password: "site",
tls: .off,
maxConnectionsPerEventLoop: 1,
poolTimeout: .seconds(10)
)
),
logger: Logger(label: "test")
)
let fluent = service()
let database = fluent.db() as? any SQLDatabase
try await fluent.shutdown()
let dialect = try #require(database?.dialect)
#expect(dialect.name == "postgresql")
}
@Test
func `builds a usable in-memory database`() async throws {
let service = try Service(
driver: .inMemory,
logger: Logger(label: "test")
)
let fluent = service()
do {
let database = try #require(fluent.db() as? any SQLDatabase)
try await database.raw("SELECT 1").run()
} catch {
try? await fluent.shutdown()
throw error
}
try await fluent.shutdown()
}
@Test("in-memory: migrate, insert, read back")
func inMemoryRoundTrip() async throws {
try await roundTrip(driver: .inMemory)
}
@Test(
"postgres: migrate, insert, read back",
.enabled(if: postgresDriver != nil)
)
func postgresRoundTrip() async throws {
try await roundTrip(
driver: postgresDriver!,
revertAfter: true
)
}
}
// MARK: - Helpers
private extension ServiceTests {
/// Migrates, inserts, and reads back a record against the given driver, shutting the pool down after.
///
/// The `Fluent` service normally owns pool shutdown via its `run()` in the service group; outside that,
/// the test must shut it down explicitly even on failure or the pool asserts on `deinit`.
/// - Parameters:
/// - driver: the persistence backend to exercise.
/// - revertAfter: whether to revert the migrations afterwards; set for a shared database (the
/// in-memory database is discarded on shutdown, so it needs no revert).
func roundTrip(
driver: Persistence.Driver,
revertAfter: Bool = false
) async throws {
let prepareDB = PrepareDB()
let service = try Service(
driver: driver,
logger: Logger(label: "test")
)
let fluent = service()
do {
await prepareDB(for: fluent)
try await fluent.migrate()
let repository = ExampleRepository(fluent: fluent)
let created = try await repository.create(name: "site")
#expect(try await repository.all().contains(created))
if revertAfter {
try await fluent.revert()
}
} catch {
try? await fluent.shutdown()
throw error
}
try await fluent.shutdown()
}
}
/// The PostgreSQL driver built from the `POSTGRES_TEST_*` environment variables, or `nil` when the gate
/// variable `POSTGRES_TEST_HOST` is unset in which case the PostgreSQL integration test is skipped, so
/// the suite stays runnable with no database available.
private let postgresDriver: Persistence.Driver? = {
let environment = ProcessInfo.processInfo.environment
guard let host = environment["POSTGRES_TEST_HOST"] else {
return nil
}
return .postgres(
.init(
host: host,
port: environment["POSTGRES_TEST_PORT"].flatMap(Int.init) ?? 5432,
name: environment["POSTGRES_TEST_NAME"] ?? "site",
username: environment["POSTGRES_TEST_USERNAME"] ?? "site",
password: environment["POSTGRES_TEST_PASSWORD"] ?? "site",
tls: .off,
maxConnectionsPerEventLoop: 2,
poolTimeout: .seconds(10)
)
)
}()
@@ -0,0 +1,8 @@
import Testing
extension Tag {
/// Tests exercising an enumeration of the Persistence package.
@Tag static var enumeration: Tag
/// Tests exercising a method of the Persistence package.
@Tag static var method: Tag
}
@@ -0,0 +1,13 @@
import FluentKit
struct NotSQLConfiguration: DatabaseConfiguration {
var middleware: [any AnyModelMiddleware] = []
func makeDriver(
for databases: Databases
) -> any DatabaseDriver {
NotSQLDriver()
}
}
@@ -0,0 +1,44 @@
import FluentKit
/// A Fluent database that is not an `SQLDatabase`, so the probe's downcast fails.
///
/// Every query succeeds, proving the probe reports "not reachable" because of the failed downcast
/// rather than a failing backend.
struct NotSQLDatabase: Database {
let context: DatabaseContext
var inTransaction: Bool { false }
func execute(
query: DatabaseQuery,
onOutput: @escaping @Sendable (any DatabaseOutput) -> Void
) -> EventLoopFuture<Void> {
context.eventLoop.makeSucceededVoidFuture()
}
func execute(
schema: DatabaseSchema
) -> EventLoopFuture<Void> {
context.eventLoop.makeSucceededVoidFuture()
}
func execute(
enum: DatabaseEnum
) -> EventLoopFuture<Void> {
context.eventLoop.makeSucceededVoidFuture()
}
func transaction<T>(
_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>
) -> EventLoopFuture<T> {
closure(self)
}
func withConnection<T>(
_ closure: @escaping @Sendable (any Database) -> EventLoopFuture<T>
) -> EventLoopFuture<T> {
closure(self)
}
}
@@ -0,0 +1,13 @@
import FluentKit
struct NotSQLDriver: DatabaseDriver {
func makeDatabase(
with context: DatabaseContext
) -> any Database {
NotSQLDatabase(context: context)
}
func shutdown() {}
}
@@ -0,0 +1,169 @@
import NIOCore
import NIOPosix
/// A fake PostgreSQL server speaking just enough of the wire protocol to complete a plaintext startup.
///
/// It answers the client's `SSLRequest` with `'N'` (no SSL) and the subsequent startup message with
/// `AuthenticationOk`, `BackendKeyData`, and `ReadyForQuery` so a client asking for TLS can only end up
/// connected in plaintext. This is what the `prefer` fallback test connects to, proving the driver
/// downgrades to plaintext rather than refusing the connection, and what the `require` test connects to,
/// proving the driver refuses the connection instead of downgrading.
final class PlaintextPostgresServer {
// MARK: Properties
/// The port the server listens on, assigned by the system at bind time.
let port: Int
/// The listening channel the server accepts connections through.
private let channel: Channel
// MARK: Initializers
private init(
channel: Channel,
port: Int
) {
self.channel = channel
self.port = port
}
// MARK: Functions
/// Starts a server on the loopback interface, on a system-assigned port.
/// - Returns: the running server, ready to be connected to at ``port``.
static func start() async throws -> PlaintextPostgresServer {
let channel = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
.childChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(Handler())
}
}
.bind(host: "127.0.0.1", port: 0)
.get()
guard let port = channel.localAddress?.port else {
throw ChannelError.unknownLocalAddress
}
return .init(
channel: channel,
port: port
)
}
/// Stops the server, closing its listening channel.
func stop() async throws {
try await channel.close().get()
}
}
// MARK: - Handlers
private extension PlaintextPostgresServer {
/// Refuses the client's `SSLRequest`, accepts whatever startup message arrives, and closes on anything
/// after that (e.g. a `Terminate`).
///
/// Unlike MySQL, the PostgreSQL client speaks first, so nothing is written on `channelActive`.
final class Handler: ChannelInboundHandler {
// MARK: Type aliases
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
// MARK: Enumerations
/// The startup phases the connection moves through.
private enum State {
case awaitingSSLRequest
case awaitingStartup
case established
}
// MARK: Constants
/// The magic code identifying an `SSLRequest` message.
private static let sslRequestCode: Int32 = 80877103
// MARK: Properties
/// The startup phase the connection is currently in.
private var state: State = .awaitingSSLRequest
// MARK: Functions
func channelRead(
context: ChannelHandlerContext,
data: NIOAny
) {
let buffer = unwrapInboundIn(data)
switch state {
case .awaitingSSLRequest:
// Peek past the Int32 length at the Int32 code: an `SSLRequest` is refused with a bare
// 'N', while a direct startup message (a client connecting with TLS disabled) is
// answered straight away.
guard buffer.getInteger(at: buffer.readerIndex + 4, as: Int32.self) == Self.sslRequestCode else {
completeStartup(context: context)
return
}
state = .awaitingStartup
var refusal = context.channel.allocator.buffer(capacity: 1)
refusal.writeInteger(UInt8(ascii: "N"))
context.writeAndFlush(
wrapOutboundOut(refusal),
promise: nil
)
case .awaitingStartup:
completeStartup(context: context)
case .established:
context.close(promise: nil)
}
}
/// Answers a startup message and marks the connection established.
private func completeStartup(context: ChannelHandlerContext) {
state = .established
context.writeAndFlush(
wrapOutboundOut(Self.startupResponse(allocator: context.channel.allocator)),
promise: nil
)
}
// MARK: Helpers
/// The reply completing a plaintext startup: `AuthenticationOk`, `BackendKeyData`, and
/// `ReadyForQuery` in a single flush.
///
/// `BackendKeyData` is not optional filler the client requires it before `ReadyForQuery` by
/// default and fails the connection when it is missing.
private static func startupResponse(allocator: ByteBufferAllocator) -> ByteBuffer {
var buffer = allocator.buffer(capacity: 32)
buffer.writeInteger(UInt8(ascii: "R")) // AuthenticationOk
buffer.writeInteger(Int32(8))
buffer.writeInteger(Int32(0))
buffer.writeInteger(UInt8(ascii: "K")) // BackendKeyData
buffer.writeInteger(Int32(12))
buffer.writeInteger(Int32(1)) // process id
buffer.writeInteger(Int32(0)) // secret key
buffer.writeInteger(UInt8(ascii: "Z")) // ReadyForQuery
buffer.writeInteger(Int32(5))
buffer.writeInteger(UInt8(ascii: "I")) // idle
return buffer
}
}
}
@@ -0,0 +1,54 @@
import NIOCore
import NIOPosix
/// A fake server accepting connections and never answering.
///
/// A client connecting to it completes the TCP handshake and then waits forever for the first protocol byte the shape of a database that hangs rather
/// than refuses. This is what the probe's deadline test connects to, proving the probe answers within its timeout instead of hanging alongside the server.
final class SilentPostgresServer {
// MARK: Properties
/// The port the server listens on, assigned by the system at bind time.
let port: Int
/// The listening channel the server accepts connections through.
private let channel: Channel
// MARK: Initializers
private init(
channel: Channel,
port: Int
) {
self.channel = channel
self.port = port
}
// MARK: Functions
/// Starts a server on the loopback interface, on a system-assigned port.
/// - Returns: the running server, ready to be connected to at ``port``.
static func start() async throws -> SilentPostgresServer {
let channel = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
.bind(host: "127.0.0.1", port: 0)
.get()
guard let port = channel.localAddress?.port else {
throw ChannelError.unknownLocalAddress
}
return .init(
channel: channel,
port: port
)
}
/// Stops the server, closing its listening channel.
func stop() async throws {
try await channel
.close()
.get()
}
}