From aec5f6912a2951d8b09e8d093457a6400e204f4b Mon Sep 17 00:00:00 2001 From: Will Banfield Date: Tue, 7 Nov 2017 13:55:22 -0500 Subject: [PATCH] GODRIVER-89 Implement Initial DNS Seedlist discovery spec --- .evergreen/.evg.yml | 4 + Makefile | 4 + .../initial-dns-seedlist-discovery/README.rst | 57 ++++++++++ .../no-results.json | 5 + .../no-results.yml | 3 + .../one-result-default-port.json | 11 ++ .../one-result-default-port.yml | 7 ++ .../one-txt-record.json | 16 +++ .../one-txt-record.yml | 11 ++ .../two-results-default-port.json | 12 ++ .../two-results-default-port.yml | 8 ++ .../two-results-nonstandard-port.json | 12 ++ .../two-results-nonstandard-port.yml | 8 ++ .../two-txt-records-with-override.json | 16 +++ .../two-txt-records-with-override.yml | 11 ++ .../two-txt-records.json | 16 +++ .../two-txt-records.yml | 11 ++ mongo/connstring/connstring.go | 74 +++++++++--- mongo/connstring/connstring_spec_test.go | 58 +--------- mongo/internal/testutil/helpers/helpers.go | 66 +++++++++++ .../initial_dns_seedlist_discovery_test.go | 107 ++++++++++++++++++ 21 files changed, 448 insertions(+), 69 deletions(-) create mode 100644 data/initial-dns-seedlist-discovery/README.rst create mode 100644 data/initial-dns-seedlist-discovery/no-results.json create mode 100644 data/initial-dns-seedlist-discovery/no-results.yml create mode 100644 data/initial-dns-seedlist-discovery/one-result-default-port.json create mode 100644 data/initial-dns-seedlist-discovery/one-result-default-port.yml create mode 100644 data/initial-dns-seedlist-discovery/one-txt-record.json create mode 100644 data/initial-dns-seedlist-discovery/one-txt-record.yml create mode 100644 data/initial-dns-seedlist-discovery/two-results-default-port.json create mode 100644 data/initial-dns-seedlist-discovery/two-results-default-port.yml create mode 100644 data/initial-dns-seedlist-discovery/two-results-nonstandard-port.json create mode 100644 data/initial-dns-seedlist-discovery/two-results-nonstandard-port.yml create mode 100644 data/initial-dns-seedlist-discovery/two-txt-records-with-override.json create mode 100644 data/initial-dns-seedlist-discovery/two-txt-records-with-override.yml create mode 100644 data/initial-dns-seedlist-discovery/two-txt-records.json create mode 100644 data/initial-dns-seedlist-discovery/two-txt-records.yml create mode 100644 mongo/private/cluster/initial_dns_seedlist_discovery_test.go diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 4ef20caa8c..e1e4a11783 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -342,6 +342,10 @@ tasks: tags: ["test", "replicaset"] commands: - func: bootstrap-mongo-orchestration + vars: + TOPOLOGY: "replica_set" + AUTH: "noauth" + SSL: "nossl" - func: run-tests vars: TOPOLOGY: "replica_set" diff --git a/Makefile b/Makefile index 8e55dcbdd6..fd4e1813f7 100644 --- a/Makefile +++ b/Makefile @@ -67,6 +67,10 @@ update-bson-corpus-tests: update-connection-string-tests: etc/update-spec-tests.sh connection-string +.PHONY: update-initial-dns-seedlist-discovery-tests +update-initial-dns-seedlist-discovery-tests: + etc/update-spec-tests.sh initial-dns-seedlist-discovery + .PHONY: update-max-staleness-tests update-max-staleness-tests: etc/update-spec-tests.sh max-staleness diff --git a/data/initial-dns-seedlist-discovery/README.rst b/data/initial-dns-seedlist-discovery/README.rst new file mode 100644 index 0000000000..5c9598329d --- /dev/null +++ b/data/initial-dns-seedlist-discovery/README.rst @@ -0,0 +1,57 @@ +==================================== +Initial DNS Seedlist Discovery tests +==================================== + +This directory contains platform-independent tests that drivers can use +to prove their conformance to the Initial DNS Seedlist Discovery spec. + +Test Setup +---------- + +Start a three-node replica set on localhost, on ports 27017, 27018, and 27019, +with replica set name "repl0". + +To run the tests that accompany this spec, you need to configure the SRV and +TXT records with a real name server. The following records are required for +these tests:: + + Record TTL Class Port Target + _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. + _mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27018 localhost.build.10gen.cc. + _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27018 localhost.build.10gen.cc. + _mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27019 localhost.build.10gen.cc. + _mongodb._tcp.test3.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. + _mongodb._tcp.test5.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. + _mongodb._tcp.test6.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc. + + Record TTL Class Text + test5.test.build.10gen.cc. 86400 IN TXT "connectTimeoutMS=300000&socketTimeoutMS=300000" + test6.test.build.10gen.cc. 86400 IN TXT "connectTimeoutMS=200000" + test6.test.build.10gen.cc. 86400 IN TXT "socketTimeoutMS=200000" + +Note that ``test4`` is omitted deliberately to test what happens with no SRV +record. + +In our tests we have used "localhost.build.10gen.cc" as the domain, and then +configured "localhost.build.10gen.cc" to resolve to 127.0.0.1. + +You need to adapt the records shown above to replace ``build.10gen.cc`` with +your own domain name, and update the "uri" field in the YAML or JSON files in +this directory with the actual domain. + +Test Format and Use +------------------- + +These YAML and JSON files contain the following fields: + +- ``uri``: a mongodb+srv connection string +- ``seeds``: the expected set of initial seeds discovered from the SRV record +- ``hosts``: the discovered topology's list of hosts once SDAM completes a scan +- ``options``: the parsed connection string options as discovered from URI and + TXT records + +For each file, create MongoClient initialized with the mongodb+srv connection +string. You SHOULD verify that the client's initial seed list matches the list of +seeds. You MUST verify that the set of ServerDescriptions in the client's +TopologyDescription eventually matches the list of hosts. You MUST verify that +the set of Connection String Options matches the client's parsed set. diff --git a/data/initial-dns-seedlist-discovery/no-results.json b/data/initial-dns-seedlist-discovery/no-results.json new file mode 100644 index 0000000000..16f7167ff8 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/no-results.json @@ -0,0 +1,5 @@ +{ + "uri": "mongodb+srv://test4.test.build.10gen.cc/", + "seeds": [], + "hosts": [] +} diff --git a/data/initial-dns-seedlist-discovery/no-results.yml b/data/initial-dns-seedlist-discovery/no-results.yml new file mode 100644 index 0000000000..83ce04ce47 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/no-results.yml @@ -0,0 +1,3 @@ +uri: "mongodb+srv://test4.test.build.10gen.cc/" +seeds: [] +hosts: [] diff --git a/data/initial-dns-seedlist-discovery/one-result-default-port.json b/data/initial-dns-seedlist-discovery/one-result-default-port.json new file mode 100644 index 0000000000..37d51168b4 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/one-result-default-port.json @@ -0,0 +1,11 @@ +{ + "uri": "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ] +} diff --git a/data/initial-dns-seedlist-discovery/one-result-default-port.yml b/data/initial-dns-seedlist-discovery/one-result-default-port.yml new file mode 100644 index 0000000000..e634e247b7 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/one-result-default-port.yml @@ -0,0 +1,7 @@ +uri: "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 diff --git a/data/initial-dns-seedlist-discovery/one-txt-record.json b/data/initial-dns-seedlist-discovery/one-txt-record.json new file mode 100644 index 0000000000..aadf737860 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/one-txt-record.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test5.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "connectTimeoutMS": 300000, + "replicaSet": "repl0", + "socketTimeoutMS": 300000 + } +} diff --git a/data/initial-dns-seedlist-discovery/one-txt-record.yml b/data/initial-dns-seedlist-discovery/one-txt-record.yml new file mode 100644 index 0000000000..14210a500f --- /dev/null +++ b/data/initial-dns-seedlist-discovery/one-txt-record.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test5.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + connectTimeoutMS: 300000 + replicaSet: repl0 + socketTimeoutMS: 300000 diff --git a/data/initial-dns-seedlist-discovery/two-results-default-port.json b/data/initial-dns-seedlist-discovery/two-results-default-port.json new file mode 100644 index 0000000000..a436bdaa96 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-results-default-port.json @@ -0,0 +1,12 @@ +{ + "uri": "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.build.10gen.cc:27017", + "localhost.build.10gen.cc:27018" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ] +} diff --git a/data/initial-dns-seedlist-discovery/two-results-default-port.yml b/data/initial-dns-seedlist-discovery/two-results-default-port.yml new file mode 100644 index 0000000000..4ed4f31ee7 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-results-default-port.yml @@ -0,0 +1,8 @@ +uri: "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.build.10gen.cc:27017 + - localhost.build.10gen.cc:27018 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 diff --git a/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.json b/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.json new file mode 100644 index 0000000000..536db6ed39 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.json @@ -0,0 +1,12 @@ +{ + "uri": "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.build.10gen.cc:27018", + "localhost.build.10gen.cc:27019" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ] +} diff --git a/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.yml b/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.yml new file mode 100644 index 0000000000..d62e62ae51 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-results-nonstandard-port.yml @@ -0,0 +1,8 @@ +uri: "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.build.10gen.cc:27018 + - localhost.build.10gen.cc:27019 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 diff --git a/data/initial-dns-seedlist-discovery/two-txt-records-with-override.json b/data/initial-dns-seedlist-discovery/two-txt-records-with-override.json new file mode 100644 index 0000000000..266d467634 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-txt-records-with-override.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0&connectTimeoutMS=250000", + "seeds": [ + "localhost.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "connectTimeoutMS": 250000, + "replicaSet": "repl0", + "socketTimeoutMS": 200000 + } +} diff --git a/data/initial-dns-seedlist-discovery/two-txt-records-with-override.yml b/data/initial-dns-seedlist-discovery/two-txt-records-with-override.yml new file mode 100644 index 0000000000..f31797be63 --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-txt-records-with-override.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0&connectTimeoutMS=250000" +seeds: + - localhost.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + connectTimeoutMS: 250000 + replicaSet: repl0 + socketTimeoutMS: 200000 diff --git a/data/initial-dns-seedlist-discovery/two-txt-records.json b/data/initial-dns-seedlist-discovery/two-txt-records.json new file mode 100644 index 0000000000..6a02fccf1d --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-txt-records.json @@ -0,0 +1,16 @@ +{ + "uri": "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0", + "seeds": [ + "localhost.build.10gen.cc:27017" + ], + "hosts": [ + "localhost:27017", + "localhost:27018", + "localhost:27019" + ], + "options": { + "connectTimeoutMS": 200000, + "replicaSet": "repl0", + "socketTimeoutMS": 200000 + } +} diff --git a/data/initial-dns-seedlist-discovery/two-txt-records.yml b/data/initial-dns-seedlist-discovery/two-txt-records.yml new file mode 100644 index 0000000000..09bd17024c --- /dev/null +++ b/data/initial-dns-seedlist-discovery/two-txt-records.yml @@ -0,0 +1,11 @@ +uri: "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0" +seeds: + - localhost.build.10gen.cc:27017 +hosts: + - localhost:27017 + - localhost:27018 + - localhost:27019 +options: + connectTimeoutMS: 200000 + replicaSet: repl0 + socketTimeoutMS: 200000 diff --git a/mongo/connstring/connstring.go b/mongo/connstring/connstring.go index ebf8a5426c..8eb0c37b90 100644 --- a/mongo/connstring/connstring.go +++ b/mongo/connstring/connstring.go @@ -82,17 +82,21 @@ type parser struct { func (p *parser) parse(original string) error { p.Original = original - uri := original + var err error - // scheme - if !strings.HasPrefix(uri, "mongodb://") { - return fmt.Errorf("scheme must be \"mongodb\"") + var isSRV bool + if strings.HasPrefix(uri, "mongodb+srv://") { + isSRV = true + // remove the scheme + uri = uri[14:] + } else if strings.HasPrefix(uri, "mongodb://") { + // remove the scheme + uri = uri[10:] + } else { + return fmt.Errorf("scheme must be \"mongodb\" or \"mongodb+srv\"") } - // user info - uri = uri[10:] - if idx := strings.Index(uri, "@"); idx != -1 { userInfo := uri[:idx] uri = uri[idx+1:] @@ -128,10 +132,9 @@ func (p *parser) parse(original string) error { return internal.WrapErrorf(err, "invalid password") } } - } - // hosts + // fetch the hosts field hosts := uri if idx := strings.IndexAny(uri, "/?@"); idx != -1 { if uri[idx] == '@' { @@ -140,17 +143,36 @@ func (p *parser) parse(original string) error { if uri[idx] == '?' { return fmt.Errorf("must have a / before the query ?") } - hosts = uri[:idx] } - for _, host := range strings.Split(hosts, ",") { + var connectionArgsFromTXT []string + parsedHosts := strings.Split(hosts, ",") + + if isSRV { + parsedHosts = strings.Split(hosts, ",") + if len(parsedHosts) != 1 { + return fmt.Errorf("URI with SRV must include one and only one hostname") + } + parsedHosts, err = fetchSeedlistFromSRV(parsedHosts[0]) + if err != nil { + return err + } + + // error ignored because finding a TXT record should not be + // considered an error. + recordsFromTXT, _ := net.LookupTXT(hosts) + for _, recordFromTXT := range recordsFromTXT { + connectionArgsFromTXT = append(connectionArgsFromTXT, strings.FieldsFunc(recordFromTXT, func(r rune) bool { return r == ';' || r == '&' })...) + } + } + + for _, host := range parsedHosts { err = p.addHost(host) if err != nil { return internal.WrapErrorf(err, "invalid host \"%s\"", host) } } - if len(p.Hosts) == 0 { return fmt.Errorf("must have at least 1 host") } @@ -195,7 +217,10 @@ func (p *parser) parse(original string) error { return nil } - for _, pair := range strings.FieldsFunc(uri, func(r rune) bool { return r == ';' || r == '&' }) { + connectionArgsFromQueryString := strings.FieldsFunc(uri, func(r rune) bool { return r == ';' || r == '&' }) + connectionArgPairs := append(connectionArgsFromTXT, connectionArgsFromQueryString...) + + for _, pair := range connectionArgPairs { err = p.addOption(pair) if err != nil { return err @@ -205,6 +230,29 @@ func (p *parser) parse(original string) error { return nil } +func fetchSeedlistFromSRV(host string) ([]string, error) { + var err error + + _, _, err = net.SplitHostPort(host) + + if err == nil { + // we were able to successfully extract a port from the host, + // but should not be able to when using SRV + return nil, fmt.Errorf("URI with srv must not include a port number") + } + + _, addresses, err := net.LookupSRV("mongodb", "tcp", host) + if err != nil { + return nil, err + } + parsedHosts := make([]string, len(addresses)) + for i, address := range addresses { + parsedHosts[i] = fmt.Sprintf("%s:%d", strings.TrimSuffix(address.Target, "."), address.Port) + } + + return parsedHosts, nil +} + func (p *parser) addHost(host string) error { if host == "" { return nil diff --git a/mongo/connstring/connstring_spec_test.go b/mongo/connstring/connstring_spec_test.go index a66545cff0..d4e635cb50 100644 --- a/mongo/connstring/connstring_spec_test.go +++ b/mongo/connstring/connstring_spec_test.go @@ -8,13 +8,12 @@ package connstring_test import ( "encoding/json" - "fmt" "io/ioutil" "path" "testing" - "time" "github.com/10gen/mongo-go-driver/mongo/connstring" + "github.com/10gen/mongo-go-driver/mongo/internal/testutil/helpers" "github.com/stretchr/testify/require" ) @@ -78,17 +77,6 @@ func hostsToStrings(hosts []host) []string { return out } -// Convert each interface{} value in the map to a string. -func mapInterfaceToString(m map[string]interface{}) map[string]string { - out := make(map[string]string) - - for key, value := range m { - out[key] = fmt.Sprint(value) - } - - return out -} - func runTestsInFile(t *testing.T, filename string) { filepath := path.Join(testsDir, filename) content, err := ioutil.ReadFile(filepath) @@ -134,49 +122,7 @@ func runTest(t *testing.T, filename string, test *testCase) { } // Check that all options are present. - for key, value := range test.Options { - switch key { - case "appname": - require.Equal(t, value, cs.AppName) - case "authsource": - require.Equal(t, value, cs.AuthSource) - case "authmechanism": - require.Equal(t, value, cs.AuthMechanism) - case "authmechanismproperties": - convertedMap := value.(map[string]interface{}) - require.Equal(t, - mapInterfaceToString(convertedMap), - cs.AuthMechanismProperties) - case "heartbeatfrequencyms": - require.Equal(t, value, float64(cs.HeartbeatInterval/time.Millisecond)) - case "maxidletimems": - require.Equal(t, value, cs.MaxConnIdleTime) - case "maxconnlifetimems": - require.Equal(t, value, cs.MaxConnLifeTime) - case "maxconnsperhost": - require.True(t, cs.MaxIdleConnsPerHostSet) - require.Equal(t, value, cs.MaxIdleConnsPerHost) - case "maxidleconnsperhost": - require.True(t, cs.MaxIdleConnsPerHostSet) - require.Equal(t, value, cs.MaxIdleConnsPerHost) - case "readpreference": - require.Equal(t, value, cs.ReadPreference) - case "readpreferencetags": - require.Equal(t, value, cs.ReadPreferenceTagSets) - case "replicaset": - require.Equal(t, value, cs.ReplicaSet) - case "serverselectiontimeoutms": - require.Equal(t, value, float64(cs.ServerSelectionTimeout/time.Millisecond)) - case "sockettimeoutms": - require.Equal(t, value, float64(cs.SocketTimeout/time.Millisecond)) - case "wtimeoutms": - require.Equal(t, value, float64(cs.WTimeout/time.Millisecond)) - default: - opt, ok := cs.UnknownOptions[key] - require.True(t, ok) - require.Contains(t, opt, fmt.Sprint(value)) - } - } + testhelpers.VerifyConnStringOptions(t, cs, test.Options) // Check that non-present options are unset. This will be redundant with the above checks // for options that are present. diff --git a/mongo/internal/testutil/helpers/helpers.go b/mongo/internal/testutil/helpers/helpers.go index de583cb7fc..fc45d9bb6c 100644 --- a/mongo/internal/testutil/helpers/helpers.go +++ b/mongo/internal/testutil/helpers/helpers.go @@ -7,13 +7,17 @@ package testhelpers import ( + "fmt" "io/ioutil" "path" + "strings" + "time" "testing" "io" + "github.com/10gen/mongo-go-driver/mongo/connstring" "github.com/stretchr/testify/require" ) @@ -37,3 +41,65 @@ func FindJSONFilesInDir(t *testing.T, dir string) []string { func RequireNoErrorOnClose(t *testing.T, c io.Closer) { require.NoError(t, c.Close()) } + +func VerifyConnStringOptions(t *testing.T, cs connstring.ConnString, options map[string]interface{}) { + // Check that all options are present. + for key, value := range options { + + key = strings.ToLower(key) + switch key { + case "appname": + require.Equal(t, value, cs.AppName) + case "authsource": + require.Equal(t, value, cs.AuthSource) + case "authmechanism": + require.Equal(t, value, cs.AuthMechanism) + case "authmechanismproperties": + convertedMap := value.(map[string]interface{}) + require.Equal(t, + mapInterfaceToString(convertedMap), + cs.AuthMechanismProperties) + case "connecttimeoutms": + require.Equal(t, value, float64(cs.ConnectTimeout/time.Millisecond)) + case "heartbeatfrequencyms": + require.Equal(t, value, float64(cs.HeartbeatInterval/time.Millisecond)) + case "maxidletimems": + require.Equal(t, value, cs.MaxConnIdleTime) + case "maxconnlifetimems": + require.Equal(t, value, cs.MaxConnLifeTime) + case "maxconnsperhost": + require.True(t, cs.MaxIdleConnsPerHostSet) + require.Equal(t, value, cs.MaxIdleConnsPerHost) + case "maxidleconnsperhost": + require.True(t, cs.MaxIdleConnsPerHostSet) + require.Equal(t, value, cs.MaxIdleConnsPerHost) + case "readpreference": + require.Equal(t, value, cs.ReadPreference) + case "readpreferencetags": + require.Equal(t, value, cs.ReadPreferenceTagSets) + case "replicaset": + require.Equal(t, value, cs.ReplicaSet) + case "serverselectiontimeoutms": + require.Equal(t, value, float64(cs.ServerSelectionTimeout/time.Millisecond)) + case "sockettimeoutms": + require.Equal(t, value, float64(cs.SocketTimeout/time.Millisecond)) + case "wtimeoutms": + require.Equal(t, value, float64(cs.WTimeout/time.Millisecond)) + default: + opt, ok := cs.UnknownOptions[key] + require.True(t, ok) + require.Contains(t, opt, fmt.Sprint(value)) + } + } +} + +// Convert each interface{} value in the map to a string. +func mapInterfaceToString(m map[string]interface{}) map[string]string { + out := make(map[string]string) + + for key, value := range m { + out[key] = fmt.Sprint(value) + } + + return out +} diff --git a/mongo/private/cluster/initial_dns_seedlist_discovery_test.go b/mongo/private/cluster/initial_dns_seedlist_discovery_test.go new file mode 100644 index 0000000000..43ad88be00 --- /dev/null +++ b/mongo/private/cluster/initial_dns_seedlist_discovery_test.go @@ -0,0 +1,107 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package cluster_test + +import ( + "context" + "encoding/json" + "io/ioutil" + "os" + "path" + "testing" + + "github.com/10gen/mongo-go-driver/mongo/connstring" + "github.com/10gen/mongo-go-driver/mongo/internal/testutil/helpers" + "github.com/10gen/mongo-go-driver/mongo/model" + "github.com/10gen/mongo-go-driver/mongo/private/cluster" + "github.com/stretchr/testify/require" +) + +const seedlistTestDir string = "../../../data/initial-dns-seedlist-discovery/" + +type seedlistTestCase struct { + URI string + Seeds []string + Hosts []string + Options map[string]interface{} +} + +func runSeedlistTest(t *testing.T, filename string, test *seedlistTestCase) { + t.Run(filename, func(t *testing.T) { + cs, err := connstring.Parse(test.URI) + if len(test.Hosts) == 0 { + // require the parsing to have errored + require.Error(t, err) + return + } + require.NoError(t, err) + + // DNS records may be out of order from the test files ordering + seeds := buildSet(test.Seeds) + hosts := buildSet(cs.Hosts) + + require.Equal(t, hosts, seeds) + + testhelpers.VerifyConnStringOptions(t, cs, test.Options) + + // make a cluster from the options + c, err := cluster.New( + cluster.WithConnString(cs), + ) + + require.NoError(t, err) + for _, host := range test.Hosts { + _, err := getServerByAddress(host, c) + require.NoError(t, err) + } + }) + +} + +// Test case for all connection string spec tests. +func TestInitialDNSSeedlistDiscoverySpec(t *testing.T) { + if os.Getenv("TOPOLOGY") != "replica_set" || os.Getenv("AUTH") != "noauth" { + t.Skip("Skipping on non-replica set topology") + } + + for _, fname := range testhelpers.FindJSONFilesInDir(t, seedlistTestDir) { + filepath := path.Join(seedlistTestDir, fname) + content, err := ioutil.ReadFile(filepath) + require.NoError(t, err) + + var testCase seedlistTestCase + require.NoError(t, json.Unmarshal(content, &testCase)) + + fname = fname[:len(fname)-5] + runSeedlistTest(t, fname, &testCase) + } +} + +func buildSet(list []string) map[string]struct{} { + set := map[string]struct{}{} + for _, s := range list { + set[s] = struct{}{} + } + return set +} + +func getServerByAddress(address string, c *cluster.Cluster) (*model.Server, error) { + selectByName := func(_ *model.Cluster, servers []*model.Server) ([]*model.Server, error) { + for _, s := range servers { + if s.Addr.String() == address { + return []*model.Server{s}, nil + } + } + return []*model.Server{}, nil + } + + selectedServer, err := c.SelectServer(context.Background(), selectByName, nil) + if err != nil { + return nil, err + } + return selectedServer.Server.Model(), nil +}