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