diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 61245c97d..7bade0c52 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -104,6 +104,7 @@ functions: MONGODB_VERSION=${MONGODB_VERSION} \ TOPOLOGY=${TOPOLOGY} \ SSL=${SSL} \ + AUTH=${AUTH} \ sh ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh # run-orchestration generates expansion file with the MONGODB_URI for the cluster - command: expansions.update @@ -130,9 +131,10 @@ functions: script: | ${PREPARE_SHELL} - MONGODB_URI=${MONGODB_URI} \ + MONGODB_URI="${MONGODB_URI}" \ TOPOLOGY=${TOPOLOGY} \ SSL=${SSL} \ + AUTH=${AUTH} \ SWIFT_VERSION=${SWIFT_VERSION} \ sh ${PROJECT_DIRECTORY}/.evergreen/run-tests.sh @@ -390,17 +392,19 @@ axes: variables: SWIFT_VERSION: "5.1.4" - - id: ssl - display_name: SSL + - id: ssl-auth + display_name: SSL and Auth values: - - id: ssl - display_name: SSL + - id: ssl-auth + display_name: SSL Auth variables: SSL: "ssl" - - id: nossl - display_name: NoSSL + AUTH: "auth" + - id: nossl-noauth + display_name: NoSSL NoAuth variables: SSL: "nossl" + AUTH: "noauth" buildvariants: @@ -409,8 +413,8 @@ buildvariants: matrix_spec: os-fully-featured: "*" swift-version: "*" - ssl: "*" - display_name: "${swift-version} ${os-fully-featured} ${ssl}" + ssl-auth: "*" + display_name: "${swift-version} ${os-fully-featured} ${ssl-auth}" tasks: - ".latest" - ".4.2" @@ -423,7 +427,7 @@ buildvariants: - if: os-fully-featured: "ubuntu-18.04" swift-version: "*" - ssl: "ssl" + ssl-auth: "ssl-auth" then: remove_tasks: ".3.6" diff --git a/Package.swift b/Package.swift index f6642345d..857236527 100644 --- a/Package.swift +++ b/Package.swift @@ -14,10 +14,10 @@ let package = Package( .target(name: "MongoSwift", dependencies: ["CLibMongoC", "NIO", "NIOConcurrencyHelpers"]), .target(name: "MongoSwiftSync", dependencies: ["MongoSwift"]), .target(name: "AtlasConnectivity", dependencies: ["MongoSwiftSync"]), - .target(name: "TestsCommon", dependencies: ["MongoSwift", "Nimble", "CLibMongoC"]), + .target(name: "TestsCommon", dependencies: ["MongoSwift", "Nimble"]), .testTarget(name: "BSONTests", dependencies: ["MongoSwift", "TestsCommon", "Nimble", "CLibMongoC"]), .testTarget(name: "MongoSwiftTests", dependencies: ["MongoSwift", "TestsCommon", "Nimble", "NIO", "CLibMongoC"]), - .testTarget(name: "MongoSwiftSyncTests", dependencies: ["MongoSwiftSync", "TestsCommon", "Nimble", "CLibMongoC"]), + .testTarget(name: "MongoSwiftSyncTests", dependencies: ["MongoSwiftSync", "TestsCommon", "Nimble", "MongoSwift"]), .target( name: "CLibMongoC", dependencies: [], diff --git a/Sources/MongoSwift/ConnectionPool.swift b/Sources/MongoSwift/ConnectionPool.swift index b0d43d5e6..4f0a47f15 100644 --- a/Sources/MongoSwift/ConnectionPool.swift +++ b/Sources/MongoSwift/ConnectionPool.swift @@ -122,6 +122,38 @@ internal class ConnectionPool { throw InternalError(message: "ConnectionPool was already closed") } } + + /// Selects a server according to the specified parameters and returns a description of a suitable server to use. + /// Throws an error if a server cannot be selected. This method will start up SDAM in libmongoc if it hasn't been + /// started already. This method may block. + internal func selectServer(forWrites: Bool, readPreference: ReadPreference? = nil) throws -> ServerDescription { + return try self.withConnection { conn in + var error = bson_error_t() + guard let desc = mongoc_client_select_server( + conn.clientHandle, + forWrites, + readPreference?._readPreference, + &error + ) else { + throw extractMongoError(error: error) + } + + defer { mongoc_server_description_destroy(desc) } + return ServerDescription(desc) + } + } + + /// Retrieves the connection string used to create this pool. If SDAM has been started in libmongoc, the getters + /// on the returned connection string will return any values that were retrieved from TXT records. Throws an error + /// if the connection string cannot be retrieved. + internal func getConnectionString() throws -> ConnectionString { + return try self.withConnection { conn in + guard let uri = mongoc_client_get_uri(conn.clientHandle) else { + throw InternalError(message: "Couldn't retrieve client's connection string") + } + return ConnectionString(copying: uri) + } + } } extension String { diff --git a/Sources/MongoSwift/ConnectionString.swift b/Sources/MongoSwift/ConnectionString.swift index 445e266c6..ae4e13e74 100644 --- a/Sources/MongoSwift/ConnectionString.swift +++ b/Sources/MongoSwift/ConnectionString.swift @@ -34,6 +34,11 @@ internal class ConnectionString { } } + /// Initializes a new connection string that wraps a copy of the provided URI. Does not destroy the input URI. + internal init(copying uri: OpaquePointer) { + self._uri = mongoc_uri_copy(uri) + } + /// Cleans up the underlying `mongoc_uri_t`. deinit { mongoc_uri_destroy(self._uri) @@ -72,4 +77,116 @@ internal class ConnectionString { mongoc_uri_set_read_prefs_t(self._uri, rp._readPreference) } } + + /// Returns the username if one was provided, otherwise nil. + internal var username: String? { + guard let username = mongoc_uri_get_username(self._uri) else { + return nil + } + return String(cString: username) + } + + /// Returns the password if one was provided, otherwise nil. + internal var password: String? { + guard let pw = mongoc_uri_get_password(self._uri) else { + return nil + } + return String(cString: pw) + } + + /// Returns the auth database if one was provided, otherwise nil. + internal var authSource: String? { + guard let source = mongoc_uri_get_auth_source(self._uri) else { + return nil + } + return String(cString: source) + } + + /// Returns the auth mechanism if one was provided, otherwise nil. + internal var authMechanism: AuthMechanism? { + guard let mechanism = mongoc_uri_get_auth_mechanism(self._uri) else { + return nil + } + let str = String(cString: mechanism) + return AuthMechanism(rawValue: str) + } + + /// Returns a document containing the auth mechanism properties if any were provided, otherwise nil. + internal var authMechanismProperties: Document? { + var props = bson_t() + return withUnsafeMutablePointer(to: &props) { propsPtr in + let opaquePtr = OpaquePointer(propsPtr) + guard mongoc_uri_get_mechanism_properties(self._uri, opaquePtr) else { + return nil + } + /// This copy should not be returned directly as its only guaranteed valid for as long as the + /// `mongoc_uri_t`, as `props` was statically initialized from data stored in the URI and may contain + /// pointers that will be invalidated once the URI is. + let copy = Document(copying: opaquePtr) + + return copy.mapValues { value in + // mongoc returns boolean options e.g. CANONICALIZE_HOSTNAME as strings, but they are boolean values. + switch value { + case "true": + return true + case "false": + return false + default: + return value + } + } + } + } + + /// Returns the credential configured on this URI. Will be empty if no options are set. + internal var credential: Credential { + return Credential( + username: self.username, + password: self.password, + source: self.authSource, + mechanism: self.authMechanism, + mechanismProperties: self.authMechanismProperties + ) + } + + internal var db: String? { + guard let db = mongoc_uri_get_database(self._uri) else { + return nil + } + return String(cString: db) + } + + /// Returns a document containing all of the options provided after the ? of the URI. + internal var options: Document? { + guard let optsDoc = mongoc_uri_get_options(self._uri) else { + return nil + } + return Document(copying: optsDoc) + } + + /// Returns the host/port pairs specified in the connection string, or nil if this connection string's scheme is + /// “mongodb+srv://”. + internal var hosts: [String]? { + guard let hostList = mongoc_uri_get_hosts(self._uri) else { + return nil + } + + var hosts = [String]() + var next = hostList.pointee + while true { + hosts.append(withUnsafeBytes(of: next.host_and_port) { rawPtr in + guard let baseAddress = rawPtr.baseAddress else { + return "" + } + return String(cString: baseAddress.assumingMemoryBound(to: CChar.self)) + }) + + if next.next == nil { + break + } + next = next.next.pointee + } + + return hosts + } } diff --git a/Sources/MongoSwift/Credential.swift b/Sources/MongoSwift/Credential.swift new file mode 100644 index 000000000..70bcb77f5 --- /dev/null +++ b/Sources/MongoSwift/Credential.swift @@ -0,0 +1,63 @@ +/// Represents an authentication credential. +internal struct Credential: Decodable, Equatable { + /// A string containing the username. For auth mechanisms that do not utilize a password, this may be the entire + /// `userinfo` token from the connection string. + internal let username: String? + /// A string containing the password. + internal let password: String? + /// A string containing the authentication database. + internal let source: String? + /// The authentication mechanism. A nil value for this property indicates that a mechanism wasn't specified and + /// that mechanism negotiation is required. + internal let mechanism: AuthMechanism? + /// A document containing mechanism-specific properties. + internal let mechanismProperties: Document? + + private enum CodingKeys: String, CodingKey { + case username, password, source, mechanism, mechanismProperties = "mechanism_properties" + } + + // TODO: SWIFT-636: remove this initializer and the one below it. + internal init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.username = try container.decodeIfPresent(String.self, forKey: .username) + self.password = try container.decodeIfPresent(String.self, forKey: .password) + self.source = try container.decodeIfPresent(String.self, forKey: .source) + self.mechanism = try container.decodeIfPresent(AuthMechanism.self, forKey: .mechanism) + + // libmongoc does not return the service name if it's the default, but it is contained in the spec test files, + // so filter it out here if it's present. + let properties = try container.decodeIfPresent(Document.self, forKey: .mechanismProperties) + let filteredProperties = properties?.filter { !($0.0 == "SERVICE_NAME" && $0.1 == "mongodb") } + // if SERVICE_NAME was the only key then don't return an empty document. + if filteredProperties?.isEmpty == true { + self.mechanismProperties = nil + } else { + self.mechanismProperties = filteredProperties + } + } + + internal init( + username: String?, + password: String?, + source: String?, + mechanism: AuthMechanism?, + mechanismProperties: Document? + ) { + self.mechanism = mechanism + self.mechanismProperties = mechanismProperties + self.password = password + self.source = source + self.username = username + } +} + +/// Possible authentication mechanisms. +internal enum AuthMechanism: String, Decodable { + case scramSHA1 = "SCRAM-SHA-1" + case scramSHA256 = "SCRAM-SHA-256" + case gssAPI = "GSSAPI" + case mongodbCR = "MONGODB-CR" + case mongodbX509 = "MONGODB-X509" + case plain = "PLAIN" +} diff --git a/Sources/TestsCommon/CommonTestUtils.swift b/Sources/TestsCommon/CommonTestUtils.swift index 75021562c..68ff78563 100644 --- a/Sources/TestsCommon/CommonTestUtils.swift +++ b/Sources/TestsCommon/CommonTestUtils.swift @@ -1,44 +1,55 @@ -import CLibMongoC import Foundation @testable import MongoSwift import Nimble import XCTest +extension String { + /// Removes the first occurrence of the specified substring from the string. If the substring is not present, has + /// no effect. + fileprivate mutating func removeSubstring(_ s: String) { + guard s.count <= self.count else { + return + } + for i in 0...(self.count - s.count) { + let startIdx = self.index(self.startIndex, offsetBy: i) + let endIdx = self.index(startIdx, offsetBy: s.count) + if self[startIdx.. String in - let ptr = rawPtr.baseAddress!.assumingMemoryBound(to: CChar.self) - return String(cString: ptr) - } - - return "mongodb://\(hostAndPort)/" - } + /// Gets the connection string to use from the environment variable, $MONGODB_URI. If the variable does not exist, + /// will return a default of "mongodb://127.0.0.1/". If singleMongos is true and this is a sharded topology, will + /// edit $MONGODB_URI as needed so that it only contains a single host. + public static func getConnectionString(singleMongos: Bool = true) -> String { + guard let uri = ProcessInfo.processInfo.environment["MONGODB_URI"] else { + return "mongodb://127.0.0.1/" + } - return connStr + // we only need to manipulate the URI if singleMongos is requested and the topology is sharded. + guard singleMongos && MongoSwiftTestCase.topologyType == .sharded else { + return uri } - return "mongodb://127.0.0.1/" + guard let hosts = try? ConnectionString(uri).hosts else { + return uri + } + + var output = uri + // remove all but the first host so we connect to a single mongos. + for host in hosts[1...] { + output.removeSubstring(",\(host)") + } + return output } // indicates whether we are running on a 32-bit platform @@ -79,6 +90,11 @@ open class MongoSwiftTestCase: XCTestCase { public static var sslCAFilePath: String? { return ProcessInfo.processInfo.environment["SSL_CA_FILE"] } + + /// Indicates that we are running the tests with auth enabled, determined by the environment variable $AUTH. + public static var auth: Bool { + return ProcessInfo.processInfo.environment["AUTH"] == "auth" + } } extension Document { @@ -208,16 +224,6 @@ public struct TestError: LocalizedError { } } -/// Possible authentication mechanisms. -public enum AuthMechanism: String, Decodable { - case scramSHA1 = "SCRAM-SHA-1" - case scramSHA256 = "SCRAM-SHA-256" - case gssAPI = "GSSAPI" - case mongodbCR = "MONGODB-CR" - case mongodbX509 = "MONGODB-X509" - case plain = "PLAIN" -} - /// Makes `Address` `Decodable` for the sake of constructing it from spec test files. extension Address: Decodable { public init(from decoder: Decoder) throws { diff --git a/Tests/MongoSwiftSyncTests/ReadWriteConcernOperationTests.swift b/Tests/MongoSwiftSyncTests/ReadWriteConcernOperationTests.swift index 66f740de5..93f4c0925 100644 --- a/Tests/MongoSwiftSyncTests/ReadWriteConcernOperationTests.swift +++ b/Tests/MongoSwiftSyncTests/ReadWriteConcernOperationTests.swift @@ -1,4 +1,3 @@ -import CLibMongoC @testable import MongoSwift import MongoSwiftSync import Nimble diff --git a/Tests/MongoSwiftSyncTests/SDAMMonitoringTests.swift b/Tests/MongoSwiftSyncTests/SDAMMonitoringTests.swift index c773791b4..6a0227264 100644 --- a/Tests/MongoSwiftSyncTests/SDAMMonitoringTests.swift +++ b/Tests/MongoSwiftSyncTests/SDAMMonitoringTests.swift @@ -1,4 +1,3 @@ -import CLibMongoC import Foundation @testable import MongoSwift import MongoSwiftSync @@ -21,10 +20,6 @@ final class SDAMTests: MongoSwiftTestCase { expect(desc.type).to(equal(ServerDescription.ServerType.unknown)) } - func checkDefaultHostPort(_ desc: ServerDescription, _ hostlist: UnsafePointer) { - expect(desc.address).to(equal(Address(hostlist))) - } - // Basic test based on the "standalone" spec test for SDAM monitoring: // swiftlint:disable line_length // https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/tests/monitoring/standalone.json @@ -60,12 +55,13 @@ final class SDAMTests: MongoSwiftTestCase { center.removeObserver(observer) - let connString = try ConnectionString(MongoSwiftTestCase.connStr) + let connString = try ConnectionString(MongoSwiftTestCase.getConnectionString()) - guard let hostlist = mongoc_uri_get_hosts(connString._uri) else { - XCTFail("Could not get hostlists for uri: \(MongoSwiftTestCase.connStr)") + guard let host = connString.hosts?[0] else { + XCTFail("Could not get hosts for uri: \(MongoSwiftTestCase.getConnectionString())") return } + let hostAddress = try Address(host) // check event count and that events are of the expected types expect(receivedEvents.count).to(beGreaterThanOrEqualTo(5)) @@ -89,17 +85,17 @@ final class SDAMTests: MongoSwiftTestCase { let event2 = receivedEvents[2] as! ServerOpeningEvent expect(event2.topologyId).to(equal(event1.topologyId)) - expect(event2.serverAddress).to(equal(Address(hostlist))) + expect(event2.serverAddress).to(equal(hostAddress)) let event3 = receivedEvents[3] as! ServerDescriptionChangedEvent expect(event3.topologyId).to(equal(event2.topologyId)) let prevServer = event3.previousDescription - self.checkDefaultHostPort(prevServer, hostlist) + expect(prevServer.address).to(equal(hostAddress)) self.checkEmptyLists(prevServer) self.checkUnknownServerType(prevServer) let newServer = event3.newDescription - self.checkDefaultHostPort(newServer, hostlist) + expect(newServer.address).to(equal(hostAddress)) self.checkEmptyLists(newServer) expect(newServer.type).to(equal(ServerDescription.ServerType.standalone)) @@ -111,7 +107,7 @@ final class SDAMTests: MongoSwiftTestCase { let newTopology = event4.newDescription expect(newTopology.type).to(equal(TopologyDescription.TopologyType.single)) - self.checkDefaultHostPort(newTopology.servers[0], hostlist) + expect(newTopology.servers[0].address).to(equal(hostAddress)) expect(newTopology.servers[0].type).to(equal(ServerDescription.ServerType.standalone)) self.checkEmptyLists(newTopology.servers[0]) } diff --git a/Tests/MongoSwiftSyncTests/SyncAuthTests.swift b/Tests/MongoSwiftSyncTests/SyncAuthTests.swift index 64a408722..19905bc72 100644 --- a/Tests/MongoSwiftSyncTests/SyncAuthTests.swift +++ b/Tests/MongoSwiftSyncTests/SyncAuthTests.swift @@ -1,4 +1,5 @@ import Foundation +@testable import MongoSwift import MongoSwiftSync import Nimble import TestsCommon @@ -34,8 +35,11 @@ struct TestUser { // we want to split right after the // to insert the username and password. let splitIdx = connStr.index(firstSlash, offsetBy: 2) + // if the connection string already has a username, remove the portion up through the @ sign to get what should + // come after the username. + let afterUsername = MongoSwiftTestCase.auth ? connStr.drop { $0 != "@" }.dropFirst() : connStr[splitIdx...] - let joined = "\(connStr[.. MongoClient { var opts = options ?? ClientOptions() @@ -101,10 +101,8 @@ extension MongoClient { return try MongoClient(uri, options: opts) } - internal func supportsFailCommand() -> Bool { - guard let version = try? self.serverVersion() else { - return false - } + internal func supportsFailCommand() throws -> Bool { + let version = try self.serverVersion() switch MongoSwiftTestCase.topologyType { case .sharded: return version >= ServerVersion(major: 4, minor: 1, patch: 5) diff --git a/Tests/MongoSwiftTests/AsyncTestUtils.swift b/Tests/MongoSwiftTests/AsyncTestUtils.swift index aa07ecef0..069dfc342 100644 --- a/Tests/MongoSwiftTests/AsyncTestUtils.swift +++ b/Tests/MongoSwiftTests/AsyncTestUtils.swift @@ -6,12 +6,13 @@ import XCTest extension MongoClient { fileprivate static func makeTestClient( - _ uri: String = MongoSwiftTestCase.connStr, + _ uri: String = MongoSwiftTestCase.getConnectionString(), eventLoopGroup: EventLoopGroup, options: ClientOptions? = nil ) throws -> MongoClient { var opts = options ?? ClientOptions() - if MongoSwiftTestCase.ssl { + // if SSL is on and custom TLS options were not provided, enable them + if MongoSwiftTestCase.ssl && opts.tlsOptions == nil { opts.tlsOptions = TLSOptions( caFile: URL(string: MongoSwiftTestCase.sslCAFilePath ?? ""), pemFile: URL(string: MongoSwiftTestCase.sslPEMKeyFilePath ?? "") @@ -50,10 +51,14 @@ extension MongoCollection { } extension MongoSwiftTestCase { - internal func withTestClient(options: ClientOptions? = nil, f: (MongoClient) throws -> T) throws -> T { + internal func withTestClient( + _ uri: String = MongoSwiftTestCase.getConnectionString(), + options: ClientOptions? = nil, + f: (MongoClient) throws -> T + ) throws -> T { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { elg.syncShutdownOrFail() } - let client = try MongoClient.makeTestClient(eventLoopGroup: elg, options: options) + let client = try MongoClient.makeTestClient(uri, eventLoopGroup: elg, options: options) defer { client.syncCloseOrFail() } return try f(client) } diff --git a/Tests/MongoSwiftTests/AuthTests.swift b/Tests/MongoSwiftTests/AuthTests.swift index bed609c01..203e39588 100644 --- a/Tests/MongoSwiftTests/AuthTests.swift +++ b/Tests/MongoSwiftTests/AuthTests.swift @@ -1,84 +1,8 @@ -import CLibMongoC import Foundation @testable import MongoSwift import Nimble import TestsCommon -/// An extension adding accessors for a number of options that may be set on a `ConnectionString`. -extension ConnectionString { - /// Returns the username if one was provided, otherwise nil. - private var username: String? { - guard let username = mongoc_uri_get_username(self._uri) else { - return nil - } - return String(cString: username) - } - - /// Returns the password if one was provided, otherwise nil. - private var password: String? { - guard let pw = mongoc_uri_get_password(self._uri) else { - return nil - } - return String(cString: pw) - } - - /// Returns the auth database if one was provided, otherwise nil. - private var authSource: String? { - guard let source = mongoc_uri_get_auth_source(self._uri) else { - return nil - } - return String(cString: source) - } - - /// Returns the auth mechanism if one was provided, otherwise nil. - private var authMechanism: AuthMechanism? { - guard let mechanism = mongoc_uri_get_auth_mechanism(self._uri) else { - return nil - } - let str = String(cString: mechanism) - return AuthMechanism(rawValue: str) - } - - /// Returns a document containing the auth mechanism properties if any were provided, otherwise nil. - private var authMechanismProperties: Document? { - var props = bson_t() - return withUnsafeMutablePointer(to: &props) { propsPtr in - let opaquePtr = OpaquePointer(propsPtr) - guard mongoc_uri_get_mechanism_properties(self._uri, opaquePtr) else { - return nil - } - /// This copy should not be returned directly as its only guaranteed valid for as long as the - /// `mongoc_uri_t`, as `props` was statically initialized from data stored in the URI and may contain - /// pointers that will be invalidated once the URI is. - let copy = Document(copying: opaquePtr) - - return copy.mapValues { value in - // mongoc returns boolean options e.g. CANONICALIZE_HOSTNAME as strings, but they are booleans in the - // spec test file. - switch value { - case "true": - return true - case "false": - return false - default: - return value - } - } - } - } - - /// Returns the credential configured on this URI. Will be empty if no options are set. - fileprivate var credential: Credential { - return Credential( - username: self.username, - password: self.password, - source: self.authSource, - mechanism: self.authMechanism, - mechanismProperties: self.authMechanismProperties - ) - } -} - /// Represents a single file containing auth tests. struct AuthTestFile: Decodable { let tests: [AuthTestCase] @@ -97,60 +21,6 @@ struct AuthTestCase: Decodable { let credential: Credential? } -/// Represents an authentication credential. -struct Credential: Decodable, Equatable { - /// A string containing the username. For auth mechanisms that do not utilize a password, this may be the entire - /// `userinfo` token from the connection string. - let username: String? - /// A string containing the password. - let password: String? - /// A string containing the authentication database. - let source: String? - /// The authentication mechanism. A nil value for this key is used to indicate that a mechanism wasn't specified - /// and that mechanism negotiation is required. - let mechanism: AuthMechanism? - /// A document containing mechanism-specific properties. - let mechanismProperties: Document? - - private enum CodingKeys: String, CodingKey { - case username, password, source, mechanism, mechanismProperties = "mechanism_properties" - } - - // TODO: SWIFT-636: remove this initializer and the one below it. - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.username = try container.decodeIfPresent(String.self, forKey: .username) - self.password = try container.decodeIfPresent(String.self, forKey: .password) - self.source = try container.decodeIfPresent(String.self, forKey: .source) - self.mechanism = try container.decodeIfPresent(AuthMechanism.self, forKey: .mechanism) - - // libmongoc does not return the service name if it's the default, but it is contained in the spec test files, - // so filter it out here if it's present. - let properties = try container.decodeIfPresent(Document.self, forKey: .mechanismProperties) - let filteredProperties = properties?.filter { !($0.0 == "SERVICE_NAME" && $0.1 == "mongodb") } - // if SERVICE_NAME was the only key then don't return an empty document. - if filteredProperties?.isEmpty == true { - self.mechanismProperties = nil - } else { - self.mechanismProperties = filteredProperties - } - } - - init( - username: String?, - password: String?, - source: String?, - mechanism: AuthMechanism?, - mechanismProperties: Document? - ) { - self.mechanism = mechanism - self.mechanismProperties = mechanismProperties - self.password = password - self.source = source - self.username = username - } -} - final class AuthTests: MongoSwiftTestCase { func testAuthConnectionStrings() throws { let testFiles = try retrieveSpecTestFiles(specName: "auth", asType: AuthTestFile.self) diff --git a/Tests/MongoSwiftSyncTests/DNSSeedlistTests.swift b/Tests/MongoSwiftTests/DNSSeedlistTests.swift similarity index 62% rename from Tests/MongoSwiftSyncTests/DNSSeedlistTests.swift rename to Tests/MongoSwiftTests/DNSSeedlistTests.swift index 077dced1d..dbc6a0241 100644 --- a/Tests/MongoSwiftSyncTests/DNSSeedlistTests.swift +++ b/Tests/MongoSwiftTests/DNSSeedlistTests.swift @@ -1,5 +1,5 @@ import Foundation -import MongoSwiftSync +@testable import MongoSwift import Nimble import TestsCommon import XCTest @@ -21,6 +21,10 @@ struct DNSSeedlistTestCase: Decodable { let error: Bool? /// A comment to indicate why a test would fail. let comment: String? + + private enum CodingKeys: String, CodingKey { + case uri, seeds, hosts, options, parsedOptions = "parsed_options", error, comment + } } final class DNSSeedlistTests: MongoSwiftTestCase { @@ -29,6 +33,7 @@ final class DNSSeedlistTests: MongoSwiftTestCase { } // Note: the file txt-record-with-overridden-uri-option.json causes a mongoc warning. This is expected. + // swiftlint:disable:next cyclomatic_complexity func testInitialDNSSeedlistDiscovery() throws { guard MongoSwiftTestCase.ssl else { print("Skipping test, requires SSL") @@ -44,11 +49,6 @@ final class DNSSeedlistTests: MongoSwiftTestCase { asType: DNSSeedlistTestCase.self ) for (filename, testCase) in tests { - // TODO: SWIFT-593: run these tests - guard !["encoded-userinfo-and-db.json", "uri-with-auth.json"].contains(filename) else { - continue - } - // listen for TopologyDescriptionChanged events and continually record the latest description we've seen. let center = NotificationCenter.default var lastTopologyDescription: TopologyDescription? @@ -62,21 +62,19 @@ final class DNSSeedlistTests: MongoSwiftTestCase { defer { center.removeObserver(observer) } // Enclose all of the potentially throwing code in `doTest`. Sometimes the expected errors come when - // parsing the URI, and other times they are not until we try to send a command. - func doTest() throws { - let opts = TLSOptions( - allowInvalidHostnames: true, + // parsing the URI, and other times they are not until we try to select a server. + func doTest() throws -> ConnectionString { + let tlsOpts = TLSOptions( caFile: URL(string: MongoSwiftTestCase.sslCAFilePath ?? ""), - pemFile: URL(string: MongoSwiftTestCase.sslPEMKeyFilePath ?? "") - ) - let client = try MongoClient( - testCase.uri, - options: ClientOptions(serverMonitoring: true, tlsOptions: opts) + pemFile: URL(string: MongoSwiftTestCase.sslPEMKeyFilePath ?? ""), + weakCertValidation: true ) - - // mongoc connects lazily so we need to send a command. - let db = client.db("test") - _ = try db.runCommand(["isMaster": 1]) + let opts = ClientOptions(serverMonitoring: true, tlsOptions: tlsOpts) + return try self.withTestClient(testCase.uri, options: opts) { client in + // try selecting a server to trigger SDAM + _ = try client.connectionPool.selectServer(forWrites: false) + return try client.connectionPool.getConnectionString() + } } // "You MUST verify that an error has been thrown if error is present." @@ -85,7 +83,13 @@ final class DNSSeedlistTests: MongoSwiftTestCase { continue } - expect(try doTest()).toNot(throwError(), description: testCase.comment ?? "") + let connStr: ConnectionString + do { + connStr = try doTest() + } catch { + XCTFail("Expected no error for test case \(testCase.comment ?? ""), got \(error)") + continue + } // "You MUST verify that the set of ServerDescriptions in the client's TopologyDescription eventually // matches the list of hosts." @@ -93,9 +97,28 @@ final class DNSSeedlistTests: MongoSwiftTestCase { // "You MUST verify that each of the values of the Connection String Options under options match the // Client's parsed value for that option." - // TODO: SWIFT-597: Implement these assertions. Not possible now. + let connStrOptions = connStr.options ?? [:] + for (k, v) in Array(testCase.options ?? [:]) + Array(testCase.parsedOptions ?? [:]) { + switch k { + // the test files still use SSL, but libmongoc uses TLS + case "ssl": + expect(connStrOptions["tls"]).to(equal(v)) + // these values are not returned as part of the options doc + case "authSource", "auth_database": + expect(connStr.authSource).to(equal(v.stringValue)) + case "user": + expect(connStr.username).to(equal(v.stringValue)) + case "password": + expect(connStr.password).to(equal(v.stringValue)) + case "db": + expect(connStr.db).to(equal(v.stringValue)) + default: + // there are some case inconsistencies between the tests and libmongoc + expect(connStrOptions[k.lowercased()]).to(equal(v)) + } + } - // Note: we also skip this assertion: "You SHOULD verify that the client's initial seed list matches the + // Note: we skip this assertion: "You SHOULD verify that the client's initial seed list matches the // list of seeds." mongoc doesn't make this assertion in their test runner either. } }