github.com/kongr45gpen/mattermost-server@v5.11.1+incompatible/api4/file_test.go (about) 1 // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package api4 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "mime/multipart" 12 "net/http" 13 "net/textproto" 14 "net/url" 15 "os" 16 "path/filepath" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/mattermost/mattermost-server/app" 22 "github.com/mattermost/mattermost-server/model" 23 "github.com/mattermost/mattermost-server/store" 24 "github.com/mattermost/mattermost-server/utils/fileutils" 25 "github.com/mattermost/mattermost-server/utils/testutils" 26 ) 27 28 var testDir = "" 29 30 func init() { 31 testDir, _ = fileutils.FindDir("tests") 32 } 33 34 func checkCond(tb testing.TB, cond bool, text string) { 35 if !cond { 36 tb.Error(text) 37 } 38 } 39 40 func checkEq(tb testing.TB, v1, v2 interface{}, text string) { 41 checkCond(tb, fmt.Sprintf("%+v", v1) == fmt.Sprintf("%+v", v2), text) 42 } 43 44 func checkNeq(tb testing.TB, v1, v2 interface{}, text string) { 45 checkCond(tb, fmt.Sprintf("%+v", v1) != fmt.Sprintf("%+v", v2), text) 46 } 47 48 type zeroReader struct { 49 limit, read int 50 } 51 52 func (z *zeroReader) Read(b []byte) (int, error) { 53 for i := range b { 54 if z.read == z.limit { 55 return i, io.EOF 56 } 57 b[i] = 0 58 z.read++ 59 } 60 61 return len(b), nil 62 } 63 64 // File Section 65 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 66 67 func escapeQuotes(s string) string { 68 return quoteEscaper.Replace(s) 69 } 70 71 type UploadOpener func() (io.ReadCloser, int64, error) 72 73 func NewUploadOpenerReader(in io.Reader) UploadOpener { 74 return func() (io.ReadCloser, int64, error) { 75 rc, ok := in.(io.ReadCloser) 76 if ok { 77 return rc, -1, nil 78 } else { 79 return ioutil.NopCloser(in), -1, nil 80 } 81 } 82 } 83 84 func NewUploadOpenerFile(path string) UploadOpener { 85 return func() (io.ReadCloser, int64, error) { 86 fi, err := os.Stat(path) 87 if err != nil { 88 return nil, 0, err 89 } 90 f, err := os.Open(path) 91 if err != nil { 92 return nil, 0, err 93 } 94 return f, fi.Size(), nil 95 } 96 } 97 98 // testUploadFile and testUploadFiles have been "staged" here, eventually they 99 // should move back to being model.Client4 methods, once the specifics of the 100 // public API are sorted out. 101 func testUploadFile(c *model.Client4, url string, body io.Reader, contentType string, 102 contentLength int64) (*model.FileUploadResponse, *model.Response) { 103 rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetFilesRoute()+url, body) 104 if contentLength != 0 { 105 rq.ContentLength = contentLength 106 } 107 rq.Header.Set("Content-Type", contentType) 108 109 if len(c.AuthToken) > 0 { 110 rq.Header.Set(model.HEADER_AUTH, c.AuthType+" "+c.AuthToken) 111 } 112 113 rp, err := c.HttpClient.Do(rq) 114 if err != nil || rp == nil { 115 return nil, model.BuildErrorResponse(rp, model.NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)) 116 } 117 defer closeBody(rp) 118 119 if rp.StatusCode >= 300 { 120 return nil, model.BuildErrorResponse(rp, model.AppErrorFromJson(rp.Body)) 121 } 122 123 return model.FileUploadResponseFromJson(rp.Body), model.BuildResponse(rp) 124 } 125 126 func testUploadFiles( 127 c *model.Client4, 128 channelId string, 129 names []string, 130 openers []UploadOpener, 131 contentLengths []int64, 132 clientIds []string, 133 useMultipart, 134 useChunkedInSimplePost bool, 135 ) ( 136 fileUploadResponse *model.FileUploadResponse, 137 response *model.Response, 138 ) { 139 // Do not check len(clientIds), leave it entirely to the user to 140 // provide. The server will error out if it does not match the number 141 // of files, but it's not critical here. 142 if len(names) == 0 || len(openers) == 0 || len(names) != len(openers) { 143 return nil, &model.Response{ 144 Error: model.NewAppError("testUploadFiles", 145 "model.client.upload_post_attachment.file.app_error", 146 nil, "Empty or mismatched file data", http.StatusBadRequest), 147 } 148 } 149 150 // emergencyResponse is a convenience wrapper to return an error response 151 emergencyResponse := func(err error, errCode string) *model.Response { 152 return &model.Response{ 153 Error: model.NewAppError("testUploadFiles", 154 "model.client."+errCode+".app_error", 155 nil, err.Error(), http.StatusBadRequest), 156 } 157 } 158 159 // For multipart, start writing the request as a goroutine, and pipe 160 // multipart.Writer into it, otherwise generate a new request each 161 // time. 162 pipeReader, pipeWriter := io.Pipe() 163 mw := multipart.NewWriter(pipeWriter) 164 165 if useMultipart { 166 fileUploadResponseChannel := make(chan *model.FileUploadResponse) 167 responseChannel := make(chan *model.Response) 168 closedMultipart := false 169 170 go func() { 171 fur, resp := testUploadFile(c, "", pipeReader, mw.FormDataContentType(), -1) 172 responseChannel <- resp 173 fileUploadResponseChannel <- fur 174 }() 175 176 defer func() { 177 for { 178 select { 179 // Premature response, before the entire 180 // multipart was sent 181 case response = <-responseChannel: 182 // Guaranteed to be there 183 fileUploadResponse = <-fileUploadResponseChannel 184 if !closedMultipart { 185 _ = mw.Close() 186 _ = pipeWriter.Close() 187 closedMultipart = true 188 } 189 return 190 191 // Normal response, after the multipart was sent. 192 default: 193 if !closedMultipart { 194 err := mw.Close() 195 if err != nil { 196 fileUploadResponse = nil 197 response = emergencyResponse(err, "upload_post_attachment.writer") 198 return 199 } 200 err = pipeWriter.Close() 201 if err != nil { 202 fileUploadResponse = nil 203 response = emergencyResponse(err, "upload_post_attachment.writer") 204 return 205 } 206 closedMultipart = true 207 } 208 } 209 } 210 }() 211 212 err := mw.WriteField("channel_id", channelId) 213 if err != nil { 214 return nil, emergencyResponse(err, "upload_post_attachment.channel_id") 215 } 216 } else { 217 fileUploadResponse = &model.FileUploadResponse{} 218 } 219 220 data := make([]byte, 512) 221 222 upload := func(i int, f io.ReadCloser) *model.Response { 223 var cl int64 224 defer f.Close() 225 226 if len(contentLengths) > i { 227 cl = contentLengths[i] 228 } 229 230 n, err := f.Read(data) 231 if err != nil && err != io.EOF { 232 return emergencyResponse(err, "upload_post_attachment") 233 } 234 ct := http.DetectContentType(data[:n]) 235 reader := io.MultiReader(bytes.NewReader(data[:n]), f) 236 237 if useMultipart { 238 if len(clientIds) > i { 239 err := mw.WriteField("client_ids", clientIds[i]) 240 if err != nil { 241 return emergencyResponse(err, "upload_post_attachment.file") 242 } 243 } 244 245 h := make(textproto.MIMEHeader) 246 h.Set("Content-Disposition", 247 fmt.Sprintf(`form-data; name="files"; filename="%s"`, escapeQuotes(names[i]))) 248 h.Set("Content-Type", ct) 249 250 // If we error here, writing to mw, the deferred handler 251 part, err := mw.CreatePart(h) 252 if err != nil { 253 return emergencyResponse(err, "upload_post_attachment.writer") 254 } 255 256 _, err = io.Copy(part, reader) 257 if err != nil { 258 return emergencyResponse(err, "upload_post_attachment.writer") 259 } 260 } else { 261 postURL := fmt.Sprintf("?channel_id=%v", url.QueryEscape(channelId)) + 262 fmt.Sprintf("&filename=%v", url.QueryEscape(names[i])) 263 if len(clientIds) > i { 264 postURL += fmt.Sprintf("&client_id=%v", url.QueryEscape(clientIds[i])) 265 } 266 if useChunkedInSimplePost { 267 cl = -1 268 } 269 fur, resp := testUploadFile(c, postURL, reader, ct, cl) 270 if resp.Error != nil { 271 return resp 272 } 273 fileUploadResponse.FileInfos = append(fileUploadResponse.FileInfos, fur.FileInfos[0]) 274 if len(clientIds) > 0 { 275 if len(fur.ClientIds) > 0 { 276 fileUploadResponse.ClientIds = append(fileUploadResponse.ClientIds, fur.ClientIds[0]) 277 } else { 278 fileUploadResponse.ClientIds = append(fileUploadResponse.ClientIds, "") 279 } 280 } 281 response = resp 282 } 283 284 return nil 285 } 286 287 for i, open := range openers { 288 f, _, err := open() 289 if err != nil { 290 return nil, emergencyResponse(err, "upload_post_attachment") 291 } 292 293 resp := upload(i, f) 294 if resp != nil && resp.Error != nil { 295 return nil, resp 296 } 297 } 298 299 // In case of a simple POST, the return values have been set by upload(), 300 // otherwise we finished writing the multipart, and the return values will 301 // be set in defer 302 return fileUploadResponse, response 303 } 304 305 func TestUploadFiles(t *testing.T) { 306 th := Setup().InitBasic() 307 defer th.TearDown() 308 if *th.App.Config().FileSettings.DriverName == "" { 309 t.Skip("skipping because no file driver is enabled") 310 } 311 312 channel := th.BasicChannel 313 date := time.Now().Format("20060102") 314 315 // Get better error messages 316 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true }) 317 318 op := func(name string) UploadOpener { 319 return NewUploadOpenerFile(filepath.Join(testDir, name)) 320 } 321 322 tests := []struct { 323 title string 324 client *model.Client4 325 openers []UploadOpener 326 names []string 327 clientIds []string 328 329 skipSuccessValidation bool 330 skipPayloadValidation bool 331 skipSimplePost bool 332 skipMultipart bool 333 channelId string 334 useChunkedInSimplePost bool 335 expectedCreatorId string 336 expectedPayloadNames []string 337 expectImage bool 338 expectedImageWidths []int 339 expectedImageHeights []int 340 expectedImageThumbnailNames []string 341 expectedImagePreviewNames []string 342 expectedImageHasPreview []bool 343 setupConfig func(a *app.App) func(a *app.App) 344 checkResponse func(t *testing.T, resp *model.Response) 345 }{ 346 // Upload a bunch of files, mixed images and non-images 347 { 348 title: "Happy", 349 names: []string{"test.png", "testgif.gif", "testplugin.tar.gz", "test-search.md", "test.tiff"}, 350 expectedCreatorId: th.BasicUser.Id, 351 }, 352 // Upload a bunch of files, with clientIds 353 { 354 title: "Happy client_ids", 355 names: []string{"test.png", "testgif.gif", "testplugin.tar.gz", "test-search.md", "test.tiff"}, 356 clientIds: []string{"1", "2", "3", "4", "5"}, 357 expectedCreatorId: th.BasicUser.Id, 358 }, 359 // Upload a bunch of images. testgif.gif is an animated GIF, 360 // so it does not have HasPreviewImage set. 361 { 362 title: "Happy images", 363 names: []string{"test.png", "testgif.gif"}, 364 expectImage: true, 365 expectedCreatorId: th.BasicUser.Id, 366 expectedImageWidths: []int{408, 118}, 367 expectedImageHeights: []int{336, 118}, 368 expectedImageHasPreview: []bool{true, false}, 369 }, 370 { 371 title: "Happy invalid image", 372 names: []string{"testgif.gif"}, 373 openers: []UploadOpener{NewUploadOpenerFile(filepath.Join(testDir, "test-search.md"))}, 374 skipPayloadValidation: true, 375 expectedCreatorId: th.BasicUser.Id, 376 }, 377 // Simple POST, chunked encoding 378 { 379 title: "Happy image chunked post", 380 skipMultipart: true, 381 useChunkedInSimplePost: true, 382 names: []string{"test.png"}, 383 expectImage: true, 384 expectedImageWidths: []int{408}, 385 expectedImageHeights: []int{336}, 386 expectedImageHasPreview: []bool{true}, 387 expectedCreatorId: th.BasicUser.Id, 388 }, 389 // Image thumbnail and preview: size and orientation. Note that 390 // the expected image dimensions remain the same regardless of the 391 // orientation - what we save in FileInfo is used by the 392 // clients to size UI elements, so the dimensions are "actual". 393 { 394 title: "Happy image thumbnail/preview 1", 395 names: []string{"orientation_test_1.jpeg"}, 396 expectedImageThumbnailNames: []string{"orientation_test_1_expected_thumb.jpeg"}, 397 expectedImagePreviewNames: []string{"orientation_test_1_expected_preview.jpeg"}, 398 expectImage: true, 399 expectedImageWidths: []int{2860}, 400 expectedImageHeights: []int{1578}, 401 expectedImageHasPreview: []bool{true}, 402 expectedCreatorId: th.BasicUser.Id, 403 }, 404 { 405 title: "Happy image thumbnail/preview 2", 406 names: []string{"orientation_test_2.jpeg"}, 407 expectedImageThumbnailNames: []string{"orientation_test_2_expected_thumb.jpeg"}, 408 expectedImagePreviewNames: []string{"orientation_test_2_expected_preview.jpeg"}, 409 expectImage: true, 410 expectedImageWidths: []int{2860}, 411 expectedImageHeights: []int{1578}, 412 expectedImageHasPreview: []bool{true}, 413 expectedCreatorId: th.BasicUser.Id, 414 }, 415 { 416 title: "Happy image thumbnail/preview 3", 417 names: []string{"orientation_test_3.jpeg"}, 418 expectedImageThumbnailNames: []string{"orientation_test_3_expected_thumb.jpeg"}, 419 expectedImagePreviewNames: []string{"orientation_test_3_expected_preview.jpeg"}, 420 expectImage: true, 421 expectedImageWidths: []int{2860}, 422 expectedImageHeights: []int{1578}, 423 expectedImageHasPreview: []bool{true}, 424 expectedCreatorId: th.BasicUser.Id, 425 }, 426 { 427 title: "Happy image thumbnail/preview 4", 428 names: []string{"orientation_test_4.jpeg"}, 429 expectedImageThumbnailNames: []string{"orientation_test_4_expected_thumb.jpeg"}, 430 expectedImagePreviewNames: []string{"orientation_test_4_expected_preview.jpeg"}, 431 expectImage: true, 432 expectedImageWidths: []int{2860}, 433 expectedImageHeights: []int{1578}, 434 expectedImageHasPreview: []bool{true}, 435 expectedCreatorId: th.BasicUser.Id, 436 }, 437 { 438 title: "Happy image thumbnail/preview 5", 439 names: []string{"orientation_test_5.jpeg"}, 440 expectedImageThumbnailNames: []string{"orientation_test_5_expected_thumb.jpeg"}, 441 expectedImagePreviewNames: []string{"orientation_test_5_expected_preview.jpeg"}, 442 expectImage: true, 443 expectedImageWidths: []int{2860}, 444 expectedImageHeights: []int{1578}, 445 expectedImageHasPreview: []bool{true}, 446 expectedCreatorId: th.BasicUser.Id, 447 }, 448 { 449 title: "Happy image thumbnail/preview 6", 450 names: []string{"orientation_test_6.jpeg"}, 451 expectedImageThumbnailNames: []string{"orientation_test_6_expected_thumb.jpeg"}, 452 expectedImagePreviewNames: []string{"orientation_test_6_expected_preview.jpeg"}, 453 expectImage: true, 454 expectedImageWidths: []int{2860}, 455 expectedImageHeights: []int{1578}, 456 expectedImageHasPreview: []bool{true}, 457 expectedCreatorId: th.BasicUser.Id, 458 }, 459 { 460 title: "Happy image thumbnail/preview 7", 461 names: []string{"orientation_test_7.jpeg"}, 462 expectedImageThumbnailNames: []string{"orientation_test_7_expected_thumb.jpeg"}, 463 expectedImagePreviewNames: []string{"orientation_test_7_expected_preview.jpeg"}, 464 expectImage: true, 465 expectedImageWidths: []int{2860}, 466 expectedImageHeights: []int{1578}, 467 expectedImageHasPreview: []bool{true}, 468 expectedCreatorId: th.BasicUser.Id, 469 }, 470 { 471 title: "Happy image thumbnail/preview 8", 472 names: []string{"orientation_test_8.jpeg"}, 473 expectedImageThumbnailNames: []string{"orientation_test_8_expected_thumb.jpeg"}, 474 expectedImagePreviewNames: []string{"orientation_test_8_expected_preview.jpeg"}, 475 expectImage: true, 476 expectedImageWidths: []int{2860}, 477 expectedImageHeights: []int{1578}, 478 expectedImageHasPreview: []bool{true}, 479 expectedCreatorId: th.BasicUser.Id, 480 }, 481 // TIFF preview test 482 { 483 title: "Happy image thumbnail/preview 9", 484 names: []string{"test.tiff"}, 485 expectedImageThumbnailNames: []string{"test_expected_thumb.tiff"}, 486 expectedImagePreviewNames: []string{"test_expected_preview.tiff"}, 487 expectImage: true, 488 expectedImageWidths: []int{701}, 489 expectedImageHeights: []int{701}, 490 expectedImageHasPreview: []bool{true}, 491 expectedCreatorId: th.BasicUser.Id, 492 }, 493 { 494 title: "Happy admin", 495 client: th.SystemAdminClient, 496 names: []string{"test.png"}, 497 expectedCreatorId: th.SystemAdminUser.Id, 498 }, 499 { 500 title: "Happy stream", 501 useChunkedInSimplePost: true, 502 skipPayloadValidation: true, 503 names: []string{"50Mb-stream"}, 504 openers: []UploadOpener{NewUploadOpenerReader(&zeroReader{limit: 50 * 1024 * 1024})}, 505 setupConfig: func(a *app.App) func(a *app.App) { 506 maxFileSize := *a.Config().FileSettings.MaxFileSize 507 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 50 * 1024 * 1024 }) 508 return func(a *app.App) { 509 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize }) 510 } 511 }, 512 expectedCreatorId: th.BasicUser.Id, 513 }, 514 // Error cases 515 { 516 title: "Error channel_id does not exist", 517 channelId: model.NewId(), 518 names: []string{"test.png"}, 519 skipSuccessValidation: true, 520 checkResponse: CheckForbiddenStatus, 521 }, 522 { 523 // on simple post this uploads the last file 524 // successfully, without a ClientId 525 title: "Error too few client_ids", 526 skipSimplePost: true, 527 names: []string{"test.png", "testplugin.tar.gz", "test-search.md"}, 528 clientIds: []string{"1", "4"}, 529 skipSuccessValidation: true, 530 checkResponse: CheckBadRequestStatus, 531 }, 532 { 533 title: "Error invalid channel_id", 534 channelId: "../../junk", 535 names: []string{"test.png"}, 536 skipSuccessValidation: true, 537 checkResponse: CheckBadRequestStatus, 538 }, 539 { 540 title: "Error admin channel_id does not exist", 541 client: th.SystemAdminClient, 542 channelId: model.NewId(), 543 names: []string{"test.png"}, 544 skipSuccessValidation: true, 545 checkResponse: CheckForbiddenStatus, 546 }, 547 { 548 title: "Error admin invalid channel_id", 549 client: th.SystemAdminClient, 550 channelId: "../../junk", 551 names: []string{"test.png"}, 552 skipSuccessValidation: true, 553 checkResponse: CheckBadRequestStatus, 554 }, 555 { 556 title: "Error admin disabled uploads", 557 client: th.SystemAdminClient, 558 names: []string{"test.png"}, 559 skipSuccessValidation: true, 560 checkResponse: CheckNotImplementedStatus, 561 setupConfig: func(a *app.App) func(a *app.App) { 562 enableFileAttachments := *a.Config().FileSettings.EnableFileAttachments 563 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false }) 564 return func(a *app.App) { 565 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = enableFileAttachments }) 566 } 567 }, 568 }, 569 { 570 title: "Error file too large", 571 names: []string{"test.png"}, 572 skipSuccessValidation: true, 573 checkResponse: CheckRequestEntityTooLargeStatus, 574 setupConfig: func(a *app.App) func(a *app.App) { 575 maxFileSize := *a.Config().FileSettings.MaxFileSize 576 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 279590 }) 577 return func(a *app.App) { 578 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize }) 579 } 580 }, 581 }, 582 // File too large (chunked, simple POST only, multipart would've been redundant with above) 583 { 584 title: "File too large chunked", 585 useChunkedInSimplePost: true, 586 skipMultipart: true, 587 names: []string{"test.png"}, 588 skipSuccessValidation: true, 589 checkResponse: CheckRequestEntityTooLargeStatus, 590 setupConfig: func(a *app.App) func(a *app.App) { 591 maxFileSize := *a.Config().FileSettings.MaxFileSize 592 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 279590 }) 593 return func(a *app.App) { 594 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize }) 595 } 596 }, 597 }, 598 { 599 title: "Error stream too large", 600 skipPayloadValidation: true, 601 names: []string{"50Mb-stream"}, 602 openers: []UploadOpener{NewUploadOpenerReader(&zeroReader{limit: 50 * 1024 * 1024})}, 603 skipSuccessValidation: true, 604 checkResponse: CheckRequestEntityTooLargeStatus, 605 setupConfig: func(a *app.App) func(a *app.App) { 606 maxFileSize := *a.Config().FileSettings.MaxFileSize 607 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 100 * 1024 }) 608 return func(a *app.App) { 609 a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize }) 610 } 611 }, 612 }, 613 } 614 615 for _, useMultipart := range []bool{true, false} { 616 for _, tc := range tests { 617 if tc.skipMultipart && useMultipart || tc.skipSimplePost && !useMultipart { 618 continue 619 } 620 621 // Set the default values and title 622 client := th.Client 623 if tc.client != nil { 624 client = tc.client 625 } 626 channelId := channel.Id 627 if tc.channelId != "" { 628 channelId = tc.channelId 629 } 630 if tc.checkResponse == nil { 631 tc.checkResponse = CheckNoError 632 } 633 634 title := "" 635 if useMultipart { 636 title = "multipart " 637 } else { 638 title = "simple " 639 } 640 if tc.title != "" { 641 title += tc.title + " " 642 } 643 title += fmt.Sprintf("%v", tc.names) 644 645 // Apply any necessary config changes 646 var restoreConfig func(a *app.App) 647 if tc.setupConfig != nil { 648 restoreConfig = tc.setupConfig(th.App) 649 } 650 651 t.Run(title, func(t *testing.T) { 652 if len(tc.openers) == 0 { 653 for _, name := range tc.names { 654 tc.openers = append(tc.openers, op(name)) 655 } 656 } 657 fileResp, resp := testUploadFiles(client, channelId, tc.names, 658 tc.openers, nil, tc.clientIds, useMultipart, 659 tc.useChunkedInSimplePost) 660 tc.checkResponse(t, resp) 661 if tc.skipSuccessValidation { 662 return 663 } 664 665 if fileResp == nil || len(fileResp.FileInfos) == 0 || len(fileResp.FileInfos) != len(tc.names) { 666 t.Fatal("Empty or mismatched actual or expected FileInfos") 667 } 668 669 for i, ri := range fileResp.FileInfos { 670 // The returned file info from the upload call will be missing some fields that will be stored in the database 671 checkEq(t, ri.CreatorId, tc.expectedCreatorId, "File should be assigned to user") 672 checkEq(t, ri.PostId, "", "File shouldn't have a post Id") 673 checkEq(t, ri.Path, "", "File path should not be set on returned info") 674 checkEq(t, ri.ThumbnailPath, "", "File thumbnail path should not be set on returned info") 675 checkEq(t, ri.PreviewPath, "", "File preview path should not be set on returned info") 676 if len(tc.clientIds) > i { 677 checkCond(t, len(fileResp.ClientIds) == len(tc.clientIds), 678 fmt.Sprintf("Wrong number of clientIds returned, expected %v, got %v", len(tc.clientIds), len(fileResp.ClientIds))) 679 checkEq(t, fileResp.ClientIds[i], tc.clientIds[i], 680 fmt.Sprintf("Wrong clientId returned, expected %v, got %v", tc.clientIds[i], fileResp.ClientIds[i])) 681 } 682 683 var dbInfo *model.FileInfo 684 result := <-th.App.Srv.Store.FileInfo().Get(ri.Id) 685 if result.Err != nil { 686 t.Error(result.Err) 687 } else { 688 dbInfo = result.Data.(*model.FileInfo) 689 } 690 checkEq(t, dbInfo.Id, ri.Id, "File id from response should match one stored in database") 691 checkEq(t, dbInfo.CreatorId, tc.expectedCreatorId, "F ile should be assigned to user") 692 checkEq(t, dbInfo.PostId, "", "File shouldn't have a post") 693 checkNeq(t, dbInfo.Path, "", "File path should be set in database") 694 _, fname := filepath.Split(dbInfo.Path) 695 ext := filepath.Ext(fname) 696 name := fname[:len(fname)-len(ext)] 697 expectedDir := fmt.Sprintf("%v/teams/%v/channels/%v/users/%s/%s", date, FILE_TEAM_ID, channel.Id, ri.CreatorId, ri.Id) 698 expectedPath := fmt.Sprintf("%s/%s", expectedDir, fname) 699 checkEq(t, dbInfo.Path, expectedPath, 700 fmt.Sprintf("File %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.Path, expectedPath)) 701 702 if tc.expectImage { 703 expectedThumbnailPath := fmt.Sprintf("%s/%s_thumb.jpg", expectedDir, name) 704 expectedPreviewPath := fmt.Sprintf("%s/%s_preview.jpg", expectedDir, name) 705 checkEq(t, dbInfo.ThumbnailPath, expectedThumbnailPath, 706 fmt.Sprintf("Thumbnail for %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.ThumbnailPath, expectedThumbnailPath)) 707 checkEq(t, dbInfo.PreviewPath, expectedPreviewPath, 708 fmt.Sprintf("Preview for %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.PreviewPath, expectedPreviewPath)) 709 710 checkCond(t, 711 dbInfo.HasPreviewImage == tc.expectedImageHasPreview[i], 712 fmt.Sprintf("Image: HasPreviewImage should be set for %s", dbInfo.Name)) 713 checkCond(t, 714 dbInfo.Width == tc.expectedImageWidths[i] && dbInfo.Height == tc.expectedImageHeights[i], 715 fmt.Sprintf("Image dimensions: expected %dwx%dh, got %vwx%dh", 716 tc.expectedImageWidths[i], tc.expectedImageHeights[i], 717 dbInfo.Width, dbInfo.Height)) 718 } 719 720 if !tc.skipPayloadValidation { 721 compare := func(get func(string) ([]byte, *model.Response), name string) { 722 data, resp := get(ri.Id) 723 if resp.Error != nil { 724 t.Fatal(resp.Error) 725 } 726 727 expected, err := ioutil.ReadFile(filepath.Join(testDir, name)) 728 if err != nil { 729 t.Fatal(err) 730 } 731 732 if bytes.Compare(data, expected) != 0 { 733 tf, err := ioutil.TempFile("", fmt.Sprintf("test_%v_*_%s", i, name)) 734 if err != nil { 735 t.Fatal(err) 736 } 737 io.Copy(tf, bytes.NewReader(data)) 738 tf.Close() 739 t.Errorf("Actual data mismatched %s, written to %q", name, tf.Name()) 740 } 741 } 742 if len(tc.expectedPayloadNames) == 0 { 743 tc.expectedPayloadNames = tc.names 744 } 745 746 compare(client.GetFile, tc.expectedPayloadNames[i]) 747 if len(tc.expectedImageThumbnailNames) > i { 748 compare(client.GetFileThumbnail, tc.expectedImageThumbnailNames[i]) 749 } 750 if len(tc.expectedImageThumbnailNames) > i { 751 compare(client.GetFilePreview, tc.expectedImagePreviewNames[i]) 752 } 753 } 754 755 th.cleanupTestFile(dbInfo) 756 } 757 }) 758 759 if restoreConfig != nil { 760 restoreConfig(th.App) 761 } 762 } 763 } 764 } 765 766 func TestGetFile(t *testing.T) { 767 th := Setup().InitBasic() 768 defer th.TearDown() 769 Client := th.Client 770 channel := th.BasicChannel 771 772 if *th.App.Config().FileSettings.DriverName == "" { 773 t.Skip("skipping because no file driver is enabled") 774 } 775 776 fileId := "" 777 var sent []byte 778 var err error 779 if sent, err = testutils.ReadTestFile("test.png"); err != nil { 780 t.Fatal(err) 781 } else { 782 fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png") 783 CheckNoError(t, resp) 784 785 fileId = fileResp.FileInfos[0].Id 786 } 787 788 data, resp := Client.GetFile(fileId) 789 CheckNoError(t, resp) 790 791 if len(data) == 0 { 792 t.Fatal("should not be empty") 793 } 794 795 for i := range data { 796 if data[i] != sent[i] { 797 t.Fatal("received file didn't match sent one") 798 } 799 } 800 801 _, resp = Client.GetFile("junk") 802 CheckBadRequestStatus(t, resp) 803 804 _, resp = Client.GetFile(model.NewId()) 805 CheckNotFoundStatus(t, resp) 806 807 Client.Logout() 808 _, resp = Client.GetFile(fileId) 809 CheckUnauthorizedStatus(t, resp) 810 811 _, resp = th.SystemAdminClient.GetFile(fileId) 812 CheckNoError(t, resp) 813 } 814 815 func TestGetFileHeaders(t *testing.T) { 816 th := Setup().InitBasic() 817 defer th.TearDown() 818 819 Client := th.Client 820 channel := th.BasicChannel 821 822 if *th.App.Config().FileSettings.DriverName == "" { 823 t.Skip("skipping because no file driver is enabled") 824 } 825 826 testHeaders := func(data []byte, filename string, expectedContentType string, getInline bool) func(*testing.T) { 827 return func(t *testing.T) { 828 fileResp, resp := Client.UploadFile(data, channel.Id, filename) 829 CheckNoError(t, resp) 830 831 fileId := fileResp.FileInfos[0].Id 832 833 _, resp = Client.GetFile(fileId) 834 CheckNoError(t, resp) 835 836 if contentType := resp.Header.Get("Content-Type"); !strings.HasPrefix(contentType, expectedContentType) { 837 t.Fatal("returned incorrect Content-Type", contentType) 838 } 839 840 if getInline { 841 if contentDisposition := resp.Header.Get("Content-Disposition"); !strings.HasPrefix(contentDisposition, "inline") { 842 t.Fatal("returned incorrect Content-Disposition", contentDisposition) 843 } 844 } else { 845 if contentDisposition := resp.Header.Get("Content-Disposition"); !strings.HasPrefix(contentDisposition, "attachment") { 846 t.Fatal("returned incorrect Content-Disposition", contentDisposition) 847 } 848 } 849 850 _, resp = Client.DownloadFile(fileId, true) 851 CheckNoError(t, resp) 852 853 if contentType := resp.Header.Get("Content-Type"); !strings.HasPrefix(contentType, expectedContentType) { 854 t.Fatal("returned incorrect Content-Type", contentType) 855 } 856 857 if contentDisposition := resp.Header.Get("Content-Disposition"); !strings.HasPrefix(contentDisposition, "attachment") { 858 t.Fatal("returned incorrect Content-Disposition", contentDisposition) 859 } 860 } 861 } 862 863 data := []byte("ABC") 864 865 t.Run("png", testHeaders(data, "test.png", "image/png", true)) 866 t.Run("gif", testHeaders(data, "test.gif", "image/gif", true)) 867 t.Run("mp4", testHeaders(data, "test.mp4", "video/mp4", true)) 868 t.Run("mp3", testHeaders(data, "test.mp3", "audio/mpeg", true)) 869 t.Run("pdf", testHeaders(data, "test.pdf", "application/pdf", false)) 870 t.Run("txt", testHeaders(data, "test.txt", "text/plain", false)) 871 t.Run("html", testHeaders(data, "test.html", "text/plain", false)) 872 t.Run("js", testHeaders(data, "test.js", "text/plain", false)) 873 t.Run("go", testHeaders(data, "test.go", "application/octet-stream", false)) 874 t.Run("zip", testHeaders(data, "test.zip", "application/zip", false)) 875 // Not every platform can recognize these 876 //t.Run("exe", testHeaders(data, "test.exe", "application/x-ms", false)) 877 t.Run("no extension", testHeaders(data, "test", "application/octet-stream", false)) 878 t.Run("no extension 2", testHeaders([]byte("<html></html>"), "test", "application/octet-stream", false)) 879 } 880 881 func TestGetFileThumbnail(t *testing.T) { 882 th := Setup().InitBasic() 883 defer th.TearDown() 884 Client := th.Client 885 channel := th.BasicChannel 886 887 if *th.App.Config().FileSettings.DriverName == "" { 888 t.Skip("skipping because no file driver is enabled") 889 } 890 891 fileId := "" 892 var sent []byte 893 var err error 894 if sent, err = testutils.ReadTestFile("test.png"); err != nil { 895 t.Fatal(err) 896 } else { 897 fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png") 898 CheckNoError(t, resp) 899 900 fileId = fileResp.FileInfos[0].Id 901 } 902 903 // Wait a bit for files to ready 904 time.Sleep(2 * time.Second) 905 906 data, resp := Client.GetFileThumbnail(fileId) 907 CheckNoError(t, resp) 908 909 if len(data) == 0 { 910 t.Fatal("should not be empty") 911 } 912 913 _, resp = Client.GetFileThumbnail("junk") 914 CheckBadRequestStatus(t, resp) 915 916 _, resp = Client.GetFileThumbnail(model.NewId()) 917 CheckNotFoundStatus(t, resp) 918 919 Client.Logout() 920 _, resp = Client.GetFileThumbnail(fileId) 921 CheckUnauthorizedStatus(t, resp) 922 923 otherUser := th.CreateUser() 924 Client.Login(otherUser.Email, otherUser.Password) 925 _, resp = Client.GetFileThumbnail(fileId) 926 CheckForbiddenStatus(t, resp) 927 928 Client.Logout() 929 _, resp = th.SystemAdminClient.GetFileThumbnail(fileId) 930 CheckNoError(t, resp) 931 } 932 933 func TestGetFileLink(t *testing.T) { 934 th := Setup().InitBasic() 935 defer th.TearDown() 936 Client := th.Client 937 channel := th.BasicChannel 938 939 if *th.App.Config().FileSettings.DriverName == "" { 940 t.Skip("skipping because no file driver is enabled") 941 } 942 943 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true }) 944 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) }) 945 946 fileId := "" 947 if data, err := testutils.ReadTestFile("test.png"); err != nil { 948 t.Fatal(err) 949 } else { 950 fileResp, resp := Client.UploadFile(data, channel.Id, "test.png") 951 CheckNoError(t, resp) 952 953 fileId = fileResp.FileInfos[0].Id 954 } 955 956 _, resp := Client.GetFileLink(fileId) 957 CheckBadRequestStatus(t, resp) 958 959 // Hacky way to assign file to a post (usually would be done by CreatePost call) 960 store.Must(th.App.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id, th.BasicUser.Id)) 961 962 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = false }) 963 _, resp = Client.GetFileLink(fileId) 964 CheckNotImplementedStatus(t, resp) 965 966 // Wait a bit for files to ready 967 time.Sleep(2 * time.Second) 968 969 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true }) 970 link, resp := Client.GetFileLink(fileId) 971 CheckNoError(t, resp) 972 973 if link == "" { 974 t.Fatal("should've received public link") 975 } 976 977 _, resp = Client.GetFileLink("junk") 978 CheckBadRequestStatus(t, resp) 979 980 _, resp = Client.GetFileLink(model.NewId()) 981 CheckNotFoundStatus(t, resp) 982 983 Client.Logout() 984 _, resp = Client.GetFileLink(fileId) 985 CheckUnauthorizedStatus(t, resp) 986 987 otherUser := th.CreateUser() 988 Client.Login(otherUser.Email, otherUser.Password) 989 _, resp = Client.GetFileLink(fileId) 990 CheckForbiddenStatus(t, resp) 991 992 Client.Logout() 993 _, resp = th.SystemAdminClient.GetFileLink(fileId) 994 CheckNoError(t, resp) 995 996 if result := <-th.App.Srv.Store.FileInfo().Get(fileId); result.Err != nil { 997 t.Fatal(result.Err) 998 } else { 999 th.cleanupTestFile(result.Data.(*model.FileInfo)) 1000 } 1001 } 1002 1003 func TestGetFilePreview(t *testing.T) { 1004 th := Setup().InitBasic() 1005 defer th.TearDown() 1006 Client := th.Client 1007 channel := th.BasicChannel 1008 1009 if *th.App.Config().FileSettings.DriverName == "" { 1010 t.Skip("skipping because no file driver is enabled") 1011 } 1012 1013 fileId := "" 1014 var sent []byte 1015 var err error 1016 if sent, err = testutils.ReadTestFile("test.png"); err != nil { 1017 t.Fatal(err) 1018 } else { 1019 fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png") 1020 CheckNoError(t, resp) 1021 1022 fileId = fileResp.FileInfos[0].Id 1023 } 1024 1025 // Wait a bit for files to ready 1026 time.Sleep(2 * time.Second) 1027 1028 data, resp := Client.GetFilePreview(fileId) 1029 CheckNoError(t, resp) 1030 1031 if len(data) == 0 { 1032 t.Fatal("should not be empty") 1033 } 1034 1035 _, resp = Client.GetFilePreview("junk") 1036 CheckBadRequestStatus(t, resp) 1037 1038 _, resp = Client.GetFilePreview(model.NewId()) 1039 CheckNotFoundStatus(t, resp) 1040 1041 Client.Logout() 1042 _, resp = Client.GetFilePreview(fileId) 1043 CheckUnauthorizedStatus(t, resp) 1044 1045 otherUser := th.CreateUser() 1046 Client.Login(otherUser.Email, otherUser.Password) 1047 _, resp = Client.GetFilePreview(fileId) 1048 CheckForbiddenStatus(t, resp) 1049 1050 Client.Logout() 1051 _, resp = th.SystemAdminClient.GetFilePreview(fileId) 1052 CheckNoError(t, resp) 1053 } 1054 1055 func TestGetFileInfo(t *testing.T) { 1056 th := Setup().InitBasic() 1057 defer th.TearDown() 1058 Client := th.Client 1059 user := th.BasicUser 1060 channel := th.BasicChannel 1061 1062 if *th.App.Config().FileSettings.DriverName == "" { 1063 t.Skip("skipping because no file driver is enabled") 1064 } 1065 1066 fileId := "" 1067 var sent []byte 1068 var err error 1069 if sent, err = testutils.ReadTestFile("test.png"); err != nil { 1070 t.Fatal(err) 1071 } else { 1072 fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png") 1073 CheckNoError(t, resp) 1074 1075 fileId = fileResp.FileInfos[0].Id 1076 } 1077 1078 // Wait a bit for files to ready 1079 time.Sleep(2 * time.Second) 1080 1081 info, resp := Client.GetFileInfo(fileId) 1082 CheckNoError(t, resp) 1083 1084 if err != nil { 1085 t.Fatal(err) 1086 } else if info.Id != fileId { 1087 t.Fatal("got incorrect file") 1088 } else if info.CreatorId != user.Id { 1089 t.Fatal("file should be assigned to user") 1090 } else if info.PostId != "" { 1091 t.Fatal("file shouldn't have a post") 1092 } else if info.Path != "" { 1093 t.Fatal("file path shouldn't have been returned to client") 1094 } else if info.ThumbnailPath != "" { 1095 t.Fatal("file thumbnail path shouldn't have been returned to client") 1096 } else if info.PreviewPath != "" { 1097 t.Fatal("file preview path shouldn't have been returned to client") 1098 } else if info.MimeType != "image/png" { 1099 t.Fatal("mime type should've been image/png") 1100 } 1101 1102 _, resp = Client.GetFileInfo("junk") 1103 CheckBadRequestStatus(t, resp) 1104 1105 _, resp = Client.GetFileInfo(model.NewId()) 1106 CheckNotFoundStatus(t, resp) 1107 1108 Client.Logout() 1109 _, resp = Client.GetFileInfo(fileId) 1110 CheckUnauthorizedStatus(t, resp) 1111 1112 otherUser := th.CreateUser() 1113 Client.Login(otherUser.Email, otherUser.Password) 1114 _, resp = Client.GetFileInfo(fileId) 1115 CheckForbiddenStatus(t, resp) 1116 1117 Client.Logout() 1118 _, resp = th.SystemAdminClient.GetFileInfo(fileId) 1119 CheckNoError(t, resp) 1120 } 1121 1122 func TestGetPublicFile(t *testing.T) { 1123 th := Setup().InitBasic() 1124 defer th.TearDown() 1125 Client := th.Client 1126 channel := th.BasicChannel 1127 1128 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true }) 1129 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) }) 1130 1131 fileId := "" 1132 if data, err := testutils.ReadTestFile("test.png"); err != nil { 1133 t.Fatal(err) 1134 } else { 1135 fileResp, resp := Client.UploadFile(data, channel.Id, "test.png") 1136 CheckNoError(t, resp) 1137 1138 fileId = fileResp.FileInfos[0].Id 1139 } 1140 1141 // Hacky way to assign file to a post (usually would be done by CreatePost call) 1142 store.Must(th.App.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id, th.BasicUser.Id)) 1143 1144 result := <-th.App.Srv.Store.FileInfo().Get(fileId) 1145 info := result.Data.(*model.FileInfo) 1146 link := th.App.GeneratePublicLink(Client.Url, info) 1147 1148 // Wait a bit for files to ready 1149 time.Sleep(2 * time.Second) 1150 1151 if resp, err := http.Get(link); err != nil || resp.StatusCode != http.StatusOK { 1152 t.Log(link) 1153 t.Fatal("failed to get image with public link", err) 1154 } 1155 1156 if resp, err := http.Get(link[:strings.LastIndex(link, "?")]); err == nil && resp.StatusCode != http.StatusBadRequest { 1157 t.Fatal("should've failed to get image with public link without hash", resp.Status) 1158 } 1159 1160 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = false }) 1161 if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusNotImplemented { 1162 t.Fatal("should've failed to get image with disabled public link") 1163 } 1164 1165 // test after the salt has changed 1166 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true }) 1167 th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) }) 1168 1169 if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusBadRequest { 1170 t.Fatal("should've failed to get image with public link after salt changed") 1171 } 1172 1173 if resp, err := http.Get(link); err == nil && resp.StatusCode != http.StatusBadRequest { 1174 t.Fatal("should've failed to get image with public link after salt changed") 1175 } 1176 1177 if err := th.cleanupTestFile(store.Must(th.App.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil { 1178 t.Fatal(err) 1179 } 1180 1181 th.cleanupTestFile(info) 1182 }