// Copyright (C) MongoDB, Inc. 2022-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 options import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "net" "net/http" "os" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/internal/ptrutil" "go.mongodb.org/mongo-driver/v2/mongo/readconcern" "go.mongodb.org/mongo-driver/v2/mongo/readpref" "go.mongodb.org/mongo-driver/v2/mongo/writeconcern" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/connstring" ) var tClientOptions = reflect.TypeOf(&ClientOptions{}) func TestClientOptions(t *testing.T) { t.Run("ApplyURI/doesn't overwrite previous errors", func(t *testing.T) { uri := "not-mongo-db-uri://" want := fmt.Errorf( "error parsing uri: %w", errors.New(`scheme must be "mongodb" or "mongodb+srv"`)) co := Client().ApplyURI(uri).ApplyURI("mongodb://localhost/") got := co.Validate() if !cmp.Equal(got, want, cmp.Comparer(compareErrors)) { t.Errorf("Did not received expected error. got %v; want %v", got, want) } }) t.Run("Set", func(t *testing.T) { testCases := []struct { name string fn any // method to be run arg any // argument for method field string // field to be set dereference bool // Should we compare a pointer or the field }{ {"AppName", (*ClientOptions).SetAppName, "example-application", "AppName", true}, {"Auth", (*ClientOptions).SetAuth, Credential{Username: "foo", Password: "bar"}, "Auth", true}, {"Compressors", (*ClientOptions).SetCompressors, []string{"zstd", "snappy", "zlib"}, "Compressors", true}, {"ConnectTimeout", (*ClientOptions).SetConnectTimeout, 5 * time.Second, "ConnectTimeout", true}, {"Dialer", (*ClientOptions).SetDialer, testDialer{Num: 12345}, "Dialer", true}, {"HeartbeatInterval", (*ClientOptions).SetHeartbeatInterval, 5 * time.Second, "HeartbeatInterval", true}, {"Hosts", (*ClientOptions).SetHosts, []string{"localhost:27017", "localhost:27018", "localhost:27019"}, "Hosts", true}, {"LocalThreshold", (*ClientOptions).SetLocalThreshold, 5 * time.Second, "LocalThreshold", true}, {"MaxConnIdleTime", (*ClientOptions).SetMaxConnIdleTime, 5 * time.Second, "MaxConnIdleTime", true}, {"MaxPoolSize", (*ClientOptions).SetMaxPoolSize, uint64(250), "MaxPoolSize", true}, {"MinPoolSize", (*ClientOptions).SetMinPoolSize, uint64(10), "MinPoolSize", true}, {"MaxConnecting", (*ClientOptions).SetMaxConnecting, uint64(10), "MaxConnecting", true}, {"PoolMonitor", (*ClientOptions).SetPoolMonitor, &event.PoolMonitor{}, "PoolMonitor", false}, {"Monitor", (*ClientOptions).SetMonitor, &event.CommandMonitor{}, "Monitor", false}, {"ReadConcern", (*ClientOptions).SetReadConcern, readconcern.Majority(), "ReadConcern", false}, {"ReadPreference", (*ClientOptions).SetReadPreference, readpref.SecondaryPreferred(), "ReadPreference", false}, {"Registry", (*ClientOptions).SetRegistry, bson.NewRegistry(), "Registry", false}, {"ReplicaSet", (*ClientOptions).SetReplicaSet, "example-replicaset", "ReplicaSet", true}, {"RetryWrites", (*ClientOptions).SetRetryWrites, true, "RetryWrites", true}, {"ServerSelectionTimeout", (*ClientOptions).SetServerSelectionTimeout, 5 * time.Second, "ServerSelectionTimeout", true}, {"Direct", (*ClientOptions).SetDirect, true, "Direct", true}, {"TLSConfig", (*ClientOptions).SetTLSConfig, &tls.Config{}, "TLSConfig", false}, {"WriteConcern", (*ClientOptions).SetWriteConcern, writeconcern.Majority(), "WriteConcern", false}, {"ZlibLevel", (*ClientOptions).SetZlibLevel, 6, "ZlibLevel", true}, {"DisableOCSPEndpointCheck", (*ClientOptions).SetDisableOCSPEndpointCheck, true, "DisableOCSPEndpointCheck", true}, {"LoadBalanced", (*ClientOptions).SetLoadBalanced, true, "LoadBalanced", true}, } opt1, opt2, optResult := Client(), Client(), Client() for idx, tc := range testCases { t.Run(tc.name, func(t *testing.T) { fn := reflect.ValueOf(tc.fn) if fn.Kind() != reflect.Func { t.Fatal("fn argument must be a function") } if fn.Type().NumIn() < 2 || fn.Type().In(0) != tClientOptions { t.Fatal("fn argument must have a *ClientOptions as the first argument and one other argument") } if _, exists := tClientOptions.Elem().FieldByName(tc.field); !exists { t.Fatalf("field (%s) does not exist in ClientOptions", tc.field) } args := make([]reflect.Value, 2) client := reflect.New(tClientOptions.Elem()) args[0] = client want := reflect.ValueOf(tc.arg) args[1] = want if !want.IsValid() || !want.CanInterface() { t.Fatal("arg property of test case must be valid") } _ = fn.Call(args) // To avoid duplication we're piggybacking on the Set* tests to make the // MergeClientOptions test simpler and more thorough. // To do this we set the odd numbered test cases to the first opt, the even and // divisible by three test cases to the second, and the result of merging the two to // the result option. This gives us coverage of options set by the first option, by // the second, and by both. if idx%2 != 0 { args[0] = reflect.ValueOf(opt1) _ = fn.Call(args) } if idx%2 == 0 || idx%3 == 0 { args[0] = reflect.ValueOf(opt2) _ = fn.Call(args) } args[0] = reflect.ValueOf(optResult) _ = fn.Call(args) got := client.Elem().FieldByName(tc.field) if !got.IsValid() || !got.CanInterface() { t.Fatal("cannot create concrete instance from retrieved field") } if got.Kind() == reflect.Ptr && tc.dereference { got = got.Elem() } if !cmp.Equal( got.Interface(), want.Interface(), cmp.AllowUnexported(readconcern.ReadConcern{}, writeconcern.WriteConcern{}, readpref.ReadPref{}), cmp.Comparer(func(r1, r2 *bson.Registry) bool { return r1 == r2 }), cmp.Comparer(func(cfg1, cfg2 *tls.Config) bool { return cfg1 == cfg2 }), cmp.Comparer(func(fp1, fp2 *event.PoolMonitor) bool { return fp1 == fp2 }), ) { t.Errorf("Field not set properly. got %v; want %v", got.Interface(), want.Interface()) } }) } t.Run("MergeClientOptions/all set", func(t *testing.T) { want := optResult got := MergeClientOptions(nil, opt1, opt2) if diff := cmp.Diff( got, want, cmp.AllowUnexported(readconcern.ReadConcern{}, writeconcern.WriteConcern{}, readpref.ReadPref{}), cmp.Comparer(func(r1, r2 *bson.Registry) bool { return r1 == r2 }), cmp.Comparer(func(cfg1, cfg2 *tls.Config) bool { return cfg1 == cfg2 }), cmp.Comparer(func(fp1, fp2 *event.PoolMonitor) bool { return fp1 == fp2 }), cmp.Comparer(optionsutil.Equal), cmp.AllowUnexported(ClientOptions{}), cmpopts.IgnoreFields(http.Client{}, "Transport"), ); diff != "" { t.Errorf("diff:\n%s", diff) t.Errorf("Merged client options do not match. got %v; want %v", got, want) } }) // go-cmp dont support error comparisons (https://github.com/google/go-cmp/issues/24) // Use specifique test for this t.Run("MergeClientOptions/err", func(t *testing.T) { opt1, opt2 := Client(), Client() opt1.err = errors.New("Test error") got := MergeClientOptions(nil, opt1, opt2) if got.err.Error() != "Test error" { t.Errorf("Merged client options do not match. got %v; want %v", got.err.Error(), opt1.err.Error()) } }) t.Run("MergeClientOptions single nil option", func(t *testing.T) { got := MergeClientOptions(nil) assert.Equal(t, Client(), got) }) t.Run("MergeClientOptions multiple nil options", func(t *testing.T) { got := MergeClientOptions(nil, nil) assert.Equal(t, Client(), got) }) }) t.Run("direct connection validation", func(t *testing.T) { t.Run("multiple hosts", func(t *testing.T) { expectedErr := errors.New("a direct connection cannot be made if multiple hosts are specified") testCases := []struct { name string opts *ClientOptions }{ {"hosts in URI", Client().ApplyURI("mongodb://localhost,localhost2")}, {"hosts in options", Client().SetHosts([]string{"localhost", "localhost2"})}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.opts.SetDirect(true).Validate() assert.NotNil(t, err, "expected error, got nil") assert.Equal(t, expectedErr.Error(), err.Error(), "expected error %v, got %v", expectedErr, err) }) } }) t.Run("srv", func(t *testing.T) { expectedErr := errors.New("a direct connection cannot be made if an SRV URI is used") // Use a non-SRV URI and manually set the scheme because using an SRV URI would force an SRV lookup. opts := Client().ApplyURI("mongodb://localhost:27017") opts.connString.Scheme = connstring.SchemeMongoDBSRV err := opts.SetDirect(true).Validate() assert.NotNil(t, err, "expected error, got nil") assert.Equal(t, expectedErr.Error(), err.Error(), "expected error %v, got %v", expectedErr, err) }) }) t.Run("loadBalanced validation", func(t *testing.T) { testCases := []struct { name string opts *ClientOptions err error }{ {"multiple hosts in URI", Client().ApplyURI("mongodb://foo,bar"), connstring.ErrLoadBalancedWithMultipleHosts}, {"multiple hosts in options", Client().SetHosts([]string{"foo", "bar"}), connstring.ErrLoadBalancedWithMultipleHosts}, {"replica set name", Client().SetReplicaSet("foo"), connstring.ErrLoadBalancedWithReplicaSet}, {"directConnection=true", Client().SetDirect(true), connstring.ErrLoadBalancedWithDirectConnection}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // The loadBalanced option should not be validated if it is unset or false. err := tc.opts.Validate() assert.Nil(t, err, "Validate error when loadBalanced is unset: %v", err) tc.opts.SetLoadBalanced(false) err = tc.opts.Validate() assert.Nil(t, err, "Validate error when loadBalanced=false: %v", err) tc.opts.SetLoadBalanced(true) err = tc.opts.Validate() assert.Equal(t, tc.err, err, "expected error %v when loadBalanced=true, got %v", tc.err, err) }) } }) t.Run("heartbeatFrequencyMS validation", func(t *testing.T) { testCases := []struct { name string opts *ClientOptions err error }{ { name: "heartbeatFrequencyMS > minimum (500ms)", opts: Client().SetHeartbeatInterval(10000 * time.Millisecond), err: nil, }, { name: "heartbeatFrequencyMS == minimum (500ms)", opts: Client().SetHeartbeatInterval(500 * time.Millisecond), err: nil, }, { name: "heartbeatFrequencyMS < minimum (500ms)", opts: Client().SetHeartbeatInterval(10 * time.Millisecond), err: errors.New("heartbeatFrequencyMS must exceed the minimum heartbeat interval of 500ms, got heartbeatFrequencyMS=\"10ms\""), }, { name: "heartbeatFrequencyMS == 0", opts: Client().SetHeartbeatInterval(0 * time.Millisecond), err: errors.New("heartbeatFrequencyMS must exceed the minimum heartbeat interval of 500ms, got heartbeatFrequencyMS=\"0s\""), }, { name: "heartbeatFrequencyMS > minimum (500ms) from URI", opts: Client().ApplyURI("mongodb://localhost:27017/?heartbeatFrequencyMS=10000"), err: nil, }, { name: "heartbeatFrequencyMS == minimum (500ms) from URI", opts: Client().ApplyURI("mongodb://localhost:27017/?heartbeatFrequencyMS=500"), err: nil, }, { name: "heartbeatFrequencyMS < minimum (500ms) from URI", opts: Client().ApplyURI("mongodb://localhost:27017/?heartbeatFrequencyMS=10"), err: errors.New("heartbeatFrequencyMS must exceed the minimum heartbeat interval of 500ms, got heartbeatFrequencyMS=\"10ms\""), }, { name: "heartbeatFrequencyMS == 0 from URI", opts: Client().ApplyURI("mongodb://localhost:27017/?heartbeatFrequencyMS=0"), err: errors.New("heartbeatFrequencyMS must exceed the minimum heartbeat interval of 500ms, got heartbeatFrequencyMS=\"0s\""), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.opts.Validate() assert.Equal(t, tc.err, err) }) } }) t.Run("minPoolSize validation", func(t *testing.T) { testCases := []struct { name string opts *ClientOptions err error }{ { "minPoolSize < maxPoolSize", Client().SetMinPoolSize(128).SetMaxPoolSize(256), nil, }, { "minPoolSize == maxPoolSize", Client().SetMinPoolSize(128).SetMaxPoolSize(128), nil, }, { "minPoolSize > maxPoolSize", Client().SetMinPoolSize(64).SetMaxPoolSize(32), errors.New("minPoolSize must be less than or equal to maxPoolSize, got minPoolSize=64 maxPoolSize=32"), }, { "maxPoolSize == 0", Client().SetMinPoolSize(128).SetMaxPoolSize(0), nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.opts.Validate() assert.Equal(t, tc.err, err, "expected error %v, got %v", tc.err, err) }) } }) t.Run("srvMaxHosts validation", func(t *testing.T) { testCases := []struct { name string opts *ClientOptions err error }{ {"replica set name", Client().SetReplicaSet("foo"), connstring.ErrSRVMaxHostsWithReplicaSet}, {"loadBalanced=true", Client().SetLoadBalanced(true), connstring.ErrSRVMaxHostsWithLoadBalanced}, {"loadBalanced=false", Client().SetLoadBalanced(false), nil}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := tc.opts.Validate() assert.Nil(t, err, "Validate error when srvMxaHosts is unset: %v", err) tc.opts.SetSRVMaxHosts(0) err = tc.opts.Validate() assert.Nil(t, err, "Validate error when srvMaxHosts is 0: %v", err) tc.opts.SetSRVMaxHosts(2) err = tc.opts.Validate() assert.Equal(t, tc.err, err, "expected error %v when srvMaxHosts > 0, got %v", tc.err, err) }) } }) t.Run("srvMaxHosts validation", func(t *testing.T) { t.Parallel() testCases := []struct { name string opts *ClientOptions err error }{ { name: "valid ServerAPI", opts: Client().SetServerAPIOptions(ServerAPI(ServerAPIVersion1)), err: nil, }, { name: "invalid ServerAPI", opts: Client().SetServerAPIOptions(ServerAPI("nope")), err: errors.New(`api version "nope" not supported; this driver version only supports API version "1"`), }, { name: "invalid ServerAPI with other invalid options", opts: Client().SetServerAPIOptions(ServerAPI("nope")).SetSRVMaxHosts(1).SetReplicaSet("foo"), err: errors.New(`api version "nope" not supported; this driver version only supports API version "1"`), }, } for _, tc := range testCases { tc := tc // Capture range variable. t.Run(tc.name, func(t *testing.T) { t.Parallel() err := tc.opts.Validate() assert.Equal(t, tc.err, err, "want error %v, got error %v", tc.err, err) }) } }) t.Run("server monitoring mode validation", func(t *testing.T) { t.Parallel() testCases := []struct { name string opts *ClientOptions err error }{ { name: "undefined", opts: Client(), err: nil, }, { name: "auto", opts: Client().SetServerMonitoringMode(ServerMonitoringModeAuto), err: nil, }, { name: "poll", opts: Client().SetServerMonitoringMode(ServerMonitoringModePoll), err: nil, }, { name: "stream", opts: Client().SetServerMonitoringMode(ServerMonitoringModeStream), err: nil, }, { name: "invalid", opts: Client().SetServerMonitoringMode("invalid"), err: errors.New("invalid server monitoring mode: \"invalid\""), }, } for _, tc := range testCases { tc := tc // Capture the range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() err := tc.opts.Validate() assert.Equal(t, tc.err, err, "expected error %v, got %v", tc.err, err) }) } }) t.Run("OIDC auth configuration validation", func(t *testing.T) { t.Parallel() emptyCb := func(_ context.Context, _ *OIDCArgs) (*OIDCCredential, error) { return nil, nil } testCases := []struct { name string opts *ClientOptions err error }{ { name: "password must not be set", opts: Client().SetAuth(Credential{AuthMechanism: "MONGODB-OIDC", Password: "password"}), err: fmt.Errorf("password must not be set for the MONGODB-OIDC auth mechanism"), }, { name: "cannot set both OIDCMachineCallback and OIDCHumanCallback simultaneously", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", OIDCMachineCallback: emptyCb, OIDCHumanCallback: emptyCb, }), err: fmt.Errorf("cannot set both OIDCMachineCallback and OIDCHumanCallback, only one may be specified"), }, { name: "cannot set ALLOWED_HOSTS without OIDCHumanCallback", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", OIDCMachineCallback: emptyCb, AuthMechanismProperties: map[string]string{"ALLOWED_HOSTS": "www.example.com"}, }), err: fmt.Errorf("cannot specify ALLOWED_HOSTS without an OIDCHumanCallback"), }, { name: "cannot set OIDCMachineCallback in GCP Environment", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", OIDCMachineCallback: emptyCb, AuthMechanismProperties: map[string]string{"ENVIRONMENT": "gcp", "TOKEN_RESOURCE": "stuff"}, }), err: fmt.Errorf(`OIDCMachineCallback cannot be specified with the gcp "ENVIRONMENT"`), }, { name: "cannot set OIDCMachineCallback in AZURE Environment", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", OIDCMachineCallback: emptyCb, AuthMechanismProperties: map[string]string{"ENVIRONMENT": "azure", "TOKEN_RESOURCE": "stuff"}, }), err: fmt.Errorf(`OIDCMachineCallback cannot be specified with the azure "ENVIRONMENT"`), }, { name: "TOKEN_RESOURCE must be set in GCP Environment", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", AuthMechanismProperties: map[string]string{"ENVIRONMENT": "gcp"}, }), err: fmt.Errorf(`"TOKEN_RESOURCE" must be set for the gcp "ENVIRONMENT"`), }, { name: "TOKEN_RESOURCE must be set in AZURE Environment", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", AuthMechanismProperties: map[string]string{"ENVIRONMENT": "azure"}, }), err: fmt.Errorf(`"TOKEN_RESOURCE" must be set for the azure "ENVIRONMENT"`), }, { name: "TOKEN_RESOURCE must not be set in TEST Environment", opts: Client().SetAuth(Credential{ AuthMechanism: "MONGODB-OIDC", AuthMechanismProperties: map[string]string{"ENVIRONMENT": "test", "TOKEN_RESOURCE": "stuff"}, }), err: fmt.Errorf(`"TOKEN_RESOURCE" must not be set for the test "ENVIRONMENT"`), }, } for _, tc := range testCases { tc := tc // Capture range variable. t.Run(tc.name, func(t *testing.T) { t.Parallel() err := tc.opts.Validate() assert.Equal(t, tc.err, err, "want error %v, got error %v", tc.err, err) }) } }) } type nonDefaultTransport struct{} func (*nonDefaultTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, nil } func TestClientHTTPTransport(t *testing.T) { t.Run("Default client", func(t *testing.T) { got := Client().HTTPClient assert.Equal(t, http.DefaultClient, got) }) t.Run("Non-default global transport", func(t *testing.T) { http.DefaultTransport = &nonDefaultTransport{} got := Client().HTTPClient.Transport assert.Equal(t, &nonDefaultTransport{}, got) }) } func createCertPool(t *testing.T, paths ...string) *x509.CertPool { t.Helper() pool := x509.NewCertPool() for _, path := range paths { pool.AddCert(loadCert(t, path)) } return pool } func loadCert(t *testing.T, file string) *x509.Certificate { t.Helper() data := readFile(t, file) block, _ := pem.Decode(data) cert, err := x509.ParseCertificate(block.Bytes) assert.Nil(t, err, "ParseCertificate error for %s: %v", file, err) return cert } func readFile(t *testing.T, path string) []byte { data, err := os.ReadFile(path) assert.Nil(t, err, "ReadFile error for %s: %v", path, err) return data } type testDialer struct { Num int } func (testDialer) DialContext(context.Context, string, string) (net.Conn, error) { return nil, nil } func compareTLSConfig(cfg1, cfg2 *tls.Config) bool { if cfg1 == nil && cfg2 == nil { return true } if cfg1 == nil || cfg2 == nil { return true } if (cfg1.RootCAs == nil && cfg1.RootCAs != nil) || (cfg1.RootCAs != nil && cfg1.RootCAs == nil) { return false } if cfg1.RootCAs != nil { cfg1Subjects := cfg1.RootCAs.Subjects() cfg2Subjects := cfg2.RootCAs.Subjects() if len(cfg1Subjects) != len(cfg2Subjects) { return false } for idx, firstSubject := range cfg1Subjects { if !bytes.Equal(firstSubject, cfg2Subjects[idx]) { return false } } } if len(cfg1.Certificates) != len(cfg2.Certificates) { return false } if cfg1.InsecureSkipVerify != cfg2.InsecureSkipVerify { return false } return true } func compareErrors(err1, err2 error) bool { if err1 == nil && err2 == nil { return true } if err1 == nil || err2 == nil { return false } var ospe1, ospe2 *os.PathError if errors.As(err1, &ospe1) && errors.As(err2, &ospe2) { return ospe1.Op == ospe2.Op && ospe1.Path == ospe2.Path } if err1.Error() != err2.Error() { return false } return true } func TestApplyURI(t *testing.T) { t.Parallel() testCases := []struct { name string uri string wantopts *ClientOptions }{ { name: "ParseError", uri: "not-mongo-db-uri://", wantopts: &ClientOptions{ err: fmt.Errorf( "error parsing uri: %w", errors.New(`scheme must be "mongodb" or "mongodb+srv"`)), }, }, { name: "ReadPreference Invalid Mode", uri: "mongodb://localhost/?maxStaleness=200", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: fmt.Errorf("unknown read preference %v", ""), }, }, { name: "ReadPreference Primary With Options", uri: "mongodb://localhost/?readPreference=Primary&maxStaleness=200", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: errors.New("can not specify tags, max staleness, or hedge with mode primary"), }, }, { name: "TLS addCertFromFile error", uri: "mongodb://localhost/?ssl=true&sslCertificateAuthorityFile=testdata/doesntexist", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: &os.PathError{Op: "open", Path: "testdata/doesntexist"}, }, }, { name: "TLS ClientCertificateKey", uri: "mongodb://localhost/?ssl=true&sslClientCertificateKeyFile=testdata/doesntexist", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: &os.PathError{Op: "open", Path: "testdata/doesntexist"}, }, }, { name: "AppName", uri: "mongodb://localhost/?appName=awesome-example-application", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, AppName: ptrutil.Ptr[string]("awesome-example-application"), err: nil, }, }, { name: "AuthMechanism", uri: "mongodb://localhost/?authMechanism=mongodb-x509", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{AuthSource: "$external", AuthMechanism: "mongodb-x509"}, err: nil, }, }, { name: "AuthMechanismProperties", uri: "mongodb://foo@localhost/?authMechanism=gssapi&authMechanismProperties=SERVICE_NAME:mongodb-fake", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{ AuthSource: "$external", AuthMechanism: "gssapi", AuthMechanismProperties: map[string]string{"SERVICE_NAME": "mongodb-fake"}, Username: "foo", }, err: nil, }, }, { name: "AuthSource", uri: "mongodb://foo@localhost/?authSource=random-database-example", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{AuthSource: "random-database-example", Username: "foo"}, err: nil, }, }, { name: "Username", uri: "mongodb://foo@localhost/", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{AuthSource: "admin", Username: "foo"}, err: nil, }, }, { name: "Unescaped slash in username", uri: "mongodb:///:pwd@localhost", wantopts: &ClientOptions{ err: fmt.Errorf("error parsing uri: %w", errors.New("unescaped slash in username")), }, }, { name: "Password", uri: "mongodb://foo:bar@localhost/", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{ AuthSource: "admin", Username: "foo", Password: "bar", PasswordSet: true, }, err: nil, }, }, { name: "Single character username and password", uri: "mongodb://f:b@localhost/", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{ AuthSource: "admin", Username: "f", Password: "b", PasswordSet: true, }, err: nil, }, }, { name: "Connect", uri: "mongodb://localhost/?connect=direct", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Direct: ptrutil.Ptr[bool](true), err: nil, }, }, { name: "ConnectTimeout", uri: "mongodb://localhost/?connectTimeoutms=5000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ConnectTimeout: ptrutil.Ptr[time.Duration](5 * time.Second), err: nil, }, }, { name: "Compressors", uri: "mongodb://localhost/?compressors=zlib,snappy", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Compressors: []string{"zlib", "snappy"}, ZlibLevel: ptrutil.Ptr[int](6), err: nil, }, }, { name: "DatabaseNoAuth", uri: "mongodb://localhost/example-database", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: nil, }, }, { name: "DatabaseAsDefault", uri: "mongodb://foo@localhost/example-database", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{AuthSource: "example-database", Username: "foo"}, err: nil, }, }, { name: "HeartbeatInterval", uri: "mongodb://localhost/?heartbeatIntervalms=12000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, HeartbeatInterval: ptrutil.Ptr[time.Duration](12 * time.Second), err: nil, }, }, { name: "Hosts", uri: "mongodb://localhost:27017,localhost:27018,localhost:27019/", wantopts: &ClientOptions{ Hosts: []string{"localhost:27017", "localhost:27018", "localhost:27019"}, err: nil, }, }, { name: "LocalThreshold", uri: "mongodb://localhost/?localThresholdMS=200", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, LocalThreshold: ptrutil.Ptr[time.Duration](200 * time.Millisecond), err: nil, }, }, { name: "MaxConnIdleTime", uri: "mongodb://localhost/?maxIdleTimeMS=300000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, MaxConnIdleTime: ptrutil.Ptr[time.Duration](5 * time.Minute), err: nil, }, }, { name: "MaxPoolSize", uri: "mongodb://localhost/?maxPoolSize=256", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, MaxPoolSize: ptrutil.Ptr[uint64](256), err: nil, }, }, { name: "MinPoolSize", uri: "mongodb://localhost/?minPoolSize=256", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, MinPoolSize: ptrutil.Ptr[uint64](256), err: nil, }, }, { name: "MaxConnecting", uri: "mongodb://localhost/?maxConnecting=10", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, MaxConnecting: ptrutil.Ptr[uint64](10), err: nil, }, }, { name: "ReadConcern", uri: "mongodb://localhost/?readConcernLevel=linearizable", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ReadConcern: readconcern.Linearizable(), err: nil, }, }, { name: "ReadPreference", uri: "mongodb://localhost/?readPreference=secondaryPreferred", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ReadPreference: readpref.SecondaryPreferred(), err: nil, }, }, { name: "ReadPreferenceTagSets", uri: "mongodb://localhost/?readPreference=secondaryPreferred&readPreferenceTags=foo:bar", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ReadPreference: readpref.SecondaryPreferred(readpref.WithTags("foo", "bar")), err: nil, }, }, { name: "MaxStaleness", uri: "mongodb://localhost/?readPreference=secondaryPreferred&maxStaleness=250", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ReadPreference: readpref.SecondaryPreferred(readpref.WithMaxStaleness(250 * time.Second)), err: nil, }, }, { name: "RetryWrites", uri: "mongodb://localhost/?retryWrites=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, RetryWrites: ptrutil.Ptr[bool](true), err: nil, }, }, { name: "ReplicaSet", uri: "mongodb://localhost/?replicaSet=rs01", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ReplicaSet: ptrutil.Ptr[string]("rs01"), err: nil, }, }, { name: "ServerSelectionTimeout", uri: "mongodb://localhost/?serverSelectionTimeoutMS=45000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ServerSelectionTimeout: ptrutil.Ptr[time.Duration](45 * time.Second), err: nil, }, }, { name: "SocketTimeout", uri: "mongodb://localhost/?socketTimeoutMS=15000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: nil, }, }, { name: "TLS CACertificate", uri: "mongodb://localhost/?ssl=true&sslCertificateAuthorityFile=testdata/ca.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ RootCAs: createCertPool(t, "testdata/ca.pem"), }, err: nil, }, }, { name: "TLS Insecure", uri: "mongodb://localhost/?ssl=true&sslInsecure=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ InsecureSkipVerify: true, }, err: nil, }, }, { name: "TLS ClientCertificateKey", uri: "mongodb://localhost/?ssl=true&sslClientCertificateKeyFile=testdata/nopass/certificate.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ Certificates: make([]tls.Certificate, 1), }, err: nil, }, }, { name: "TLS ClientCertificateKey with password", uri: "mongodb://localhost/?ssl=true&sslClientCertificateKeyFile=testdata/certificate.pem&sslClientCertificateKeyPassword=passphrase", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ Certificates: make([]tls.Certificate, 1), }, err: nil, }, }, { name: "TLS Username", uri: "mongodb://localhost/?ssl=true&authMechanism=mongodb-x509&sslClientCertificateKeyFile=testdata/nopass/certificate.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{ AuthMechanism: "mongodb-x509", AuthSource: "$external", Username: `C=US,ST=New York,L=New York City, Inc,O=MongoDB\,OU=WWW`, }, err: nil, }, }, { name: "WriteConcern J", uri: "mongodb://localhost/?journal=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, WriteConcern: writeconcern.Journaled(), err: nil, }, }, { name: "WriteConcern WString", uri: "mongodb://localhost/?w=majority", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, WriteConcern: writeconcern.Majority(), err: nil, }, }, { name: "WriteConcern W", uri: "mongodb://localhost/?w=3", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, WriteConcern: &writeconcern.WriteConcern{W: 3}, err: nil, }, }, { name: "WriteConcern WTimeout", uri: "mongodb://localhost/?wTimeoutMS=45000", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: nil, }, }, { name: "ZLibLevel", uri: "mongodb://localhost/?zlibCompressionLevel=4", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, ZlibLevel: ptrutil.Ptr[int](4), err: nil, }, }, { name: "TLS tlsCertificateFile and tlsPrivateKeyFile", uri: "mongodb://localhost/?tlsCertificateFile=testdata/nopass/cert.pem&tlsPrivateKeyFile=testdata/nopass/key.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ Certificates: make([]tls.Certificate, 1), }, err: nil, }, }, { name: "TLS only tlsCertificateFile", uri: "mongodb://localhost/?tlsCertificateFile=testdata/nopass/cert.pem", wantopts: &ClientOptions{ err: fmt.Errorf( "error validating uri: %w", errors.New("the tlsPrivateKeyFile URI option must be provided if the tlsCertificateFile option is specified")), }, }, { name: "TLS only tlsPrivateKeyFile", uri: "mongodb://localhost/?tlsPrivateKeyFile=testdata/nopass/key.pem", wantopts: &ClientOptions{ err: fmt.Errorf( "error validating uri: %w", errors.New("the tlsCertificateFile URI option must be provided if the tlsPrivateKeyFile option is specified")), }, }, { name: "TLS tlsCertificateFile and tlsPrivateKeyFile and tlsCertificateKeyFile", uri: "mongodb://localhost/?tlsCertificateFile=testdata/nopass/cert.pem&tlsPrivateKeyFile=testdata/nopass/key.pem&tlsCertificateKeyFile=testdata/nopass/certificate.pem", wantopts: &ClientOptions{ err: fmt.Errorf( "error validating uri: %w", errors.New("the sslClientCertificateKeyFile/tlsCertificateKeyFile URI option cannot be provided "+ "along with tlsCertificateFile or tlsPrivateKeyFile")), }, }, { name: "disable OCSP endpoint check", uri: "mongodb://localhost/?tlsDisableOCSPEndpointCheck=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, DisableOCSPEndpointCheck: ptrutil.Ptr[bool](true), err: nil, }, }, { name: "directConnection", uri: "mongodb://localhost/?directConnection=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Direct: ptrutil.Ptr[bool](true), err: nil, }, }, { name: "TLS CA file with multiple certificiates", uri: "mongodb://localhost/?tlsCAFile=testdata/ca-with-intermediates.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{ RootCAs: createCertPool(t, "testdata/ca-with-intermediates-first.pem", "testdata/ca-with-intermediates-second.pem", "testdata/ca-with-intermediates-third.pem"), }, err: nil, }, }, { name: "TLS empty CA file", uri: "mongodb://localhost/?tlsCAFile=testdata/empty-ca.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: errors.New("the specified CA file does not contain any valid certificates"), }, }, { name: "TLS CA file with no certificates", uri: "mongodb://localhost/?tlsCAFile=testdata/ca-key.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: errors.New("the specified CA file does not contain any valid certificates"), }, }, { name: "TLS malformed CA file", uri: "mongodb://localhost/?tlsCAFile=testdata/malformed-ca.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, err: errors.New("the specified CA file does not contain any valid certificates"), }, }, { name: "loadBalanced=true", uri: "mongodb://localhost/?loadBalanced=true", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, LoadBalanced: ptrutil.Ptr[bool](true), err: nil, }, }, { name: "loadBalanced=false", uri: "mongodb://localhost/?loadBalanced=false", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, LoadBalanced: ptrutil.Ptr[bool](false), err: nil, }, }, { name: "srvServiceName", uri: "mongodb+srv://test22.test.build.10gen.cc/?srvServiceName=customname", wantopts: &ClientOptions{ Hosts: []string{"localhost.test.build.10gen.cc:27017", "localhost.test.build.10gen.cc:27018"}, SRVServiceName: ptrutil.Ptr[string]("customname"), err: nil, }, }, { name: "srvMaxHosts", uri: "mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=2", wantopts: &ClientOptions{ Hosts: []string{"localhost.test.build.10gen.cc:27017", "localhost.test.build.10gen.cc:27018"}, SRVMaxHosts: ptrutil.Ptr[int](2), err: nil, }, }, { name: "GODRIVER-2263 regression test", uri: "mongodb://localhost/?tlsCertificateKeyFile=testdata/one-pk-multiple-certs.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, TLSConfig: &tls.Config{Certificates: make([]tls.Certificate, 1)}, err: nil, }, }, { name: "GODRIVER-2650 X509 certificate", uri: "mongodb://localhost/?ssl=true&authMechanism=mongodb-x509&sslClientCertificateKeyFile=testdata/one-pk-multiple-certs.pem", wantopts: &ClientOptions{ Hosts: []string{"localhost"}, Auth: &Credential{ AuthMechanism: "mongodb-x509", AuthSource: "$external", // Subject name in the first certificate is used as the username for X509 auth. Username: `C=US,ST=New York,L=New York City,O=MongoDB,OU=Drivers,CN=localhost`, }, TLSConfig: &tls.Config{Certificates: make([]tls.Certificate, 1)}, err: nil, }, }, { name: "ALLOWED_HOSTS cannot be specified in URI connection", uri: "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:example.com", wantopts: &ClientOptions{ HTTPClient: httputil.DefaultHTTPClient, err: errors.New(`error validating uri: ALLOWED_HOSTS cannot be specified in the URI connection string for the "MONGODB-OIDC" auth mechanism, it must be specified through the ClientOptions directly`), }, }, { name: "colon in TOKEN_RESOURCE works as expected", uri: "mongodb://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=TOKEN_RESOURCE:mongodb://test-cluster", wantopts: &ClientOptions{ Hosts: []string{"example.com"}, Auth: &Credential{AuthMechanism: "MONGODB-OIDC", AuthSource: "$external", AuthMechanismProperties: map[string]string{"TOKEN_RESOURCE": "mongodb://test-cluster"}}, HTTPClient: httputil.DefaultHTTPClient, err: nil, }, }, { name: "oidc azure", uri: "mongodb://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=TOKEN_RESOURCE:mongodb://test-cluster,ENVIRONMENT:azureManagedIdentities", wantopts: &ClientOptions{ Hosts: []string{"example.com"}, Auth: &Credential{AuthMechanism: "MONGODB-OIDC", AuthSource: "$external", AuthMechanismProperties: map[string]string{ "ENVIRONMENT": "azureManagedIdentities", "TOKEN_RESOURCE": "mongodb://test-cluster", }}, HTTPClient: httputil.DefaultHTTPClient, err: nil, }, }, { name: "oidc gcp", uri: "mongodb://test.mongodb.net/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:mongodb://test-cluster", wantopts: &ClientOptions{ Hosts: []string{"test.mongodb.net"}, Auth: &Credential{AuthMechanism: "MONGODB-OIDC", AuthSource: "$external", AuthMechanismProperties: map[string]string{ "ENVIRONMENT": "gcp", "TOKEN_RESOURCE": "mongodb://test-cluster", }}, HTTPClient: httputil.DefaultHTTPClient, err: nil, }, }, { name: "comma in key:value pair causes error", uri: "mongodb://example.com/?authMechanismProperties=TOKEN_RESOURCE:mongodb://host1%2Chost2", wantopts: &ClientOptions{ HTTPClient: httputil.DefaultHTTPClient, err: errors.New(`error parsing uri: invalid authMechanism property`), }, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { result := Client().ApplyURI(test.uri) // Manually add the URI and ConnString to the test expectations to avoid adding them in each test // definition. The ConnString should only be recorded if there was no error while parsing. cs, err := connstring.ParseAndValidate(test.uri) if err == nil { test.wantopts.connString = cs } if test.wantopts.HTTPClient == nil { test.wantopts.HTTPClient = httputil.DefaultHTTPClient } // We have to sort string slices in comparison, as Hosts resolved from SRV URIs do not have a set order. stringLess := func(a, b string) bool { return a < b } if diff := cmp.Diff( test.wantopts, result, cmp.AllowUnexported(ClientOptions{}, readconcern.ReadConcern{}, writeconcern.WriteConcern{}, readpref.ReadPref{}), cmp.Comparer(func(r1, r2 *bson.Registry) bool { return r1 == r2 }), cmp.Comparer(compareTLSConfig), cmp.Comparer(compareErrors), cmp.Comparer(optionsutil.Equal), cmpopts.SortSlices(stringLess), cmpopts.IgnoreFields(connstring.ConnString{}, "SSLClientCertificateKeyPassword"), cmpopts.IgnoreFields(http.Client{}, "Transport"), ); diff != "" { t.Errorf("URI did not apply correctly: (-want +got)\n%s", diff) } }) } }