1// Copyright (C) MongoDB, Inc. 2014-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 mongofiles
8
9import (
10	"bytes"
11	"fmt"
12	"io/ioutil"
13	"os"
14	"strings"
15	"testing"
16
17	"github.com/mongodb/mongo-tools/common/db"
18	"github.com/mongodb/mongo-tools/common/json"
19	"github.com/mongodb/mongo-tools/common/log"
20	"github.com/mongodb/mongo-tools/common/options"
21	"github.com/mongodb/mongo-tools/common/testtype"
22	"github.com/mongodb/mongo-tools/common/testutil"
23	"github.com/mongodb/mongo-tools/common/util"
24	. "github.com/smartystreets/goconvey/convey"
25	"gopkg.in/mgo.v2"
26)
27
28var (
29	testDB     = "mongofiles_test_db"
30	testServer = "localhost"
31	testPort   = db.DefaultTestPort
32
33	ssl        = testutil.GetSSLOptions()
34	auth       = testutil.GetAuthOptions()
35	connection = &options.Connection{
36		Host: testServer,
37		Port: testPort,
38	}
39	toolOptions = &options.ToolOptions{
40		SSL:        &ssl,
41		Connection: connection,
42		Auth:       &auth,
43		Verbosity:  &options.Verbosity{},
44		URI:        &options.URI{},
45	}
46)
47
48// put in some test data into GridFS
49func setUpGridFSTestData() ([]interface{}, error) {
50	sessionProvider, err := db.NewSessionProvider(*toolOptions)
51	if err != nil {
52		return nil, err
53	}
54	session, err := sessionProvider.GetSession()
55	if err != nil {
56		return nil, err
57	}
58	defer session.Close()
59
60	bytesExpected := []interface{}{}
61	gfs := session.DB(testDB).GridFS("fs")
62
63	var testfile *mgo.GridFile
64
65	for i, item := range []string{"testfile1", "testfile2", "testfile3"} {
66		testfile, err = gfs.Create(item)
67		if err != nil {
68			return nil, err
69		}
70		defer testfile.Close()
71
72		n, err := testfile.Write([]byte(strings.Repeat("a", (i+1)*5)))
73		if err != nil {
74			return nil, err
75		}
76
77		bytesExpected = append(bytesExpected, n)
78	}
79
80	return bytesExpected, nil
81}
82
83// remove test data from GridFS
84func tearDownGridFSTestData() error {
85	sessionProvider, err := db.NewSessionProvider(*toolOptions)
86	if err != nil {
87		return err
88	}
89	session, err := sessionProvider.GetSession()
90	if err != nil {
91		return err
92	}
93	defer session.Close()
94
95	if err = session.DB(testDB).DropDatabase(); err != nil {
96		return err
97	}
98
99	return nil
100}
101func simpleMongoFilesInstanceWithID(command, Id string) (*MongoFiles, error) {
102	return simpleMongoFilesInstanceWithFilenameAndID(command, "", Id)
103}
104func simpleMongoFilesInstanceWithFilename(command, fname string) (*MongoFiles, error) {
105	return simpleMongoFilesInstanceWithFilenameAndID(command, fname, "")
106}
107func simpleMongoFilesInstanceCommandOnly(command string) (*MongoFiles, error) {
108	return simpleMongoFilesInstanceWithFilenameAndID(command, "", "")
109}
110
111func simpleMongoFilesInstanceWithFilenameAndID(command, fname, Id string) (*MongoFiles, error) {
112	sessionProvider, err := db.NewSessionProvider(*toolOptions)
113	if err != nil {
114		return nil, err
115	}
116
117	mongofiles := MongoFiles{
118		ToolOptions:     toolOptions,
119		InputOptions:    &InputOptions{},
120		StorageOptions:  &StorageOptions{GridFSPrefix: "fs", DB: testDB},
121		SessionProvider: sessionProvider,
122		Command:         command,
123		FileName:        fname,
124		Id:              Id,
125	}
126
127	return &mongofiles, nil
128}
129
130func fileContentsCompare(file1, file2 *os.File, t *testing.T) (bool, error) {
131	file1Stat, err := file1.Stat()
132	if err != nil {
133		return false, err
134	}
135
136	file2Stat, err := file2.Stat()
137	if err != nil {
138		return false, err
139	}
140
141	file1Size := file1Stat.Size()
142	file2Size := file2Stat.Size()
143
144	if file1Size != file2Size {
145		t.Log("file sizes not the same")
146		return false, nil
147	}
148
149	file1ContentsBytes, err := ioutil.ReadAll(file1)
150	if err != nil {
151		return false, err
152	}
153	file2ContentsBytes, err := ioutil.ReadAll(file2)
154	if err != nil {
155		return false, err
156	}
157
158	isContentSame := bytes.Compare(file1ContentsBytes, file2ContentsBytes) == 0
159	return isContentSame, nil
160
161}
162
163// get an id of an existing file, for _id access
164func idOfFile(mf *MongoFiles, filename string) string {
165	session, _ := mf.SessionProvider.GetSession()
166	defer session.Close()
167	gfs := session.DB(mf.StorageOptions.DB).GridFS(mf.StorageOptions.GridFSPrefix)
168	gFile, _ := gfs.Open(filename)
169	bytes, _ := json.Marshal(gFile.Id())
170	return fmt.Sprintf("ObjectId(%v)", string(bytes))
171}
172
173// test output needs some cleaning
174func cleanAndTokenizeTestOutput(str string) []string {
175	// remove last \r\n in str to avoid unnecessary line on split
176	if str != "" {
177		str = str[:len(str)-1]
178	}
179
180	return strings.Split(strings.Trim(str, "\r\n"), "\n")
181}
182
183// return slices of files and bytes in each file represented by each line
184func getFilesAndBytesFromLines(lines []string) ([]interface{}, []interface{}) {
185	var fileName string
186	var byteCount int
187
188	filesGotten := []interface{}{}
189	bytesGotten := []interface{}{}
190
191	for _, line := range lines {
192		fmt.Sscanf(line, "%s\t%d", &fileName, &byteCount)
193
194		filesGotten = append(filesGotten, fileName)
195		bytesGotten = append(bytesGotten, byteCount)
196	}
197
198	return filesGotten, bytesGotten
199}
200
201func getFilesAndBytesListFromGridFS() ([]interface{}, []interface{}, error) {
202	mfAfter, err := simpleMongoFilesInstanceCommandOnly("list")
203	if err != nil {
204		return nil, nil, err
205	}
206	str, err := mfAfter.Run(false)
207	if err != nil {
208		return nil, nil, err
209	}
210
211	lines := cleanAndTokenizeTestOutput(str)
212	filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
213	return filesGotten, bytesGotten, nil
214}
215
216// inefficient but fast way to ensure set equality of
217func ensureSetEquality(firstArray []interface{}, secondArray []interface{}) {
218	for _, line := range firstArray {
219		So(secondArray, ShouldContain, line)
220	}
221}
222
223// check if file exists
224func fileExists(name string) bool {
225	if _, err := os.Stat(name); err != nil {
226		if os.IsNotExist(err) {
227			return false
228		}
229	}
230	return true
231}
232
233// Test that it works whenever valid arguments are passed in and that
234// it barfs whenever invalid ones are passed
235func TestValidArguments(t *testing.T) {
236	testtype.VerifyTestType(t, testtype.UnitTestType)
237
238	Convey("With a MongoFiles instance", t, func() {
239		mf, err := simpleMongoFilesInstanceWithFilename("search", "file")
240		So(err, ShouldBeNil)
241		Convey("It should error out when no arguments fed", func() {
242			args := []string{}
243			err := mf.ValidateCommand(args)
244			So(err, ShouldNotBeNil)
245			So(err.Error(), ShouldEqual, "no command specified")
246		})
247
248		Convey("(list|get|put|delete|search|get_id|delete_id) should error out when more than 1 positional argument provided", func() {
249			for _, command := range []string{"list", "get", "put", "delete", "search", "get_id", "delete_id"} {
250				args := []string{command, "arg1", "arg2"}
251				err := mf.ValidateCommand(args)
252				So(err, ShouldNotBeNil)
253				So(err.Error(), ShouldEqual, "too many positional arguments")
254			}
255		})
256
257		Convey("put_id should error out when more than 3 positional argument provided", func() {
258			args := []string{"put_id", "arg1", "arg2", "arg3"}
259			err := mf.ValidateCommand(args)
260			So(err, ShouldNotBeNil)
261			So(err.Error(), ShouldEqual, "too many positional arguments")
262		})
263
264		Convey("put_id should error out when only 1 positional argument provided", func() {
265			args := []string{"put_id", "arg1"}
266			err := mf.ValidateCommand(args)
267			So(err, ShouldNotBeNil)
268			So(err.Error(), ShouldEqual, fmt.Sprintf("'%v' argument(s) missing", "put_id"))
269		})
270
271		Convey("It should not error out when list command isn't given an argument", func() {
272			args := []string{"list"}
273			So(mf.ValidateCommand(args), ShouldBeNil)
274			So(mf.StorageOptions.LocalFileName, ShouldEqual, "")
275		})
276
277		Convey("It should error out when any of (get|put|delete|search|get_id|delete_id) not given supporting argument", func() {
278			for _, command := range []string{"get", "put", "delete", "search", "get_id", "delete_id"} {
279				args := []string{command}
280				err := mf.ValidateCommand(args)
281				So(err, ShouldNotBeNil)
282				So(err.Error(), ShouldEqual, fmt.Sprintf("'%v' argument missing", command))
283			}
284		})
285
286		Convey("It should error out when a nonsensical command is given", func() {
287			args := []string{"commandnonexistent"}
288
289			err := mf.ValidateCommand(args)
290			So(err, ShouldNotBeNil)
291			So(err.Error(), ShouldEqual, fmt.Sprintf("'%v' is not a valid command", args[0]))
292		})
293
294	})
295}
296
297// Test that the output from mongofiles is actually correct
298func TestMongoFilesCommands(t *testing.T) {
299	testtype.VerifyTestType(t, testtype.IntegrationTestType)
300
301	Convey("Testing the various commands (get|get_id|put|delete|delete_id|search|list) "+
302		"with a MongoDump instance", t, func() {
303
304		bytesExpected, err := setUpGridFSTestData()
305		So(err, ShouldBeNil)
306
307		// []interface{} here so we can use 'ensureSetEquality' method for both []string and []int
308		filesExpected := []interface{}{"testfile1", "testfile2", "testfile3"}
309
310		Convey("Testing the 'list' command with a file that isn't in GridFS should", func() {
311			mf, err := simpleMongoFilesInstanceWithFilename("list", "gibberish")
312			So(err, ShouldBeNil)
313			So(mf, ShouldNotBeNil)
314
315			Convey("produce no output", func() {
316				output, err := mf.Run(false)
317				So(err, ShouldBeNil)
318				So(len(output), ShouldEqual, 0)
319			})
320		})
321
322		Convey("Testing the 'list' command with files that are in GridFS should", func() {
323			mf, err := simpleMongoFilesInstanceWithFilename("list", "testf")
324			So(err, ShouldBeNil)
325			So(mf, ShouldNotBeNil)
326
327			Convey("produce some output", func() {
328				str, err := mf.Run(false)
329				So(err, ShouldBeNil)
330				So(len(str), ShouldNotEqual, 0)
331
332				lines := cleanAndTokenizeTestOutput(str)
333				So(len(lines), ShouldEqual, len(filesExpected))
334
335				filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
336				ensureSetEquality(filesExpected, filesGotten)
337				ensureSetEquality(bytesExpected, bytesGotten)
338			})
339		})
340
341		Convey("Testing the 'search' command with files that are in GridFS should", func() {
342			mf, err := simpleMongoFilesInstanceWithFilename("search", "file")
343			So(err, ShouldBeNil)
344			So(mf, ShouldNotBeNil)
345
346			Convey("produce some output", func() {
347				str, err := mf.Run(false)
348				So(err, ShouldBeNil)
349				So(len(str), ShouldNotEqual, 0)
350
351				lines := cleanAndTokenizeTestOutput(str)
352				So(len(lines), ShouldEqual, len(filesExpected))
353
354				filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
355				ensureSetEquality(filesExpected, filesGotten)
356				ensureSetEquality(bytesExpected, bytesGotten)
357			})
358		})
359
360		Convey("Testing the 'get' command with a file that is in GridFS should", func() {
361			mf, err := simpleMongoFilesInstanceWithFilename("get", "testfile1")
362			So(err, ShouldBeNil)
363			So(mf, ShouldNotBeNil)
364
365			var buff bytes.Buffer
366			log.SetWriter(&buff)
367
368			Convey("copy the file to the local filesystem", func() {
369				buff.Truncate(0)
370				str, err := mf.Run(false)
371				So(err, ShouldBeNil)
372				So(str, ShouldEqual, "")
373				So(buff.Len(), ShouldNotEqual, 0)
374
375				testFile, err := os.Open("testfile1")
376				So(err, ShouldBeNil)
377				defer testFile.Close()
378
379				// pretty small file; so read all
380				testFile1Bytes, err := ioutil.ReadAll(testFile)
381				So(err, ShouldBeNil)
382				So(len(testFile1Bytes), ShouldEqual, bytesExpected[0])
383			})
384
385			Convey("store the file contents in a file with different name if '--local' flag used", func() {
386				buff.Truncate(0)
387				mf.StorageOptions.LocalFileName = "testfile1copy"
388				str, err := mf.Run(false)
389				So(err, ShouldBeNil)
390				So(str, ShouldEqual, "")
391				So(buff.Len(), ShouldNotEqual, 0)
392
393				testFile, err := os.Open("testfile1copy")
394				So(err, ShouldBeNil)
395				defer testFile.Close()
396
397				// pretty small file; so read all
398				testFile1Bytes, err := ioutil.ReadAll(testFile)
399				So(err, ShouldBeNil)
400				So(len(testFile1Bytes), ShouldEqual, bytesExpected[0])
401			})
402
403			// cleanup file we just copied to the local FS
404			Reset(func() {
405
406				// remove 'testfile1' or 'testfile1copy'
407				if fileExists("testfile1") {
408					err = os.Remove("testfile1")
409				}
410				So(err, ShouldBeNil)
411
412				if fileExists("testfile1copy") {
413					err = os.Remove("testfile1copy")
414				}
415				So(err, ShouldBeNil)
416
417			})
418		})
419
420		Convey("Testing the 'get_id' command with a file that is in GridFS should", func() {
421			// hack to grab an _id
422			mf, _ := simpleMongoFilesInstanceWithFilename("get", "testfile1")
423			idString := idOfFile(mf, "testfile1")
424
425			mf, err = simpleMongoFilesInstanceWithID("get_id", idString)
426			So(err, ShouldBeNil)
427			So(mf, ShouldNotBeNil)
428
429			var buff bytes.Buffer
430			log.SetWriter(&buff)
431
432			Convey("copy the file to the local filesystem", func() {
433				buff.Truncate(0)
434				str, err := mf.Run(false)
435				So(err, ShouldBeNil)
436				So(str, ShouldEqual, "")
437				So(buff.Len(), ShouldNotEqual, 0)
438
439				testFile, err := os.Open("testfile1")
440				So(err, ShouldBeNil)
441				defer testFile.Close()
442
443				// pretty small file; so read all
444				testFile1Bytes, err := ioutil.ReadAll(testFile)
445				So(err, ShouldBeNil)
446				So(len(testFile1Bytes), ShouldEqual, bytesExpected[0])
447			})
448
449			Reset(func() {
450				// remove 'testfile1' or 'testfile1copy'
451				if fileExists("testfile1") {
452					err = os.Remove("testfile1")
453				}
454				So(err, ShouldBeNil)
455				if fileExists("testfile1copy") {
456					err = os.Remove("testfile1copy")
457				}
458				So(err, ShouldBeNil)
459			})
460		})
461
462		Convey("Testing the 'put' command by putting some lorem ipsum file with 287613 bytes should", func() {
463			mf, err := simpleMongoFilesInstanceWithFilename("put", "lorem_ipsum_287613_bytes.txt")
464			So(err, ShouldBeNil)
465			So(mf, ShouldNotBeNil)
466			mf.StorageOptions.LocalFileName = util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt")
467
468			var buff bytes.Buffer
469			log.SetWriter(&buff)
470
471			Convey("insert the file by creating two chunks (ceil(287,613 / 255 * 1024)) in GridFS", func() {
472				buff.Truncate(0)
473				str, err := mf.Run(false)
474				So(err, ShouldBeNil)
475				So(str, ShouldEqual, "")
476				So(buff.Len(), ShouldNotEqual, 0)
477
478				Convey("and files should exist in gridfs", func() {
479					filesGotten, _, err := getFilesAndBytesListFromGridFS()
480					So(err, ShouldBeNil)
481					So(len(filesGotten), ShouldEqual, len(filesExpected)+1)
482					So(filesGotten, ShouldContain, "lorem_ipsum_287613_bytes.txt")
483				})
484
485				Convey("and should have exactly the same content as the original file", func() {
486					buff.Truncate(0)
487					mfAfter, err := simpleMongoFilesInstanceWithFilename("get", "lorem_ipsum_287613_bytes.txt")
488					So(err, ShouldBeNil)
489					So(mf, ShouldNotBeNil)
490
491					mfAfter.StorageOptions.LocalFileName = "lorem_ipsum_copy.txt"
492					str, err = mfAfter.Run(false)
493					So(err, ShouldBeNil)
494					So(str, ShouldEqual, "")
495					So(buff.Len(), ShouldNotEqual, 0)
496
497					loremIpsumOrig, err := os.Open(util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt"))
498					So(err, ShouldBeNil)
499
500					loremIpsumCopy, err := os.Open("lorem_ipsum_copy.txt")
501					So(err, ShouldBeNil)
502
503					Convey("compare the copy of the lorem ipsum file with the original", func() {
504
505						defer loremIpsumOrig.Close()
506						defer loremIpsumCopy.Close()
507						isContentSame, err := fileContentsCompare(loremIpsumOrig, loremIpsumCopy, t)
508						So(err, ShouldBeNil)
509						So(isContentSame, ShouldBeTrue)
510					})
511
512					Reset(func() {
513						err = os.Remove("lorem_ipsum_copy.txt")
514						So(err, ShouldBeNil)
515					})
516
517				})
518
519			})
520
521		})
522
523		Convey("Testing the 'put_id' command by putting some lorem ipsum file with 287613 bytes with different ids should succeed", func() {
524			for _, idToTest := range []string{"'test_id'", "'{a:\"b\"}'", "'{$numberlong:9999999999999999999999}'", "'{a:{b:{c:{}}}}'"} {
525				runPutIdTestCase(idToTest, t)
526			}
527		})
528
529		Convey("Testing the 'delete' command with a file that is in GridFS should", func() {
530			mf, err := simpleMongoFilesInstanceWithFilename("delete", "testfile2")
531			So(err, ShouldBeNil)
532			So(mf, ShouldNotBeNil)
533
534			var buff bytes.Buffer
535			log.SetWriter(&buff)
536
537			Convey("delete the file from GridFS", func() {
538				str, err := mf.Run(false)
539				So(err, ShouldBeNil)
540				So(str, ShouldEqual, "")
541				So(buff.Len(), ShouldNotEqual, 0)
542
543				Convey("check that the file has been deleted from GridFS", func() {
544					filesGotten, bytesGotten, err := getFilesAndBytesListFromGridFS()
545					So(err, ShouldEqual, nil)
546					So(len(filesGotten), ShouldEqual, len(filesExpected)-1)
547
548					So(filesGotten, ShouldNotContain, "testfile2")
549					So(bytesGotten, ShouldNotContain, bytesExpected[1])
550				})
551			})
552		})
553
554		Convey("Testing the 'delete_id' command with a file that is in GridFS should", func() {
555			// hack to grab an _id
556			mf, _ := simpleMongoFilesInstanceWithFilename("get", "testfile2")
557			idString := idOfFile(mf, "testfile2")
558
559			mf, err := simpleMongoFilesInstanceWithID("delete_id", idString)
560			So(err, ShouldBeNil)
561			So(mf, ShouldNotBeNil)
562
563			var buff bytes.Buffer
564			log.SetWriter(&buff)
565
566			Convey("delete the file from GridFS", func() {
567				str, err := mf.Run(false)
568				So(err, ShouldBeNil)
569				So(str, ShouldEqual, "")
570				So(buff.Len(), ShouldNotEqual, 0)
571
572				Convey("check that the file has been deleted from GridFS", func() {
573					filesGotten, bytesGotten, err := getFilesAndBytesListFromGridFS()
574					So(err, ShouldEqual, nil)
575					So(len(filesGotten), ShouldEqual, len(filesExpected)-1)
576
577					So(filesGotten, ShouldNotContain, "testfile2")
578					So(bytesGotten, ShouldNotContain, bytesExpected[1])
579				})
580			})
581		})
582
583		Reset(func() {
584			So(tearDownGridFSTestData(), ShouldBeNil)
585			err = os.Remove("lorem_ipsum_copy.txt")
586		})
587	})
588
589}
590
591func runPutIdTestCase(idToTest string, t *testing.T) {
592	remoteName := "remoteName"
593	mongoFilesInstance, err := simpleMongoFilesInstanceWithFilenameAndID("put_id", remoteName, idToTest)
594
595	var buff bytes.Buffer
596	log.SetWriter(&buff)
597
598	So(err, ShouldBeNil)
599	So(mongoFilesInstance, ShouldNotBeNil)
600	mongoFilesInstance.StorageOptions.LocalFileName = util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt")
601
602	t.Log("Should correctly insert the file into GridFS")
603	str, err := mongoFilesInstance.Run(false)
604	So(err, ShouldBeNil)
605	So(str, ShouldEqual, "")
606	So(buff.Len(), ShouldNotEqual, 0)
607
608	t.Log("and its filename should exist when the 'list' command is run")
609	filesGotten, _, err := getFilesAndBytesListFromGridFS()
610	So(err, ShouldBeNil)
611	So(filesGotten, ShouldContain, remoteName)
612
613	t.Log("and get_id should have exactly the same content as the original file")
614
615	mfAfter, err := simpleMongoFilesInstanceWithID("get_id", idToTest)
616	So(err, ShouldBeNil)
617	So(mfAfter, ShouldNotBeNil)
618
619	mfAfter.StorageOptions.LocalFileName = "lorem_ipsum_copy.txt"
620	buff.Truncate(0)
621	str, err = mfAfter.Run(false)
622	So(err, ShouldBeNil)
623	So(str, ShouldEqual, "")
624	So(buff.Len(), ShouldNotEqual, 0)
625
626	loremIpsumOrig, err := os.Open(util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt"))
627	So(err, ShouldBeNil)
628
629	loremIpsumCopy, err := os.Open("lorem_ipsum_copy.txt")
630	So(err, ShouldBeNil)
631
632	defer loremIpsumOrig.Close()
633	defer loremIpsumCopy.Close()
634
635	isContentSame, err := fileContentsCompare(loremIpsumOrig, loremIpsumCopy, t)
636	So(err, ShouldBeNil)
637	So(isContentSame, ShouldBeTrue)
638}
639