github.com/ethersphere/bee/v2@v2.2.0/pkg/api/dirs_test.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package api_test 6 7 import ( 8 "archive/tar" 9 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "mime/multipart" 14 "net/http" 15 "net/textproto" 16 "path" 17 "strconv" 18 "testing" 19 20 "github.com/ethersphere/bee/v2/pkg/api" 21 "github.com/ethersphere/bee/v2/pkg/file/loadsave" 22 "github.com/ethersphere/bee/v2/pkg/jsonhttp" 23 "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" 24 "github.com/ethersphere/bee/v2/pkg/manifest" 25 mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" 26 mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" 27 "github.com/ethersphere/bee/v2/pkg/swarm" 28 ) 29 30 // nolint:paralleltest 31 func TestDirs(t *testing.T) { 32 var ( 33 dirUploadResource = "/bzz" 34 bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path } 35 ctx = context.Background() 36 storer = mockstorer.New() 37 client, _, _, _ = newTestServer(t, testServerOptions{ 38 Storer: storer, 39 PreventRedirect: true, 40 Post: mockpost.New(mockpost.WithAcceptAll()), 41 }) 42 ) 43 44 t.Run("empty request body", func(t *testing.T) { 45 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, 46 http.StatusBadRequest, 47 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 48 jsonhttptest.WithRequestBody(bytes.NewReader(nil)), 49 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 50 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 51 Message: api.InvalidRequest.Error(), 52 Code: http.StatusBadRequest, 53 }), 54 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 55 ) 56 }) 57 58 t.Run("non tar file", func(t *testing.T) { 59 file := bytes.NewReader([]byte("some data")) 60 61 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, 62 http.StatusInternalServerError, 63 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 64 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 65 jsonhttptest.WithRequestBody(file), 66 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 67 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 68 Message: api.DirectoryStoreError.Error(), 69 Code: http.StatusInternalServerError, 70 }), 71 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 72 ) 73 }) 74 75 t.Run("wrong content type", func(t *testing.T) { 76 tarReader := tarFiles(t, []f{{ 77 data: []byte("some data"), 78 name: "binary-file", 79 }}) 80 81 // submit valid tar, but with wrong content-type 82 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, 83 http.StatusBadRequest, 84 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 85 jsonhttptest.WithRequestBody(tarReader), 86 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 87 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 88 Message: api.InvalidContentType.Error(), 89 Code: http.StatusBadRequest, 90 }), 91 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "other"), 92 ) 93 }) 94 95 // valid tars 96 for _, tc := range []struct { 97 name string 98 expectedReference swarm.Address 99 encrypt bool 100 wantIndexFilename string 101 wantErrorFilename string 102 indexFilenameOption jsonhttptest.Option 103 errorFilenameOption jsonhttptest.Option 104 doMultipart bool 105 files []f // files in dir for test case 106 }{ 107 { 108 name: "non-nested files without extension", 109 expectedReference: swarm.MustParseHexAddress("f3312af64715d26b5e1a3dc90f012d2c9cc74a167899dab1d07cdee8c107f939"), 110 files: []f{ 111 { 112 data: []byte("first file data"), 113 name: "file1", 114 dir: "", 115 header: http.Header{ 116 api.ContentTypeHeader: {""}, 117 }, 118 }, 119 { 120 data: []byte("second file data"), 121 name: "file2", 122 dir: "", 123 header: http.Header{ 124 api.ContentTypeHeader: {""}, 125 }, 126 }, 127 }, 128 }, 129 { 130 name: "nested files with extension", 131 doMultipart: true, 132 expectedReference: swarm.MustParseHexAddress("4c9c76d63856102e54092c38a7cd227d769752d768b7adc8c3542e3dd9fcf295"), 133 files: []f{ 134 { 135 data: []byte("robots text"), 136 name: "robots.txt", 137 dir: "", 138 header: http.Header{ 139 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 140 }, 141 }, 142 { 143 data: []byte("image 1"), 144 name: "1.png", 145 dir: "img", 146 header: http.Header{ 147 api.ContentTypeHeader: {"image/png"}, 148 }, 149 }, 150 { 151 data: []byte("image 2"), 152 name: "2.png", 153 dir: "img", 154 header: http.Header{ 155 api.ContentTypeHeader: {"image/png"}, 156 }, 157 }, 158 }, 159 }, 160 { 161 name: "no index filename", 162 expectedReference: swarm.MustParseHexAddress("9e178dbd1ed4b748379e25144e28dfb29c07a4b5114896ef454480115a56b237"), 163 doMultipart: true, 164 files: []f{ 165 { 166 data: []byte("<h1>Swarm"), 167 name: "index.html", 168 dir: "", 169 header: http.Header{ 170 api.ContentTypeHeader: {"text/html; charset=utf-8"}, 171 }, 172 }, 173 }, 174 }, 175 { 176 name: "explicit index filename", 177 expectedReference: swarm.MustParseHexAddress("a58484e3d77bbdb40323ddc9020c6e96e5eb5deb52015d3e0f63cce629ac1aa6"), 178 wantIndexFilename: "index.html", 179 indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"), 180 doMultipart: true, 181 files: []f{ 182 { 183 data: []byte("<h1>Swarm"), 184 name: "index.html", 185 dir: "", 186 header: http.Header{ 187 api.ContentTypeHeader: {"text/html; charset=utf-8"}, 188 }, 189 }, 190 }, 191 }, 192 { 193 name: "nested index filename", 194 expectedReference: swarm.MustParseHexAddress("3e2f008a578c435efa7a1fce146e21c4ae8c20b80fbb4c4e0c1c87ca08fef414"), 195 wantIndexFilename: "index.html", 196 indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"), 197 files: []f{ 198 { 199 data: []byte("<h1>Swarm"), 200 name: "index.html", 201 dir: "dir", 202 header: http.Header{ 203 api.ContentTypeHeader: {"text/html; charset=utf-8"}, 204 }, 205 }, 206 }, 207 }, 208 { 209 name: "explicit index and error filename", 210 expectedReference: swarm.MustParseHexAddress("2cd9a6ac11eefbb71b372fb97c3ef64109c409955964a294fdc183c1014b3844"), 211 wantIndexFilename: "index.html", 212 wantErrorFilename: "error.html", 213 indexFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"), 214 errorFilenameOption: jsonhttptest.WithRequestHeader(api.SwarmErrorDocumentHeader, "error.html"), 215 doMultipart: true, 216 files: []f{ 217 { 218 data: []byte("<h1>Swarm"), 219 name: "index.html", 220 dir: "", 221 header: http.Header{ 222 api.ContentTypeHeader: {"text/html; charset=utf-8"}, 223 }, 224 }, 225 { 226 data: []byte("<h2>404"), 227 name: "error.html", 228 dir: "", 229 header: http.Header{ 230 api.ContentTypeHeader: {"text/html; charset=utf-8"}, 231 }, 232 }, 233 }, 234 }, 235 { 236 name: "invalid archive paths", 237 expectedReference: swarm.MustParseHexAddress("133c92414c047708f3d6a8561571a0cc96512899ff0edbd9690c857f01ab6883"), 238 files: []f{ 239 { 240 data: []byte("<h1>Swarm"), 241 name: "index.html", 242 dir: "", 243 filePath: "./index.html", 244 }, 245 { 246 data: []byte("body {}"), 247 name: "app.css", 248 dir: "", 249 filePath: "./app.css", 250 }, 251 { 252 data: []byte(`User-agent: * 253 Disallow: /`), 254 name: "robots.txt", 255 dir: "", 256 filePath: "./robots.txt", 257 }, 258 }, 259 }, 260 { 261 name: "encrypted", 262 encrypt: true, 263 files: []f{ 264 { 265 data: []byte("<h1>Swarm"), 266 name: "index.html", 267 dir: "", 268 filePath: "./index.html", 269 }, 270 }, 271 }, 272 } { 273 verify := func(t *testing.T, resp api.BzzUploadResponse) { 274 t.Helper() 275 // NOTE: reference will be different each time when encryption is enabled 276 if !tc.encrypt { 277 if !resp.Reference.Equal(tc.expectedReference) { 278 t.Fatalf("expected root reference to match %s, got %s", tc.expectedReference, resp.Reference) 279 } 280 } 281 282 // verify manifest content 283 verifyManifest, err := manifest.NewDefaultManifestReference( 284 resp.Reference, 285 loadsave.NewReadonly(storer.ChunkStore()), 286 ) 287 if err != nil { 288 t.Fatal(err) 289 } 290 291 validateFile := func(t *testing.T, file f, filePath string) { 292 t.Helper() 293 294 jsonhttptest.Request(t, client, http.MethodGet, 295 bzzDownloadResource(resp.Reference.String(), filePath), 296 http.StatusOK, 297 jsonhttptest.WithExpectedResponse(file.data), 298 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, file.header.Get(api.ContentTypeHeader)), 299 ) 300 } 301 302 validateIsPermanentRedirect := func(t *testing.T, fromPath, toPath string) { 303 t.Helper() 304 305 expectedResponse := fmt.Sprintf("<a href=\"%s\">Permanent Redirect</a>.\n\n", 306 bzzDownloadResource(resp.Reference.String(), toPath)) 307 308 jsonhttptest.Request(t, client, http.MethodGet, 309 bzzDownloadResource(resp.Reference.String(), fromPath), 310 http.StatusPermanentRedirect, 311 jsonhttptest.WithExpectedResponse([]byte(expectedResponse)), 312 ) 313 } 314 315 validateAltPath := func(t *testing.T, fromPath, toPath string) { 316 t.Helper() 317 318 var respBytes []byte 319 320 jsonhttptest.Request(t, client, http.MethodGet, 321 bzzDownloadResource(resp.Reference.String(), toPath), http.StatusOK, 322 jsonhttptest.WithPutResponseBody(&respBytes), 323 ) 324 325 jsonhttptest.Request(t, client, http.MethodGet, 326 bzzDownloadResource(resp.Reference.String(), fromPath), http.StatusOK, 327 jsonhttptest.WithExpectedResponse(respBytes), 328 ) 329 } 330 331 // check if each file can be located and read 332 for _, file := range tc.files { 333 validateFile(t, file, path.Join(file.dir, file.name)) 334 } 335 336 // check index filename 337 if tc.wantIndexFilename != "" { 338 entry, err := verifyManifest.Lookup(ctx, manifest.RootPath) 339 if err != nil { 340 t.Fatal(err) 341 } 342 343 manifestRootMetadata := entry.Metadata() 344 indexDocumentSuffixPath, ok := manifestRootMetadata[manifest.WebsiteIndexDocumentSuffixKey] 345 if !ok { 346 t.Fatalf("expected index filename '%s', did not find any", tc.wantIndexFilename) 347 } 348 349 // check index suffix for each dir 350 for _, file := range tc.files { 351 if file.dir != "" { 352 validateIsPermanentRedirect(t, file.dir, file.dir+"/") 353 validateAltPath(t, file.dir+"/", path.Join(file.dir, indexDocumentSuffixPath)) 354 } 355 } 356 } 357 358 // check error filename 359 if tc.wantErrorFilename != "" { 360 entry, err := verifyManifest.Lookup(ctx, manifest.RootPath) 361 if err != nil { 362 t.Fatal(err) 363 } 364 365 manifestRootMetadata := entry.Metadata() 366 errorDocumentPath, ok := manifestRootMetadata[manifest.WebsiteErrorDocumentPathKey] 367 if !ok { 368 t.Fatalf("expected error filename '%s', did not find any", tc.wantErrorFilename) 369 } 370 371 // check error document 372 validateAltPath(t, "_non_existent_file_path_", errorDocumentPath) 373 } 374 375 } 376 t.Run(tc.name, func(t *testing.T) { 377 t.Run("tar_upload", func(t *testing.T) { 378 // tar all the test case files 379 tarReader := tarFiles(t, tc.files) 380 381 var resp api.BzzUploadResponse 382 383 options := []jsonhttptest.Option{ 384 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 385 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 386 jsonhttptest.WithRequestBody(tarReader), 387 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 388 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 389 jsonhttptest.WithUnmarshalJSONResponse(&resp), 390 } 391 if tc.indexFilenameOption != nil { 392 options = append(options, tc.indexFilenameOption) 393 } 394 if tc.errorFilenameOption != nil { 395 options = append(options, tc.errorFilenameOption) 396 } 397 if tc.encrypt { 398 options = append(options, jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, "true")) 399 } 400 401 // verify directory tar upload response 402 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusCreated, options...) 403 404 if resp.Reference.String() == "" { 405 t.Fatalf("expected file reference, did not got any") 406 } 407 408 verify(t, resp) 409 }) 410 if tc.doMultipart { 411 t.Run("multipart_upload", func(t *testing.T) { 412 // tar all the test case files 413 mwReader, mwBoundary := multipartFiles(t, tc.files) 414 415 var resp api.BzzUploadResponse 416 417 options := []jsonhttptest.Option{ 418 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 419 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 420 jsonhttptest.WithRequestBody(mwReader), 421 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 422 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, fmt.Sprintf("multipart/form-data; boundary=%q", mwBoundary)), 423 jsonhttptest.WithUnmarshalJSONResponse(&resp), 424 } 425 if tc.indexFilenameOption != nil { 426 options = append(options, tc.indexFilenameOption) 427 } 428 if tc.errorFilenameOption != nil { 429 options = append(options, tc.errorFilenameOption) 430 } 431 if tc.encrypt { 432 options = append(options, jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, "true")) 433 } 434 435 // verify directory tar upload response 436 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusCreated, options...) 437 438 if resp.Reference.String() == "" { 439 t.Fatalf("expected file reference, did not got any") 440 } 441 442 verify(t, resp) 443 }) 444 } 445 }) 446 } 447 448 t.Run("upload invalid tag", func(t *testing.T) { 449 tr := tarFiles(t, []f{ 450 { 451 data: []byte("robots text"), 452 name: "robots.txt", 453 dir: "", 454 header: http.Header{ 455 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 456 }, 457 }, 458 }) 459 460 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusBadRequest, 461 jsonhttptest.WithRequestHeader(api.SwarmTagHeader, "tag"), 462 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 463 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 464 jsonhttptest.WithRequestBody(tr), 465 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 466 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 467 Message: "invalid header params", 468 Code: http.StatusBadRequest, 469 Reasons: []jsonhttp.Reason{ 470 { 471 Field: "Swarm-Tag", 472 Error: "invalid syntax", 473 }, 474 }, 475 }), 476 ) 477 }) 478 479 t.Run("upload tag not found", func(t *testing.T) { 480 tr := tarFiles(t, []f{ 481 { 482 data: []byte("robots text"), 483 name: "robots.txt", 484 dir: "", 485 header: http.Header{ 486 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 487 }, 488 }, 489 }) 490 491 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, http.StatusNotFound, 492 jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(uint64(10000), 10)), 493 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 494 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 495 jsonhttptest.WithRequestBody(tr), 496 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar)) 497 }) 498 } 499 500 func TestDirsEmtpyDir(t *testing.T) { 501 t.Parallel() 502 503 var ( 504 dirUploadResource = "/bzz" 505 storer = mockstorer.New() 506 client, _, _, _ = newTestServer(t, testServerOptions{ 507 Storer: storer, 508 PreventRedirect: true, 509 Post: mockpost.New(mockpost.WithAcceptAll()), 510 }) 511 ) 512 513 tarReader := tarEmptyDir(t) 514 515 jsonhttptest.Request(t, client, http.MethodPost, dirUploadResource, 516 http.StatusBadRequest, 517 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 518 jsonhttptest.WithRequestBody(tarReader), 519 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "true"), 520 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 521 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 522 Message: api.EmptyDir.Error(), 523 Code: http.StatusBadRequest, 524 }), 525 ) 526 } 527 528 // tarFiles receives an array of test case files and creates a new tar with those files as a collection 529 // it returns a bytes.Buffer which can be used to read the created tar 530 func tarFiles(t *testing.T, files []f) *bytes.Buffer { 531 t.Helper() 532 533 var buf bytes.Buffer 534 tw := tar.NewWriter(&buf) 535 536 for _, file := range files { 537 filePath := path.Join(file.dir, file.name) 538 if file.filePath != "" { 539 filePath = file.filePath 540 } 541 542 // create tar header and write it 543 hdr := &tar.Header{ 544 Name: filePath, 545 Mode: 0600, 546 Size: int64(len(file.data)), 547 } 548 if err := tw.WriteHeader(hdr); err != nil { 549 t.Fatal(err) 550 } 551 552 // write the file data to the tar 553 if _, err := tw.Write(file.data); err != nil { 554 t.Fatal(err) 555 } 556 } 557 558 // finally close the tar writer 559 if err := tw.Close(); err != nil { 560 t.Fatal(err) 561 } 562 563 return &buf 564 } 565 566 func tarEmptyDir(t *testing.T) *bytes.Buffer { 567 t.Helper() 568 569 var buf bytes.Buffer 570 tw := tar.NewWriter(&buf) 571 572 hdr := &tar.Header{ 573 Name: "empty/", 574 Mode: 0600, 575 } 576 577 if err := tw.WriteHeader(hdr); err != nil { 578 t.Fatal(err) 579 } 580 581 // finally close the tar writer 582 if err := tw.Close(); err != nil { 583 t.Fatal(err) 584 } 585 586 return &buf 587 } 588 589 func multipartFiles(t *testing.T, files []f) (*bytes.Buffer, string) { 590 t.Helper() 591 592 var buf bytes.Buffer 593 mw := multipart.NewWriter(&buf) 594 595 for _, file := range files { 596 filePath := path.Join(file.dir, file.name) 597 if file.filePath != "" { 598 filePath = file.filePath 599 } 600 601 hdr := make(textproto.MIMEHeader) 602 hdr.Set(api.ContentDispositionHeader, fmt.Sprintf("form-data; name=%q", filePath)) 603 604 contentType := file.header.Get(api.ContentTypeHeader) 605 if contentType != "" { 606 hdr.Set(api.ContentTypeHeader, contentType) 607 608 } 609 if len(file.data) > 0 { 610 hdr.Set(api.ContentLengthHeader, strconv.Itoa(len(file.data))) 611 612 } 613 part, err := mw.CreatePart(hdr) 614 if err != nil { 615 t.Fatal(err) 616 } 617 if _, err = io.Copy(part, bytes.NewBuffer(file.data)); err != nil { 618 t.Fatal(err) 619 } 620 } 621 622 // finally close the tar writer 623 if err := mw.Close(); err != nil { 624 t.Fatal(err) 625 } 626 627 return &buf, mw.Boundary() 628 } 629 630 // struct for dir files for test cases 631 type f struct { 632 data []byte 633 name string 634 dir string 635 filePath string 636 header http.Header 637 }