1// Copyright (C) MongoDB, Inc. 2017-present. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may 4// not use this file except in compliance with the License. You may obtain 5// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 6 7package integration 8 9import ( 10 "errors" 11 "fmt" 12 "reflect" 13 "testing" 14 15 "go.mongodb.org/mongo-driver/bson" 16 "go.mongodb.org/mongo-driver/bson/bsontype" 17 "go.mongodb.org/mongo-driver/internal/testutil/assert" 18 "go.mongodb.org/mongo-driver/mongo" 19 "go.mongodb.org/mongo-driver/mongo/integration/mtest" 20 "go.mongodb.org/mongo-driver/mongo/options" 21 "go.mongodb.org/mongo-driver/mongo/readpref" 22 "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" 23) 24 25const ( 26 listCollCapped = "listcoll_capped" 27 listCollUncapped = "listcoll_uncapped" 28) 29 30var ( 31 interfaceAsMapRegistry = bson.NewRegistryBuilder(). 32 RegisterTypeMapEntry(bsontype.EmbeddedDocument, reflect.TypeOf(bson.M{})). 33 Build() 34) 35 36func TestDatabase(t *testing.T) { 37 mt := mtest.New(t, mtest.NewOptions().CreateClient(false)) 38 defer mt.Close() 39 40 mt.RunOpts("run command", noClientOpts, func(mt *mtest.T) { 41 mt.Run("decode raw", func(mt *mtest.T) { 42 res, err := mt.DB.RunCommand(mtest.Background, bson.D{{"ismaster", 1}}).DecodeBytes() 43 assert.Nil(mt, err, "RunCommand error: %v", err) 44 45 ok, err := res.LookupErr("ok") 46 assert.Nil(mt, err, "ok field not found in result") 47 assert.Equal(mt, bson.TypeDouble, ok.Type, "expected ok type %v, got %v", bson.TypeDouble, ok.Type) 48 assert.Equal(mt, 1.0, ok.Double(), "expected ok value 1.0, got %v", ok.Double()) 49 50 isMaster, err := res.LookupErr("ismaster") 51 assert.Nil(mt, err, "ismaster field not found in result") 52 assert.Equal(mt, bson.TypeBoolean, isMaster.Type, "expected isMaster type %v, got %v", bson.TypeBoolean, isMaster.Type) 53 assert.True(mt, isMaster.Boolean(), "expected isMaster value true, got false") 54 }) 55 mt.Run("decode struct", func(mt *mtest.T) { 56 result := struct { 57 IsMaster bool `bson:"ismaster"` 58 Ok float64 `bson:"ok"` 59 }{} 60 err := mt.DB.RunCommand(mtest.Background, bson.D{{"ismaster", 1}}).Decode(&result) 61 assert.Nil(mt, err, "RunCommand error: %v", err) 62 assert.Equal(mt, true, result.IsMaster, "expected isMaster value true, got false") 63 assert.Equal(mt, 1.0, result.Ok, "expected ok value 1.0, got %v", result.Ok) 64 }) 65 66 // We set min server version 3.6 because pre-3.6 servers use OP_QUERY, so the command document will look like 67 // {$query: {...}, $readPreference: {...}}. Per the command monitoring spec, the $query subdocument is unwrapped 68 // and the $readPreference is dropped for monitoring purposes, so we can't examine it. 69 readPrefOpts := mtest.NewOptions(). 70 Topologies(mtest.Sharded). 71 MinServerVersion("3.6") 72 mt.RunOpts("read pref passed to mongos", readPrefOpts, func(mt *mtest.T) { 73 // When communicating with a mongos, the supplied read preference should be passed down to the operations 74 // layer, which should add a top-level $readPreference field to the command. 75 76 runCmdOpts := options.RunCmd(). 77 SetReadPreference(readpref.SecondaryPreferred()) 78 err := mt.DB.RunCommand(mtest.Background, bson.D{{"isMaster", 1}}, runCmdOpts).Err() 79 assert.Nil(mt, err, "RunCommand error: %v", err) 80 81 expected := bson.Raw(bsoncore.BuildDocumentFromElements( 82 nil, 83 bsoncore.AppendStringElement(nil, "mode", "secondaryPreferred"), 84 )) 85 evt := mt.GetStartedEvent() 86 assert.Equal(mt, "isMaster", evt.CommandName, "expected 'isMaster' command to be sent, got %q", evt.CommandName) 87 actual, ok := evt.Command.Lookup("$readPreference").DocumentOK() 88 assert.True(mt, ok, "expected command %v to contain a $readPreference document", evt.Command) 89 assert.Equal(mt, expected, actual, "expected $readPreference document %v, got %v", expected, actual) 90 }) 91 failpointOpts := mtest.NewOptions().MinServerVersion("4.0").Topologies(mtest.ReplicaSet) 92 mt.RunOpts("gets result and error", failpointOpts, func(mt *mtest.T) { 93 mt.SetFailPoint(mtest.FailPoint{ 94 ConfigureFailPoint: "failCommand", 95 Mode: mtest.FailPointMode{ 96 Times: 1, 97 }, 98 Data: mtest.FailPointData{ 99 FailCommands: []string{"insert"}, 100 WriteConcernError: &mtest.WriteConcernErrorData{ 101 Code: 100, 102 }, 103 }, 104 }) 105 cmd := bson.D{ 106 {"insert", "test"}, 107 {"documents", bson.A{bson.D{{"a", 1}}}}, 108 } 109 res, gotErr := mt.DB.RunCommand(mtest.Background, cmd).DecodeBytes() 110 111 n, ok := res.Lookup("n").Int32OK() 112 assert.True(mt, ok, "expected n in response") 113 assert.Equal(mt, int32(1), n, "expected n value 1, got %v", n) 114 115 writeExcept, ok := gotErr.(mongo.WriteException) 116 assert.True(mt, ok, "expected WriteCommandError, got %T", gotErr) 117 assert.NotNil(mt, writeExcept.WriteConcernError, "expected WriteConcernError to be non-nil") 118 assert.Equal(mt, writeExcept.WriteConcernError.Code, 100, "expeced error code 100, got %v", writeExcept.WriteConcernError.Code) 119 }) 120 }) 121 122 dropOpts := mtest.NewOptions().DatabaseName("dropDb") 123 mt.RunOpts("drop", dropOpts, func(mt *mtest.T) { 124 err := mt.DB.Drop(mtest.Background) 125 assert.Nil(mt, err, "Drop error: %v", err) 126 127 list, err := mt.Client.ListDatabaseNames(mtest.Background, bson.D{}) 128 assert.Nil(mt, err, "ListDatabaseNames error: %v", err) 129 for _, db := range list { 130 if db == "dropDb" { 131 mt.Fatal("dropped database 'dropDb' found in database names") 132 } 133 } 134 }) 135 136 lcNamesOpts := mtest.NewOptions().MinServerVersion("4.0") 137 mt.RunOpts("list collection names", lcNamesOpts, func(mt *mtest.T) { 138 collName := "lcNamesCollection" 139 mt.CreateCollection(mtest.Collection{Name: collName}, true) 140 141 testCases := []struct { 142 name string 143 filter bson.D 144 found bool 145 }{ 146 {"no filter", bson.D{}, true}, 147 {"filter", bson.D{{"name", "lcNamesCollection"}}, true}, 148 {"filter not found", bson.D{{"name", "123"}}, false}, 149 } 150 for _, tc := range testCases { 151 mt.Run(tc.name, func(mt *mtest.T) { 152 colls, err := mt.DB.ListCollectionNames(mtest.Background, tc.filter) 153 assert.Nil(mt, err, "ListCollectionNames error: %v", err) 154 155 var found bool 156 for _, coll := range colls { 157 if coll == collName { 158 found = true 159 break 160 } 161 } 162 163 assert.Equal(mt, tc.found, found, "expected to find collection: %v, found collection: %v", tc.found, found) 164 }) 165 } 166 }) 167 168 mt.RunOpts("list collections", noClientOpts, func(mt *mtest.T) { 169 testCases := []struct { 170 name string 171 expectedTopology mtest.TopologyKind 172 cappedOnly bool 173 }{ 174 {"standalone no filter", mtest.Single, false}, 175 {"standalone filter", mtest.Single, true}, 176 {"replica set no filter", mtest.ReplicaSet, false}, 177 {"replica set filter", mtest.ReplicaSet, true}, 178 {"sharded no filter", mtest.Sharded, false}, 179 {"sharded filter", mtest.Sharded, true}, 180 } 181 for _, tc := range testCases { 182 tcOpts := mtest.NewOptions().Topologies(tc.expectedTopology) 183 mt.RunOpts(tc.name, tcOpts, func(mt *mtest.T) { 184 mt.CreateCollection(mtest.Collection{Name: listCollUncapped}, true) 185 mt.CreateCollection(mtest.Collection{ 186 Name: listCollCapped, 187 CreateOpts: bson.D{{"capped", true}, {"size", 64 * 1024}}, 188 }, true) 189 190 filter := bson.D{} 191 if tc.cappedOnly { 192 filter = bson.D{{"options.capped", true}} 193 } 194 195 var err error 196 for i := 0; i < 1; i++ { 197 cursor, err := mt.DB.ListCollections(mtest.Background, filter) 198 assert.Nil(mt, err, "ListCollections error (iteration %v): %v", i, err) 199 200 err = verifyListCollections(cursor, tc.cappedOnly) 201 if err == nil { 202 return 203 } 204 } 205 mt.Fatalf("error verifying list collections result: %v", err) 206 }) 207 } 208 }) 209 210 mt.RunOpts("run command cursor", noClientOpts, func(mt *mtest.T) { 211 var data []interface{} 212 for i := 0; i < 5; i++ { 213 data = append(data, bson.D{{"x", i}}) 214 } 215 findCollName := "runcommandcursor_find" 216 findCmd := bson.D{{"find", findCollName}} 217 aggCollName := "runcommandcursor_agg" 218 aggCmd := bson.D{ 219 {"aggregate", aggCollName}, 220 {"pipeline", bson.A{}}, 221 {"cursor", bson.D{}}, 222 } 223 pingCmd := bson.D{{"ping", 1}} 224 pingErr := errors.New("cursor should be an embedded document but is of BSON type invalid") 225 226 testCases := []struct { 227 name string 228 collName string 229 cmd interface{} 230 toInsert []interface{} 231 expectedErr error 232 numExpected int 233 minVersion string 234 }{ 235 {"success find", findCollName, findCmd, data, nil, 5, "3.2"}, 236 {"success aggregate", aggCollName, aggCmd, data, nil, 5, ""}, 237 {"failures", "runcommandcursor_ping", pingCmd, nil, pingErr, 0, ""}, 238 } 239 for _, tc := range testCases { 240 tcOpts := mtest.NewOptions().CollectionName(tc.collName) 241 if tc.minVersion != "" { 242 tcOpts.MinServerVersion(tc.minVersion) 243 } 244 245 mt.RunOpts(tc.name, tcOpts, func(mt *mtest.T) { 246 if len(tc.toInsert) > 0 { 247 _, err := mt.Coll.InsertMany(mtest.Background, tc.toInsert) 248 assert.Nil(mt, err, "InsertMany error: %v", err) 249 } 250 251 cursor, err := mt.DB.RunCommandCursor(mtest.Background, tc.cmd) 252 assert.Equal(mt, tc.expectedErr, err, "expected error %v, got %v", tc.expectedErr, err) 253 if tc.expectedErr != nil { 254 return 255 } 256 257 var count int 258 for cursor.Next(mtest.Background) { 259 count++ 260 } 261 assert.Equal(mt, tc.numExpected, count, "expected document count %v, got %v", tc.numExpected, count) 262 }) 263 } 264 }) 265 266 mt.RunOpts("create collection", noClientOpts, func(mt *mtest.T) { 267 collectionName := "create-collection-test" 268 269 mt.RunOpts("options", noClientOpts, func(mt *mtest.T) { 270 // Tests for various options combinations. The test creates a collection with some options and then verifies 271 // the result using the options document reported by listCollections. 272 273 // All possible options except collation. The collation is omitted here and tested below because the 274 // collation document reported by listCollections fills in extra fields and includes a "version" field 275 // that's not described in https://docs.mongodb.com/manual/reference/collation/. 276 storageEngine := bson.M{ 277 "wiredTiger": bson.M{ 278 "configString": "block_compressor=zlib", 279 }, 280 } 281 defaultIndexOpts := options.DefaultIndex().SetStorageEngine(storageEngine) 282 validator := bson.M{ 283 "$or": bson.A{ 284 bson.M{ 285 "phone": bson.M{"$type": "string"}, 286 }, 287 bson.M{ 288 "email": bson.M{"$type": "string"}, 289 }, 290 }, 291 } 292 nonCollationOpts := options.CreateCollection(). 293 SetCapped(true). 294 SetDefaultIndexOptions(defaultIndexOpts). 295 SetMaxDocuments(100). 296 SetSizeInBytes(1024). 297 SetStorageEngine(storageEngine). 298 SetValidator(validator). 299 SetValidationAction("warn"). 300 SetValidationLevel("moderate") 301 nonCollationExpected := bson.M{ 302 "capped": true, 303 "indexOptionDefaults": bson.M{ 304 "storageEngine": storageEngine, 305 }, 306 "max": int32(100), 307 "size": int32(1024), 308 "storageEngine": storageEngine, 309 "validator": validator, 310 "validationAction": "warn", 311 "validationLevel": "moderate", 312 } 313 314 testCases := []struct { 315 name string 316 minServerVersion string 317 maxServerVersion string 318 createOpts *options.CreateCollectionOptions 319 expectedOpts bson.M 320 }{ 321 {"all options except collation", "3.2", "", nonCollationOpts, nonCollationExpected}, 322 } 323 324 for _, tc := range testCases { 325 mtOpts := mtest.NewOptions(). 326 MinServerVersion(tc.minServerVersion). 327 MaxServerVersion(tc.maxServerVersion) 328 329 mt.RunOpts(tc.name, mtOpts, func(mt *mtest.T) { 330 mt.CreateCollection(mtest.Collection{ 331 Name: collectionName, 332 }, false) 333 334 err := mt.DB.CreateCollection(mtest.Background, collectionName, tc.createOpts) 335 assert.Nil(mt, err, "CreateCollection error: %v", err) 336 337 actualOpts := getCollectionOptions(mt, collectionName) 338 assert.Equal(mt, tc.expectedOpts, actualOpts, "options mismatch; expected %v, got %v", 339 tc.expectedOpts, actualOpts) 340 }) 341 } 342 }) 343 mt.RunOpts("collation", mtest.NewOptions().MinServerVersion("3.4"), func(mt *mtest.T) { 344 mt.CreateCollection(mtest.Collection{ 345 Name: collectionName, 346 }, false) 347 348 locale := "en_US" 349 createOpts := options.CreateCollection().SetCollation(&options.Collation{ 350 Locale: locale, 351 }) 352 err := mt.DB.CreateCollection(mtest.Background, collectionName, createOpts) 353 assert.Nil(mt, err, "CreateCollection error: %v", err) 354 355 actualOpts := getCollectionOptions(mt, collectionName) 356 collationVal, ok := actualOpts["collation"] 357 assert.True(mt, ok, "expected key 'collation' in collection options %v", actualOpts) 358 collation := collationVal.(bson.M) 359 assert.Equal(mt, locale, collation["locale"], "expected locale %v, got %v", locale, collation["locale"]) 360 }) 361 mt.Run("write concern", func(mt *mtest.T) { 362 mt.CreateCollection(mtest.Collection{ 363 Name: collectionName, 364 }, false) 365 366 err := mt.DB.CreateCollection(mtest.Background, collectionName) 367 assert.Nil(mt, err, "CreateCollection error: %v", err) 368 369 evt := mt.GetStartedEvent() 370 assert.Equal(mt, evt.CommandName, "create", "expected event for 'create', got '%v'", evt.CommandName) 371 _, err = evt.Command.LookupErr("writeConcern") 372 assert.Nil(mt, err, "expected write concern to be included in command %v", evt.Command) 373 }) 374 }) 375 376 mt.RunOpts("create view", mtest.NewOptions().CreateClient(false).MinServerVersion("3.4"), func(mt *mtest.T) { 377 sourceCollectionName := "create-view-test-collection" 378 viewName := "create-view-test-view" 379 projectStage := bson.M{ 380 "$project": bson.M{ 381 "projectedField": "foo", 382 }, 383 } 384 pipeline := []bson.M{projectStage} 385 386 mt.Run("function parameters are translated into options", func(mt *mtest.T) { 387 mt.CreateCollection(mtest.Collection{ 388 Name: viewName, 389 }, false) 390 391 err := mt.DB.CreateView(mtest.Background, viewName, sourceCollectionName, pipeline) 392 assert.Nil(mt, err, "CreateView error: %v", err) 393 394 expectedOpts := bson.M{ 395 "viewOn": sourceCollectionName, 396 "pipeline": bson.A{projectStage}, 397 } 398 actualOpts := getCollectionOptions(mt, viewName) 399 assert.Equal(mt, expectedOpts, actualOpts, "options mismatch; expected %v, got %v", expectedOpts, 400 actualOpts) 401 }) 402 mt.Run("collation", func(mt *mtest.T) { 403 mt.CreateCollection(mtest.Collection{ 404 Name: viewName, 405 }, false) 406 407 locale := "en_US" 408 viewOpts := options.CreateView().SetCollation(&options.Collation{ 409 Locale: locale, 410 }) 411 err := mt.DB.CreateView(mtest.Background, viewName, sourceCollectionName, mongo.Pipeline{}, viewOpts) 412 assert.Nil(mt, err, "CreateView error: %v", err) 413 414 actualOpts := getCollectionOptions(mt, viewName) 415 collationVal, ok := actualOpts["collation"] 416 assert.True(mt, ok, "expected key 'collation' in view options %v", actualOpts) 417 collation := collationVal.(bson.M) 418 assert.Equal(mt, locale, collation["locale"], "expected locale %v, got %v", locale, collation["locale"]) 419 }) 420 }) 421} 422 423func getCollectionOptions(mt *mtest.T, collectionName string) bson.M { 424 mt.Helper() 425 426 filter := bson.M{ 427 "name": collectionName, 428 } 429 cursor, err := mt.DB.ListCollections(mtest.Background, filter) 430 assert.Nil(mt, err, "ListCollections error: %v", err) 431 defer cursor.Close(mtest.Background) 432 assert.True(mt, cursor.Next(mtest.Background), "expected Next to return true, got false") 433 434 var actualOpts bson.M 435 err = bson.UnmarshalWithRegistry(interfaceAsMapRegistry, cursor.Current.Lookup("options").Document(), &actualOpts) 436 assert.Nil(mt, err, "UnmarshalWithRegistry error: %v", err) 437 438 return actualOpts 439} 440 441func verifyListCollections(cursor *mongo.Cursor, cappedOnly bool) error { 442 var cappedFound, uncappedFound bool 443 444 for cursor.Next(mtest.Background) { 445 nameElem, err := cursor.Current.LookupErr("name") 446 if err != nil { 447 return fmt.Errorf("name element not found in document %v", cursor.Current) 448 } 449 if nameElem.Type != bson.TypeString { 450 return fmt.Errorf("expected name type %v, got %v", bson.TypeString, nameElem.Type) 451 } 452 453 name := nameElem.StringValue() 454 // legacy servers can return an indexes collection that shouldn't be considered here 455 if name != listCollUncapped && name != listCollCapped { 456 continue 457 } 458 459 if name == listCollUncapped && !uncappedFound { 460 if cappedOnly { 461 return fmt.Errorf("found uncapped collection %v but expected only capped collections", listCollUncapped) 462 } 463 464 uncappedFound = true 465 continue 466 } 467 if name == listCollCapped && !cappedFound { 468 cappedFound = true 469 continue 470 } 471 472 // duplicate found 473 return fmt.Errorf("found duplicate collection %v", name) 474 } 475 476 if !cappedFound { 477 return fmt.Errorf("capped collection %v not found", listCollCapped) 478 } 479 if !cappedOnly && !uncappedFound { 480 return fmt.Errorf("uncapped collection %v not found", listCollUncapped) 481 } 482 return nil 483} 484