diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index 0fea43d..316a00d 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -3,6 +3,9 @@ on: push: branches: - main +permissions: + contents: read + id-token: write jobs: build-and-deploy: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72ffa43..7191f71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,22 +5,22 @@ concurrency: on: pull_request: { types: [opened, reopened, synchronize, ready_for_review] } push: { branches: [ main ] } - +permissions: + contents: read env: LOG_LEVEL: info - SWIFT_DETERMINISTIC_HASHING: 1 - POSTGRES_HOSTNAME: 'psql-a' - POSTGRES_HOSTNAME_A: 'psql-a' - POSTGRES_HOSTNAME_B: 'psql-b' - POSTGRES_DB: 'test_database' - POSTGRES_DB_A: 'test_database' - POSTGRES_DB_B: 'test_database' - POSTGRES_USER: 'test_username' - POSTGRES_USER_A: 'test_username' - POSTGRES_USER_B: 'test_username' - POSTGRES_PASSWORD: 'test_password' - POSTGRES_PASSWORD_A: 'test_password' - POSTGRES_PASSWORD_B: 'test_password' + POSTGRES_HOSTNAME_A: &postgres_host_a 'psql-a' + POSTGRES_HOSTNAME_B: &postgres_host_b 'psql-b' + POSTGRES_HOSTNAME: *postgres_host_a + POSTGRES_DB_A: &postgres_db_a 'test_database' + POSTGRES_DB_B: &postgres_db_b 'test_database' + POSTGRES_DB: *postgres_db_a + POSTGRES_USER_A: &postgres_user_a 'test_username' + POSTGRES_USER_B: &postgres_user_b 'test_username' + POSTGRES_USER: *postgres_user_a + POSTGRES_PASSWORD_A: &postgres_pass_a 'test_password' + POSTGRES_PASSWORD_B: &postgres_pass_b 'test_password' + POSTGRES_PASSWORD: *postgres_pass_a jobs: api-breakage: @@ -42,29 +42,29 @@ jobs: fail-fast: false matrix: postgres-image: - - postgres:17 - - postgres:15 - - postgres:13 + - postgres:18 + - postgres:16 + - postgres:14 swift-image: - - swift:5.10-jammy - swift:6.0-noble - swift:6.1-noble + - swift:6.2-noble include: - - postgres-image: postgres:17 + - postgres-image: postgres:18 postgres-auth: scram-sha-256 - - postgres-image: postgres:15 + - postgres-image: postgres:16 postgres-auth: md5 - - postgres-image: postgres:13 + - postgres-image: postgres:14 postgres-auth: trust runs-on: ubuntu-latest container: ${{ matrix.swift-image }} services: - psql-a: + *postgres_host_a: image: ${{ matrix.postgres-image }} env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_a + POSTGRES_DB: *postgres_db_a + POSTGRES_PASSWORD: *postgres_pass_a POSTGRES_HOST_AUTH_METHOD: ${{ matrix.postgres-auth }} POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.postgres-auth }} steps: @@ -73,7 +73,7 @@ jobs: - name: Check out package uses: actions/checkout@v5 - name: Run local tests - run: swift test --enable-code-coverage + run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable - name: Upload coverage data uses: vapor/swift-codecov-action@v0.3 with: @@ -82,22 +82,22 @@ jobs: linux-integration: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble services: - psql-a: - image: postgres:17 + *postgres_host_a: + image: postgres:18 env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_a + POSTGRES_DB: *postgres_db_a + POSTGRES_PASSWORD: *postgres_pass_a POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 - psql-b: - image: postgres:15 + *postgres_host_b: + image: postgres:16 env: - POSTGRES_USER: test_username - POSTGRES_DB: test_database - POSTGRES_PASSWORD: test_password + POSTGRES_USER: *postgres_user_b + POSTGRES_DB: *postgres_db_b + POSTGRES_PASSWORD: *postgres_pass_b POSTGRES_HOST_AUTH_METHOD: scram-sha-256 POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 steps: @@ -118,10 +118,10 @@ jobs: fail-fast: false matrix: include: - - macos-version: macos-14 - xcode-version: latest-stable - macos-version: macos-15 xcode-version: latest-stable + - macos-version: macos-26 + xcode-version: latest-stable runs-on: ${{ matrix.macos-version }} env: POSTGRES_HOSTNAME: 127.0.0.1 @@ -134,15 +134,15 @@ jobs: - name: Install Postgres, setup DB and auth, and wait for server start run: | brew upgrade || true - export PATH="$(brew --prefix)/opt/postgresql@13/bin:$PATH" PGDATA=/tmp/vapor-postgres-test - brew install "postgresql@17" && brew link --force "postgresql@17" + export PGDATA=/tmp/vapor-postgres-test + brew install "postgresql@18" && brew link --force "postgresql@18" initdb --locale=C --auth-host "scram-sha-256" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}") pg_ctl start --wait timeout-minutes: 15 - name: Checkout code uses: actions/checkout@v5 - name: Run local tests - run: swift test --enable-code-coverage + run: swift test --enable-code-coverage --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable - name: Upload coverage data uses: vapor/swift-codecov-action@v0.3 with: @@ -150,12 +150,12 @@ jobs: musl: runs-on: ubuntu-latest - container: swift:6.1-noble + container: swift:6.2-noble timeout-minutes: 30 steps: - name: Check out code uses: actions/checkout@v5 - name: Install SDK - run: swift sdk install https://download.swift.org/swift-6.1.2-release/static-sdk/swift-6.1.2-RELEASE/swift-6.1.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum df0b40b9b582598e7e3d70c82ab503fd6fbfdff71fd17e7f1ab37115a0665b3b + run: swift sdk install https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum d2225840e592389ca517bbf71652f7003dbf45ac35d1e57d98b9250368769378 - name: Build run: swift build --swift-sdk x86_64-swift-linux-musl diff --git a/Package.swift b/Package.swift index 324ba77..4eb6987 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.27.0"), - .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.1"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.2"), .package(url: "https://github.com/vapor/async-kit.git", from: "1.21.0"), ], targets: [ @@ -40,9 +40,9 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), + //.enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableUpcomingFeature("InferIsolatedConformances"), + //.enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .enableUpcomingFeature("ImmutableWeakCaptures"), ] } diff --git a/README.md b/README.md index 4861d16..d53e3d4 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Team Chat MIT License Continuous Integration - -Swift 5.10+ +Code Coverage +Swift 6.0+


diff --git a/Sources/PostgresKit/Docs.docc/theme-settings.json b/Sources/PostgresKit/Docs.docc/theme-settings.json index 52bf00b..f13ead3 100644 --- a/Sources/PostgresKit/Docs.docc/theme-settings.json +++ b/Sources/PostgresKit/Docs.docc/theme-settings.json @@ -1,6 +1,6 @@ { "theme": { - "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, + "aside": { "border-radius": "16px", "border-width": "3px", "border-style": "double" }, "border-radius": "0", "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, @@ -8,9 +8,9 @@ "psqlkit": "#336791", "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlkit) 30%, #000 100%)", "documentation-intro-accent": "var(--color-psqlkit)", - "documentation-intro-eyebrow": "white", + "hero-eyebrow": "white", "documentation-intro-figure": "white", - "documentation-intro-title": "white", + "hero-title": "white", "logo-base": { "dark": "#fff", "light": "#000" }, "logo-shape": { "dark": "#000", "light": "#fff" }, "fill": { "dark": "#000", "light": "#fff" } diff --git a/Sources/PostgresKit/PostgresDataTranslation.swift b/Sources/PostgresKit/PostgresDataTranslation.swift index eb7053c..6fb9346 100644 --- a/Sources/PostgresKit/PostgresDataTranslation.swift +++ b/Sources/PostgresKit/PostgresDataTranslation.swift @@ -55,11 +55,7 @@ extension URL { } } -#if compiler(>=6.0) extension URL: @retroactive PostgresNonThrowingEncodable, @retroactive PostgresDecodable {} -#else -extension Foundation.URL: PostgresNIO.PostgresNonThrowingEncodable, PostgresNIO.PostgresDecodable {} -#endif struct PostgresDataTranslation { /// This typealias serves to limit the deprecation noise caused by `PostgresDataConvertible` to a single diff --git a/Tests/PostgresKitTests/PostgresKitTests.swift b/Tests/PostgresKitTests/PostgresKitTests.swift index 7253612..1b16b9f 100644 --- a/Tests/PostgresKitTests/PostgresKitTests.swift +++ b/Tests/PostgresKitTests/PostgresKitTests.swift @@ -3,53 +3,27 @@ import Logging import NIOCore import PostgresNIO import SQLKitBenchmark -import XCTest +import Testing @testable import PostgresKit -final class PostgresKitTests: XCTestCase { - func testSQLKitBenchmark() async throws { - let conn = try await PostgresConnection.test(on: self.eventLoop).get() - do { +extension AllSuites { + +@Suite +struct PostgresKitTests { + @Test + func sqlKitBenchmark() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) + + await #expect(throws: Never.self) { let benchmark = SQLBenchmarker(on: conn.sql()) try await benchmark.runAllTests() - } catch { - try? await conn.close() - XCTFail("Caught error: \(String(reflecting: error))") - throw error } try await conn.close() } - // Disable for now, test is of questionable utility - /* - func testPerformance() throws { - let db = PostgresConnectionSource(sqlConfiguration: .test) - let pool = EventLoopGroupConnectionPool( - source: db, - maxConnectionsPerEventLoop: 2, - on: MultiThreadedEventLoopGroup.singleton - ) - defer { pool.shutdown() } - // Postgres seems to take much longer on initial connections when using SCRAM-SHA-256 auth, - // which causes XCTest to bail due to the first measurement having a very high deviation. - // Spin the pool a bit before running the measurement to warm it up. - for _ in 1...25 { - _ = try pool.withConnection { conn in - conn.query("SELECT 1") - }.wait() - } - self.measure { - for _ in 1...100 { - _ = try! pool.withConnection { conn in - conn.query("SELECT 1") - }.wait() - } - } - } - */ - - func testLeak() throws { + @Test + func leak() async throws { struct Foo: Codable { var id: String var description: String? @@ -61,29 +35,22 @@ final class PostgresKitTests: XCTestCase { var modified_at: Date } - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } - + let conn = try await PostgresConnection.test(on: self.eventLoop) let db = conn.sql() - - do { - try db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run().wait() - try db.raw(""" - CREATE TABLE \(ident: "foos") ( - \(ident: "id") TEXT PRIMARY KEY, - \(ident: "description") TEXT, - \(ident: "latitude") DOUBLE PRECISION, - \(ident: "longitude") DOUBLE PRECISION, - \(ident: "created_by") TEXT, - \(ident: "created_at") TIMESTAMPTZ, - \(ident: "modified_by") TEXT, - \(ident: "modified_at") TIMESTAMPTZ - ) - """).run().wait() - defer { - try? db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run().wait() - } - + + await #expect(throws: Never.self) { + try await db.drop(table: "foos").ifExists().run() + try await db.create(table: "foos") + .column("id", type: .text, .primaryKey(autoIncrement: false)) + .column("description", type: .text) + .column("latitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) + .column("longitude", type: .custom(SQLRaw("DOUBLE PRECISION"))) + .column("created_by", type: .text) + .column("created_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) + .column("modified_by", type: .text) + .column("modified_at", type: .custom(SQLRaw("TIMESTAMPTZ"))) + .run() + for i in 0..<5_000 { let zipcode = Foo( id: UUID().uuidString, @@ -95,79 +62,91 @@ final class PostgresKitTests: XCTestCase { modified_by: "test", modified_at: Date() ) - try db.insert(into: "foos") + try await db.insert(into: "foos") .model(zipcode) - .run().wait() + .run() } - } catch { - XCTFail("Caught error: \(String(reflecting: error))") } + try? await db.raw("DROP TABLE IF EXISTS \(ident: "foos")").run() + try await conn.close() } - func testArrayEncoding() throws { - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } - + @Test + func arrayEncoding() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) + struct Foo: Codable { var bar: Int } - let foos: [Foo] = [.init(bar: 1), .init(bar: 2)] - try conn.sql().raw("SELECT \(bind: foos)::JSONB[] as \(ident: "foos")") - .run().wait() + + await #expect(throws: Never.self) { + let foos: [Foo] = [.init(bar: 1), .init(bar: 2)] + try await conn.sql().raw("SELECT \(bind: foos)::JSONB[] as \(ident: "foos")").run() + } + try await conn.close() } - func testDecodeModelWithNil() throws { - let conn = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! conn.close().wait() } + @Test + func decodeModelWithNil() async throws { + let conn = try await PostgresConnection.test(on: self.eventLoop) - let rows = try conn.sql().raw("SELECT \(literal: "foo")::text as \(ident: "foo"), \(SQLLiteral.null) as \(ident: "bar"), \(literal: "baz")::text as \(ident: "baz")").all().wait() - let row = rows[0] - - struct Test: Codable { - var foo: String - var bar: String? - var baz: String? - } + await #expect(throws: Never.self) { + let rows = try await conn.sql().raw("SELECT \(literal: "foo")::text as \(ident: "foo"), \(SQLLiteral.null) as \(ident: "bar"), \(literal: "baz")::text as \(ident: "baz")").all() + let row = rows[0] + + struct Test: Codable { + var foo: String + var bar: String? + var baz: String? + } - let test = try row.decode(model: Test.self) - XCTAssertEqual(test.foo, "foo") - XCTAssertEqual(test.bar, nil) - XCTAssertEqual(test.baz, "baz") + let test = try row.decode(model: Test.self) + #expect(test.foo == "foo") + #expect(test.bar == nil) + #expect(test.baz == "baz") + } + try await conn.close() } - func testEventLoopGroupSQL() throws { + @Test + func eventLoopGroupSQL() async throws { var configuration = SQLPostgresConfiguration.test configuration.searchPath = ["foo", "bar", "baz"] let source = PostgresConnectionSource(sqlConfiguration: configuration) let pool = EventLoopGroupConnectionPool(source: source, on: MultiThreadedEventLoopGroup.singleton) - defer { pool.shutdown() } let db = pool.database(logger: .init(label: "test")).sql() - let rows = try db.raw("SELECT version()").all().wait() - XCTAssertEqual(rows.count, 1) + await #expect(throws: Never.self) { + try await #expect(db.raw("SELECT version()").all().count == 1) + } + try await pool.shutdownAsync() } - func testIntegerArrayEncoding() throws { - let connection = try PostgresConnection.test(on: self.eventLoop).wait() - defer { try! connection.close().wait() } - let sql = connection.sql() - _ = try sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run().wait() - _ = try sql.raw("CREATE TABLE \(ident: "foo") (\(ident: "bar") bigint[] not null)").run().wait() - defer { - _ = try! sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run().wait() + @Test + func integerArrayEncoding() async throws { + let connection = try await PostgresConnection.test(on: self.eventLoop) + + await #expect(throws: Never.self) { + let sql = connection.sql() + _ = try await sql.raw("DROP TABLE IF EXISTS \(ident: "foo")").run() + try await sql.withSession { db in + _ = try await db.create(table: "foo").column("bar", type: .custom(SQLRaw("bigint[]")), .notNull).run() + _ = try await db.insert(into: "foo").columns("bar").values(SQLBind([Bar]())).run() + let rows = try await connection.query("SELECT bar FROM foo", logger: connection.logger).collect() + #expect(rows.count == 1) + #expect(rows.first?.count == 1) + #expect(rows.first?.first?.dataType == Bar.psqlArrayType) + #expect(try rows.first?.first?.decode([Bar].self) == [Bar]()) + } } - _ = try sql.raw("INSERT INTO \(ident: "foo") (\(ident: "bar")) VALUES (\(bind: [Bar]()))").run().wait() - let rows = try connection.query("SELECT bar FROM foo", logger: connection.logger).wait() - XCTAssertEqual(rows.count, 1) - XCTAssertEqual(rows.first?.count, 1) - XCTAssertEqual(rows.first?.first?.dataType, Bar.psqlArrayType) - XCTAssertEqual(try rows.first?.first?.decode([Bar].self), [Bar]()) + try await connection.close() } /// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()` /// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test /// at this layer). - func testValuesThatUseSuperEncoder() throws { + @Test + func valuesThatUseSuperEncoder() throws { struct UnusualType: Codable { var prop1: String, prop2: [Bool], prop3: [[Bool]] @@ -207,53 +186,57 @@ final class PostgresKitTests: XCTestCase { let encoded1 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: instance, in: .default, file: #fileID, line: #line) let encoded2 = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: [instance, instance], in: .default, file: #fileID, line: #line) - XCTAssertEqual(encoded1.type, .jsonb) - XCTAssertEqual(encoded2.type, .jsonbArray) - + #expect(encoded1.type == .jsonb) + #expect(encoded2.type == .jsonbArray) + let decoded1 = try PostgresDataTranslation.decode(UnusualType.self, from: .init(bytes: encoded1.value, dataType: encoded1.type, format: encoded1.formatCode, columnName: "", columnIndex: -1), in: .default) let decoded2 = try PostgresDataTranslation.decode([UnusualType].self, from: .init(bytes: encoded2.value, dataType: encoded2.type, format: encoded2.formatCode, columnName: "", columnIndex: -1), in: .default) - XCTAssertEqual(decoded1.prop3, instance.prop3) - XCTAssertEqual(decoded2.count, 2) + #expect(decoded1.prop3 == instance.prop3) + #expect(decoded2.count == 2) } - - func testFluentWorkaroundsDecoding() throws { + + @Test + func fluentWorkaroundsDecoding() throws { // SQLKit benchmarks already test enum handling // Text encoding for Decimal let decimalBuffer = ByteBuffer(string: Decimal(12345.6789).description) var decimalValue: Decimal? - XCTAssertNoThrow(decimalValue = try PostgresDataTranslation.decode(Decimal.self, from: .init(bytes: decimalBuffer, dataType: .numeric, format: .text, columnName: "", columnIndex: -1), in: .default)) - XCTAssertEqual(decimalValue, Decimal(12345.6789)) - + #expect(throws: Never.self) { decimalValue = try PostgresDataTranslation.decode(Decimal.self, from: .init(bytes: decimalBuffer, dataType: .numeric, format: .text, columnName: "", columnIndex: -1), in: .default) } + #expect(decimalValue == Decimal(12345.6789)) + // Decoding Double from NUMERIC let numericBuffer = PostgresData(numeric: .init(decimal: 12345.6789)).value var numericValue: Double? - XCTAssertNoThrow(numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default)) - XCTAssertEqual(numericValue, Double(Decimal(12345.6789).description)) + #expect(throws: Never.self) { numericValue = try PostgresDataTranslation.decode(Double.self, from: .init(bytes: numericBuffer, dataType: .numeric, format: .binary, columnName: "", columnIndex: -1), in: .default) } + #expect(numericValue == Double(Decimal(12345.6789).description)) } - - func testURLWorkaroundDecoding() throws { + + @Test + func urlWorkaroundDecoding() throws { let url = URL(string: "https://user:pass@www.example.com:8080/path/to/endpoint?query=value#fragment")! let encodedNormal = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: url, in: .default, file: #fileID, line: #line) - XCTAssertEqual(encodedNormal.value?.getString(at: 0, length: encodedNormal.value?.readableBytes ?? 0), url.absoluteString) - + #expect(encodedNormal.value?.getString(at: 0, length: encodedNormal.value?.readableBytes ?? 0) == url.absoluteString) + let encodedBroken = try PostgresDataTranslation.encode(codingPath: [], userInfo: [:], value: "\"\(url.absoluteString)\"", in: .default, file: #fileID, line: #line) - XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default), url) - XCTAssertEqual(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default), url) + #expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedNormal), in: .default) == url) + #expect(try PostgresDataTranslation.decode(URL.self, from: .init(with: encodedBroken), in: .default) == url) } var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() } - override class func setUp() { - XCTAssertTrue(isLoggingConfigured) + init() { + #expect(isLoggingConfigured) } } +} + extension PostgresCell { fileprivate init(with data: PostgresData) { self.init(bytes: data.value, dataType: data.type, format: data.formatCode, columnName: "", columnIndex: -1) diff --git a/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift b/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift index 8281122..e064839 100644 --- a/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift +++ b/Tests/PostgresKitTests/SQLPostgresConfigurationTests.swift @@ -1,75 +1,83 @@ import PostgresKit -import XCTest +import Testing -final class SQLPostgresConfigurationTests: XCTestCase { - func testURLHandling() throws { +extension AllSuites { +@Suite +struct SQLPostgresConfigurationTests { + @Test + func urlHandling() throws { let config1 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username:test_password@test_hostname:9999/test_database?tlsmode=disable") - XCTAssertEqual(config1.coreConfiguration.database, "test_database") - XCTAssertEqual(config1.coreConfiguration.password, "test_password") - XCTAssertEqual(config1.coreConfiguration.username, "test_username") - XCTAssertEqual(config1.coreConfiguration.host, "test_hostname") - XCTAssertEqual(config1.coreConfiguration.port, 9999) - XCTAssertNil(config1.coreConfiguration.unixSocketPath) - XCTAssertFalse(config1.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config1.coreConfiguration.tls.isEnforced) + #expect(config1.coreConfiguration.database == "test_database") + #expect(config1.coreConfiguration.password == "test_password") + #expect(config1.coreConfiguration.username == "test_username") + #expect(config1.coreConfiguration.host == "test_hostname") + #expect(config1.coreConfiguration.port == 9999) + #expect(config1.coreConfiguration.unixSocketPath == nil) + #expect(!config1.coreConfiguration.tls.isAllowed) + #expect(!config1.coreConfiguration.tls.isEnforced) let config2 = try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname") - XCTAssertNil(config2.coreConfiguration.database) - XCTAssertNil(config2.coreConfiguration.password) - XCTAssertEqual(config2.coreConfiguration.username, "test_username") - XCTAssertEqual(config2.coreConfiguration.host, "test_hostname") - XCTAssertEqual(config2.coreConfiguration.port, SQLPostgresConfiguration.ianaPortNumber) - XCTAssertNil(config2.coreConfiguration.unixSocketPath) - XCTAssertTrue(config2.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config2.coreConfiguration.tls.isEnforced) + #expect(config2.coreConfiguration.database == nil) + #expect(config2.coreConfiguration.password == nil) + #expect(config2.coreConfiguration.username == "test_username") + #expect(config2.coreConfiguration.host == "test_hostname") + #expect(config2.coreConfiguration.port == SQLPostgresConfiguration.ianaPortNumber) + #expect(config2.coreConfiguration.unixSocketPath == nil) + #expect(config2.coreConfiguration.tls.isAllowed) + #expect(!config2.coreConfiguration.tls.isEnforced) let config3 = try SQLPostgresConfiguration(url: "postgres+uds://test_username:test_password@localhost/tmp/postgres.sock?tlsmode=require#test_database") - XCTAssertEqual(config3.coreConfiguration.database, "test_database") - XCTAssertEqual(config3.coreConfiguration.password, "test_password") - XCTAssertEqual(config3.coreConfiguration.username, "test_username") - XCTAssertNil(config3.coreConfiguration.host) - XCTAssertNil(config3.coreConfiguration.port) - XCTAssertEqual(config3.coreConfiguration.unixSocketPath, "/tmp/postgres.sock") - XCTAssertTrue(config3.coreConfiguration.tls.isAllowed) - XCTAssertTrue(config3.coreConfiguration.tls.isEnforced) + #expect(config3.coreConfiguration.database == "test_database") + #expect(config3.coreConfiguration.password == "test_password") + #expect(config3.coreConfiguration.username == "test_username") + #expect(config3.coreConfiguration.host == nil) + #expect(config3.coreConfiguration.port == nil) + #expect(config3.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") + #expect(config3.coreConfiguration.tls.isAllowed) + #expect(config3.coreConfiguration.tls.isEnforced) let config4 = try SQLPostgresConfiguration(url: "postgres+uds://test_username@/tmp/postgres.sock") - XCTAssertNil(config4.coreConfiguration.database) - XCTAssertNil(config4.coreConfiguration.password) - XCTAssertEqual(config4.coreConfiguration.username, "test_username") - XCTAssertNil(config4.coreConfiguration.host) - XCTAssertNil(config4.coreConfiguration.port) - XCTAssertEqual(config4.coreConfiguration.unixSocketPath, "/tmp/postgres.sock") - XCTAssertFalse(config4.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config4.coreConfiguration.tls.isEnforced) - + #expect(config4.coreConfiguration.database == nil) + #expect(config4.coreConfiguration.password == nil) + #expect(config4.coreConfiguration.username == "test_username") + #expect(config4.coreConfiguration.host == nil) + #expect(config4.coreConfiguration.port == nil) + #expect(config4.coreConfiguration.unixSocketPath == "/tmp/postgres.sock") + #expect(!config4.coreConfiguration.tls.isAllowed) + #expect(!config4.coreConfiguration.tls.isEnforced) + for modestr in ["tlsmode=false", "tlsmode=verify-full&tlsmode=disable"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertFalse(config.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config.coreConfiguration.tls.isEnforced) + #expect(!config.coreConfiguration.tls.isAllowed) + #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=prefer", "tlsmode=allow", "tlsmode=true"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertTrue(config.coreConfiguration.tls.isAllowed) - XCTAssertFalse(config.coreConfiguration.tls.isEnforced) + #expect(config.coreConfiguration.tls.isAllowed) + #expect(!config.coreConfiguration.tls.isEnforced) } for modestr in ["tlsmode=require", "tlsmode=verify-ca", "tlsmode=verify-full", "tls=verify-full", "ssl=verify-full", "tlsmode=prefer&sslmode=verify-full"] { let config = try SQLPostgresConfiguration(url: "postgres://u@h?\(modestr)") - XCTAssertTrue(config.coreConfiguration.tls.isAllowed) - XCTAssertTrue(config.coreConfiguration.tls.isEnforced) + #expect(config.coreConfiguration.tls.isAllowed) + #expect(config.coreConfiguration.tls.isEnforced) } - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql://test_username@test_hostname")) - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql+tcp://test_username@test_hostname")) - XCTAssertNoThrow(try SQLPostgresConfiguration(url: "postgresql+uds://test_username@/tmp/postgres.sock")) - - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+tcp://test_hostname"), "should fail when username missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname?tlsmode=absurd"), "should fail when TLS mode invalid") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://localhost/tmp/postgres.sock?tlsmode=require"), "should fail when username missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds:///tmp/postgres.sock"), "should fail when authority missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://username@localhost/"), "should fail when path missing") - XCTAssertThrowsError(try SQLPostgresConfiguration(url: "postgres+uds://username@remotehost/tmp"), "should fail when authority not localhost or empty") + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql://test_username@test_hostname") } + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+tcp://test_username@test_hostname") } + #expect(throws: Never.self) { try SQLPostgresConfiguration(url: "postgresql+uds://test_username@/tmp/postgres.sock") } + + #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+tcp://test_hostname") } + #expect(throws: (any Error).self, "should fail when TLS mode invalid") { try SQLPostgresConfiguration(url: "postgres+tcp://test_username@test_hostname?tlsmode=absurd") } + #expect(throws: (any Error).self, "should fail when username missing") { try SQLPostgresConfiguration(url: "postgres+uds://localhost/tmp/postgres.sock?tlsmode=require") } + #expect(throws: (any Error).self, "should fail when authority missing") { try SQLPostgresConfiguration(url: "postgres+uds:///tmp/postgres.sock") } + #expect(throws: (any Error).self, "should fail when path missing") { try SQLPostgresConfiguration(url: "postgres+uds://username@localhost/") } + #expect(throws: (any Error).self, "should fail when authority not localhost or empty") { try SQLPostgresConfiguration(url: "postgres+uds://username@remotehost/tmp") } } + + init() { + #expect(isLoggingConfigured) + } +} } diff --git a/Tests/PostgresKitTests/Utilities.swift b/Tests/PostgresKitTests/Utilities.swift index ed2d189..ec2ad58 100644 --- a/Tests/PostgresKitTests/Utilities.swift +++ b/Tests/PostgresKitTests/Utilities.swift @@ -3,14 +3,14 @@ import Logging import NIOCore import PostgresKit import PostgresNIO -import XCTest +import Testing extension PostgresConnection { - static func test(on eventLoop: any EventLoop) -> EventLoopFuture { - PostgresConnectionSource(sqlConfiguration: .test).makeConnection( + static func test(on eventLoop: any EventLoop) async throws -> PostgresConnection { + try await PostgresConnectionSource(sqlConfiguration: .test).makeConnection( logger: .init(label: "vapor.codes.postgres-kit.test"), on: eventLoop - ) + ).get() } } @@ -32,10 +32,39 @@ func env(_ name: String) -> String? { } let isLoggingConfigured: Bool = { - LoggingSystem.bootstrap { label in - var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info - return handler - } + LoggingSystem.bootstrap { QuickLogHandler(label: $0, level: env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info) } return true }() + +struct QuickLogHandler: LogHandler { + private let label: String + var logLevel = Logger.Level.info, metadataProvider = LoggingSystem.metadataProvider, metadata = Logger.Metadata() + subscript(metadataKey key: String) -> Logger.Metadata.Value? { get { self.metadata[key] } set { self.metadata[key] = newValue } } + init(label: String, level: Logger.Level) { (self.label, self.logLevel) = (label, level) } + func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) { + print("\(self.timestamp()) \(level) \(self.label):\(self.prettify(metadata ?? [:]).map { " \($0)" } ?? "") [\(source)] \(message)") + } + private func prettify(_ metadata: Logger.Metadata) -> String? { + self.metadata.merging(self.metadataProvider?.get() ?? [:]) { $1 }.merging(metadata) { $1 }.sorted { $0.0 < $1.0 }.map { "\($0)=\($1.mvDesc)" }.joined(separator: " ") + } + private func timestamp() -> String { .init(unsafeUninitializedCapacity: 255) { buffer in + var timestamp = time(nil) + return localtime(×tamp).map { strftime(buffer.baseAddress!, buffer.count, "%Y-%m-%dT%H:%M:%S%z", $0) } ?? buffer.initialize(fromContentsOf: "".utf8) + } } +} +extension Logger.MetadataValue { + var mvDesc: String { switch self { + case .dictionary(let dict): "[\(dict.mapValues(\.mvDesc).lazy.sorted { $0.0 < $1.0 }.map { "\($0): \($1)" }.joined(separator: ", "))]" + case .array(let list): "[\(list.map(\.mvDesc).joined(separator: ", "))]" + case .string(let str): #""\#(str)""# + case .stringConvertible(let repr): switch repr { + case let repr as Bool: "\(repr)" + case let repr as any FixedWidthInteger: "\(repr)" + case let repr as any BinaryFloatingPoint: "\(repr)" + default: #""\#(String(describing: repr))""# + } + } } +} + +@Suite(.serialized) +struct AllSuites {}