Merge branch 'setup' into template
This commit is contained in:
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ struct ProbeTests {
|
||||
|
||||
@Test
|
||||
func `reports a reachable database`() async throws {
|
||||
let service = Service(
|
||||
let service = try Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
@@ -33,8 +33,8 @@ struct ProbeTests {
|
||||
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 = Service(
|
||||
driver: .mysql(
|
||||
let service = try Service(
|
||||
driver: .postgres(
|
||||
.init(
|
||||
host: "127.0.0.1",
|
||||
port: 1,
|
||||
@@ -42,7 +42,8 @@ struct ProbeTests {
|
||||
username: "nobody",
|
||||
password: "nothing",
|
||||
tls: .off,
|
||||
maxConnectionsPerEventLoop: 1
|
||||
maxConnectionsPerEventLoop: 1,
|
||||
poolTimeout: .seconds(10)
|
||||
)
|
||||
),
|
||||
logger: Logger(label: "test")
|
||||
@@ -57,6 +58,47 @@ struct ProbeTests {
|
||||
#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"))
|
||||
|
||||
@@ -15,7 +15,7 @@ struct ServiceTests {
|
||||
|
||||
@Test
|
||||
func `registers an SQLite database as the default for the in-memory driver`() async throws {
|
||||
let service = Service(
|
||||
let service = try Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
@@ -31,19 +31,20 @@ struct ServiceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func `registers a MySQL database as the default for the mysql driver`() async throws {
|
||||
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 = Service(
|
||||
driver: .mysql(
|
||||
let service = try Service(
|
||||
driver: .postgres(
|
||||
.init(
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
port: 5432,
|
||||
name: "site",
|
||||
username: "site",
|
||||
password: "site",
|
||||
tls: .off,
|
||||
maxConnectionsPerEventLoop: 1
|
||||
maxConnectionsPerEventLoop: 1,
|
||||
poolTimeout: .seconds(10)
|
||||
)
|
||||
),
|
||||
logger: Logger(label: "test")
|
||||
@@ -56,12 +57,12 @@ struct ServiceTests {
|
||||
|
||||
let dialect = try #require(database?.dialect)
|
||||
|
||||
#expect(dialect.name == "mysql")
|
||||
#expect(dialect.name == "postgresql")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `builds a usable in-memory database`() async throws {
|
||||
let service = Service(
|
||||
let service = try Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
@@ -87,12 +88,12 @@ struct ServiceTests {
|
||||
}
|
||||
|
||||
@Test(
|
||||
"mysql: migrate, insert, read back",
|
||||
.enabled(if: mysqlDriver != nil)
|
||||
"postgres: migrate, insert, read back",
|
||||
.enabled(if: postgresDriver != nil)
|
||||
)
|
||||
func mysqlRoundTrip() async throws {
|
||||
func postgresRoundTrip() async throws {
|
||||
try await roundTrip(
|
||||
driver: mysqlDriver!,
|
||||
driver: postgresDriver!,
|
||||
revertAfter: true
|
||||
)
|
||||
}
|
||||
@@ -116,11 +117,11 @@ private extension ServiceTests {
|
||||
revertAfter: Bool = false
|
||||
) async throws {
|
||||
let prepareDB = PrepareDB()
|
||||
let service = Service(
|
||||
let service = try Service(
|
||||
driver: driver,
|
||||
logger: Logger(label: "test")
|
||||
)
|
||||
|
||||
|
||||
let fluent = service()
|
||||
|
||||
do {
|
||||
@@ -147,25 +148,26 @@ private extension ServiceTests {
|
||||
|
||||
}
|
||||
|
||||
/// The MySQL/MariaDB driver built from the `MYSQL_TEST_*` environment variables, or `nil` when the gate
|
||||
/// variable `MYSQL_TEST_HOST` is unset — in which case the MySQL integration test is skipped, so the suite
|
||||
/// stays runnable with no database available.
|
||||
private let mysqlDriver: Persistence.Driver? = {
|
||||
/// 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["MYSQL_TEST_HOST"] else {
|
||||
guard let host = environment["POSTGRES_TEST_HOST"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .mysql(
|
||||
return .postgres(
|
||||
.init(
|
||||
host: host,
|
||||
port: environment["MYSQL_TEST_PORT"].flatMap(Int.init) ?? 3306,
|
||||
name: environment["MYSQL_TEST_NAME"] ?? "site",
|
||||
username: environment["MYSQL_TEST_USERNAME"] ?? "site",
|
||||
password: environment["MYSQL_TEST_PASSWORD"] ?? "site",
|
||||
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
|
||||
maxConnectionsPerEventLoop: 2,
|
||||
poolTimeout: .seconds(10)
|
||||
)
|
||||
)
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user