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