github.com/ethersphere/bee/v2@v2.2.0/pkg/api/bzz_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 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "mime" 14 "mime/multipart" 15 "net/http" 16 "strconv" 17 "strings" 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/file/redundancy" 23 "github.com/ethersphere/bee/v2/pkg/jsonhttp" 24 "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" 25 "github.com/ethersphere/bee/v2/pkg/log" 26 "github.com/ethersphere/bee/v2/pkg/manifest" 27 mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" 28 mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" 29 "github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore" 30 mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" 31 "github.com/ethersphere/bee/v2/pkg/swarm" 32 "github.com/ethersphere/bee/v2/pkg/util/testutil/pseudorand" 33 ) 34 35 // nolint:paralleltest,tparallel,thelper 36 37 // TestBzzUploadDownloadWithRedundancy tests the API for upload and download files 38 // with all combinations of redundancy level, encryption and size (levels, i.e., the 39 // 40 // height of the swarm hash tree). 41 // 42 // This is a variation on the same play as TestJoinerRedundancy 43 // but here the tested scenario is simplified since we are not testing the intricacies of 44 // download strategies, but only correct parameter passing and correct recovery functionality 45 // 46 // The test cases have the following structure: 47 // 48 // 1. upload a file with a given redundancy level and encryption 49 // 50 // 2. [positive test] download the file by the reference returned by the upload API response 51 // This uses range queries to target specific (number of) chunks of the file structure. 52 // During path traversal in the swarm hash tree, the underlying mocksore (forgetting) 53 // is in 'recording' mode, flagging all the retrieved chunks as chunks to forget. 54 // This is to simulate the scenario where some of the chunks are not available/lost 55 // NOTE: For this to work one needs to switch off lookaheadbuffer functionality 56 // (see langos pkg) 57 // 58 // 3. [negative test] attempt at downloading the file using once again the same root hash 59 // and the same redundancy strategy to find the file inaccessible after forgetting. 60 // 61 // 4. [positive test] attempt at downloading the file using a strategy that allows for 62 // using redundancy to reconstruct the file and find the file recoverable. 63 // 64 // nolint:thelper 65 func TestBzzUploadDownloadWithRedundancy_FLAKY(t *testing.T) { 66 t.Skip("flaky") 67 t.Parallel() 68 fileUploadResource := "/bzz" 69 fileDownloadResource := func(addr string) string { return "/bzz/" + addr + "/" } 70 71 testRedundancy := func(t *testing.T, rLevel redundancy.Level, encrypt bool, levels int, chunkCnt int, shardCnt int, parityCnt int) { 72 t.Helper() 73 seed, err := pseudorand.NewSeed() 74 if err != nil { 75 t.Fatal(err) 76 } 77 store := mockstorer.NewForgettingStore(inmemchunkstore.New()) 78 storerMock := mockstorer.NewWithChunkStore(store) 79 client, _, _, _ := newTestServer(t, testServerOptions{ 80 Storer: storerMock, 81 Logger: log.Noop, 82 Post: mockpost.New(mockpost.WithAcceptAll()), 83 }) 84 85 dataReader := pseudorand.NewReader(seed, chunkCnt*swarm.ChunkSize) 86 87 var refResponse api.BzzUploadResponse 88 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource, 89 http.StatusCreated, 90 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "True"), 91 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 92 jsonhttptest.WithRequestBody(dataReader), 93 jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, fmt.Sprintf("%t", encrypt)), 94 jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, fmt.Sprintf("%d", rLevel)), 95 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 96 jsonhttptest.WithUnmarshalJSONResponse(&refResponse), 97 ) 98 99 t.Run("download multiple ranges without redundancy should succeed", func(t *testing.T) { 100 // the underlying chunk store is in recording mode, so all chunks retrieved 101 // in this test will be forgotten in the subsequent ones. 102 store.Record() 103 defer store.Unrecord() 104 // we intend to forget as many chunks as possible for the given redundancy level 105 forget := parityCnt 106 if parityCnt > shardCnt { 107 forget = shardCnt 108 } 109 if levels == 1 { 110 forget = 2 111 } 112 start, end := 420, 450 113 gap := swarm.ChunkSize 114 for j := 2; j < levels; j++ { 115 gap *= shardCnt 116 } 117 ranges := make([][2]int, forget) 118 for i := 0; i < forget; i++ { 119 pre := i * gap 120 ranges[i] = [2]int{pre + start, pre + end} 121 } 122 rangeHeader, want := createRangeHeader(dataReader, ranges) 123 124 var body []byte 125 respHeaders := jsonhttptest.Request(t, client, http.MethodGet, 126 fileDownloadResource(refResponse.Reference.String()), 127 http.StatusPartialContent, 128 jsonhttptest.WithRequestHeader(api.RangeHeader, rangeHeader), 129 jsonhttptest.WithRequestHeader(api.SwarmLookAheadBufferSizeHeader, "0"), 130 // set for the replicas so that no replica gets deleted 131 jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, "0"), 132 jsonhttptest.WithRequestHeader(api.SwarmRedundancyStrategyHeader, "0"), 133 jsonhttptest.WithRequestHeader(api.SwarmRedundancyFallbackModeHeader, "false"), 134 jsonhttptest.WithPutResponseBody(&body), 135 ) 136 137 got := parseRangeParts(t, respHeaders.Get(api.ContentTypeHeader), body) 138 139 if len(got) != len(want) { 140 t.Fatalf("got %v parts, want %v parts", len(got), len(want)) 141 } 142 for i := 0; i < len(want); i++ { 143 if !bytes.Equal(got[i], want[i]) { 144 t.Errorf("part %v: got %q, want %q", i, string(got[i]), string(want[i])) 145 } 146 } 147 }) 148 149 t.Run("download without redundancy should NOT succeed", func(t *testing.T) { 150 if rLevel == 0 { 151 t.Skip("NA") 152 } 153 req, err := http.NewRequestWithContext(context.Background(), "GET", fileDownloadResource(refResponse.Reference.String()), nil) 154 if err != nil { 155 t.Fatal(err) 156 } 157 req.Header.Set(api.SwarmRedundancyStrategyHeader, "0") 158 req.Header.Set(api.SwarmRedundancyFallbackModeHeader, "false") 159 160 resp, err := client.Do(req) 161 if err != nil { 162 t.Fatal(err) 163 } 164 defer resp.Body.Close() 165 166 if resp.StatusCode != http.StatusOK { 167 t.Fatalf("expected status %d; got %d", http.StatusOK, resp.StatusCode) 168 } 169 _, err = dataReader.Seek(0, io.SeekStart) 170 if err != nil { 171 t.Fatal(err) 172 } 173 ok, err := dataReader.Equal(resp.Body) 174 if err != nil { 175 t.Fatal(err) 176 } 177 if ok { 178 t.Fatal("there should be missing data") 179 } 180 }) 181 182 t.Run("download with redundancy should succeed", func(t *testing.T) { 183 req, err := http.NewRequestWithContext(context.Background(), "GET", fileDownloadResource(refResponse.Reference.String()), nil) 184 if err != nil { 185 t.Fatal(err) 186 } 187 req.Header.Set(api.SwarmRedundancyStrategyHeader, "3") 188 req.Header.Set(api.SwarmRedundancyFallbackModeHeader, "true") 189 190 resp, err := client.Do(req) 191 if err != nil { 192 t.Fatal(err) 193 } 194 defer resp.Body.Close() 195 196 if resp.StatusCode != http.StatusOK { 197 t.Fatalf("expected status %d; got %d", http.StatusOK, resp.StatusCode) 198 } 199 _, err = dataReader.Seek(0, io.SeekStart) 200 if err != nil { 201 t.Fatal(err) 202 } 203 ok, err := dataReader.Equal(resp.Body) 204 if err != nil { 205 t.Fatal(err) 206 } 207 if !ok { 208 t.Fatalf("content mismatch") 209 } 210 }) 211 } 212 for _, rLevel := range []redundancy.Level{1, 2, 3, 4} { 213 rLevel := rLevel 214 t.Run(fmt.Sprintf("level=%d", rLevel), func(t *testing.T) { 215 for _, encrypt := range []bool{false, true} { 216 encrypt := encrypt 217 shardCnt := rLevel.GetMaxShards() 218 parityCnt := rLevel.GetParities(shardCnt) 219 if encrypt { 220 shardCnt = rLevel.GetMaxEncShards() 221 parityCnt = rLevel.GetEncParities(shardCnt) 222 } 223 for _, levels := range []int{1, 2, 3} { 224 chunkCnt := 1 225 switch levels { 226 case 1: 227 chunkCnt = 2 228 case 2: 229 chunkCnt = shardCnt + 1 230 case 3: 231 chunkCnt = shardCnt*shardCnt + 1 232 } 233 levels := levels 234 t.Run(fmt.Sprintf("encrypt=%v levels=%d chunks=%d", encrypt, levels, chunkCnt), func(t *testing.T) { 235 if levels > 2 && (encrypt == (rLevel%2 == 1)) { 236 t.Skip("skipping to save time") 237 } 238 t.Parallel() 239 testRedundancy(t, rLevel, encrypt, levels, chunkCnt, shardCnt, parityCnt) 240 }) 241 } 242 } 243 }) 244 } 245 } 246 247 func TestBzzFiles(t *testing.T) { 248 t.Parallel() 249 250 var ( 251 fileUploadResource = "/bzz" 252 fileDownloadResource = func(addr string) string { return "/bzz/" + addr } 253 simpleData = []byte("this is a simple text") 254 storerMock = mockstorer.New() 255 logger = log.Noop 256 client, _, _, _ = newTestServer(t, testServerOptions{ 257 Storer: storerMock, 258 Logger: logger, 259 Post: mockpost.New(mockpost.WithAcceptAll()), 260 }) 261 ) 262 263 t.Run("tar-file-upload", func(t *testing.T) { 264 tr := tarFiles(t, []f{ 265 { 266 data: []byte("robots text"), 267 name: "robots.txt", 268 dir: "", 269 header: http.Header{ 270 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 271 }, 272 }, 273 { 274 data: []byte("image 1"), 275 name: "1.png", 276 dir: "img", 277 header: http.Header{ 278 api.ContentTypeHeader: {"image/png"}, 279 }, 280 }, 281 { 282 data: []byte("image 2"), 283 name: "2.png", 284 dir: "img", 285 header: http.Header{ 286 api.ContentTypeHeader: {"image/png"}, 287 }, 288 }, 289 }) 290 address := swarm.MustParseHexAddress("f30c0aa7e9e2a0ef4c9b1b750ebfeaeb7c7c24da700bb089da19a46e3677824b") 291 rcvdHeader := jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource, http.StatusCreated, 292 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 293 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 294 jsonhttptest.WithRequestBody(tr), 295 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 296 jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ 297 Reference: address, 298 }), 299 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 300 ) 301 302 isTagFoundInResponse(t, rcvdHeader, nil) 303 304 has, err := storerMock.ChunkStore().Has(context.Background(), address) 305 if err != nil { 306 t.Fatal(err) 307 } 308 if !has { 309 t.Fatal("storer check root chunk address: have none; want one") 310 } 311 312 refs, err := storerMock.Pins() 313 if err != nil { 314 t.Fatal("unable to get pinned references") 315 } 316 if have, want := len(refs), 0; have != want { 317 t.Fatalf("root pin count mismatch: have %d; want %d", have, want) 318 } 319 }) 320 321 t.Run("tar-file-upload-with-pinning", func(t *testing.T) { 322 tr := tarFiles(t, []f{ 323 { 324 data: []byte("robots text"), 325 name: "robots.txt", 326 dir: "", 327 header: http.Header{ 328 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 329 }, 330 }, 331 { 332 data: []byte("image 1"), 333 name: "1.png", 334 dir: "img", 335 header: http.Header{ 336 api.ContentTypeHeader: {"image/png"}, 337 }, 338 }, 339 { 340 data: []byte("image 2"), 341 name: "2.png", 342 dir: "img", 343 header: http.Header{ 344 api.ContentTypeHeader: {"image/png"}, 345 }, 346 }, 347 }) 348 reference := swarm.MustParseHexAddress("f30c0aa7e9e2a0ef4c9b1b750ebfeaeb7c7c24da700bb089da19a46e3677824b") 349 rcvdHeader := jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource, http.StatusCreated, 350 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 351 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 352 jsonhttptest.WithRequestHeader(api.SwarmPinHeader, "true"), 353 jsonhttptest.WithRequestBody(tr), 354 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 355 jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ 356 Reference: reference, 357 }), 358 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 359 ) 360 361 isTagFoundInResponse(t, rcvdHeader, nil) 362 363 has, err := storerMock.ChunkStore().Has(context.Background(), reference) 364 if err != nil { 365 t.Fatal(err) 366 } 367 if !has { 368 t.Fatal("storer check root chunk reference: have none; want one") 369 } 370 371 refs, err := storerMock.Pins() 372 if err != nil { 373 t.Fatal(err) 374 } 375 if have, want := len(refs), 1; have != want { 376 t.Fatalf("root pin count mismatch: have %d; want %d", have, want) 377 } 378 if have, want := refs[0], reference; !have.Equal(want) { 379 t.Fatalf("root pin reference mismatch: have %q; want %q", have, want) 380 } 381 }) 382 383 t.Run("encrypt-decrypt", func(t *testing.T) { 384 fileName := "my-pictures.jpeg" 385 386 var resp api.BzzUploadResponse 387 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, 388 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 389 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 390 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 391 jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, "True"), 392 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 393 jsonhttptest.WithUnmarshalJSONResponse(&resp), 394 ) 395 396 jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(resp.Reference.String()), http.StatusOK, 397 jsonhttptest.WithExpectedContentLength(len(simpleData)), 398 jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 399 jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), 400 jsonhttptest.WithExpectedResponse(simpleData), 401 ) 402 }) 403 404 t.Run("redundancy", func(t *testing.T) { 405 fileName := "my-pictures.jpeg" 406 407 var resp api.BzzUploadResponse 408 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, 409 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 410 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 411 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 412 jsonhttptest.WithRequestHeader(api.SwarmEncryptHeader, "True"), 413 jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, "4"), 414 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 415 jsonhttptest.WithUnmarshalJSONResponse(&resp), 416 ) 417 418 jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(resp.Reference.String()), http.StatusOK, 419 jsonhttptest.WithExpectedContentLength(len(simpleData)), 420 jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 421 jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), 422 jsonhttptest.WithExpectedResponse(simpleData), 423 ) 424 }) 425 426 t.Run("filter out filename path", func(t *testing.T) { 427 fileName := "my-pictures.jpeg" 428 fileNameWithPath := "../../" + fileName 429 430 var resp api.BzzUploadResponse 431 432 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileNameWithPath, http.StatusCreated, 433 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 434 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 435 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 436 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 437 jsonhttptest.WithUnmarshalJSONResponse(&resp), 438 ) 439 440 rootHash := resp.Reference.String() 441 jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(rootHash), http.StatusOK, 442 jsonhttptest.WithExpectedContentLength(len(simpleData)), 443 jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 444 jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), 445 jsonhttptest.WithExpectedResponse(simpleData), 446 ) 447 }) 448 449 t.Run("check-content-type-detection", func(t *testing.T) { 450 fileName := "my-pictures.jpeg" 451 rootHash := "4f9146b3813ccbd7ce45a18be23763d7e436ab7a3982ef39961c6f3cd4da1dcf" 452 453 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, 454 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 455 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 456 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 457 jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ 458 Reference: swarm.MustParseHexAddress(rootHash), 459 }), 460 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 461 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 462 ) 463 464 jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(rootHash), http.StatusOK, 465 jsonhttptest.WithExpectedResponse(simpleData), 466 jsonhttptest.WithExpectedContentLength(len(simpleData)), 467 jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "image/jpeg; charset=utf-8"), 468 jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), 469 ) 470 }) 471 472 t.Run("upload-then-download-and-check-data", func(t *testing.T) { 473 fileName := "sample.html" 474 rootHash := "36e6c1bbdfee6ac21485d5f970479fd1df458d36df9ef4e8179708ed46da557f" 475 sampleHtml := `<!DOCTYPE html> 476 <html> 477 <body> 478 479 <h1>My First Heading</h1> 480 481 <p>My first paragraph.</p> 482 483 </body> 484 </html>` 485 486 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, 487 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 488 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 489 jsonhttptest.WithRequestBody(strings.NewReader(sampleHtml)), 490 jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ 491 Reference: swarm.MustParseHexAddress(rootHash), 492 }), 493 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), 494 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 495 jsonhttptest.WithExpectedResponseHeader(api.ETagHeader, fmt.Sprintf("%q", rootHash)), 496 ) 497 498 // try to fetch the same file and check the data 499 jsonhttptest.Request(t, client, http.MethodGet, fileDownloadResource(rootHash), http.StatusOK, 500 jsonhttptest.WithExpectedResponse([]byte(sampleHtml)), 501 jsonhttptest.WithExpectedContentLength(len(sampleHtml)), 502 jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), 503 jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, fmt.Sprintf(`inline; filename="%s"`, fileName)), 504 ) 505 }) 506 507 t.Run("upload-then-download-with-targets", func(t *testing.T) { 508 fileName := "simple_file.txt" 509 rootHash := "65148cd89b58e91616773f5acea433f7b5a6274f2259e25f4893a332b74a7e28" 510 511 jsonhttptest.Request(t, client, http.MethodPost, fileUploadResource+"?name="+fileName, http.StatusCreated, 512 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 513 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 514 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 515 jsonhttptest.WithExpectedJSONResponse(api.BzzUploadResponse{ 516 Reference: swarm.MustParseHexAddress(rootHash), 517 }), 518 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), 519 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 520 ) 521 522 t.Run("head", func(t *testing.T) { 523 rootHash := "65148cd89b58e91616773f5acea433f7b5a6274f2259e25f4893a332b74a7e28" 524 525 jsonhttptest.Request(t, client, http.MethodHead, fileDownloadResource(rootHash), http.StatusOK, 526 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 527 jsonhttptest.WithRequestBody(bytes.NewReader(simpleData)), 528 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), 529 jsonhttptest.WithExpectedContentLength(21), 530 ) 531 }) 532 }) 533 } 534 535 // TestRangeRequests validates that all endpoints are serving content with 536 // respect to HTTP Range headers. 537 func TestBzzFilesRangeRequests(t *testing.T) { 538 t.Parallel() 539 540 data := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus dignissim tincidunt orci id aliquam. Praesent eget turpis in lectus semper consectetur et ut nibh. Nam rhoncus, augue sit amet sollicitudin lacinia, turpis tortor molestie urna, at mattis sem sapien sit amet augue. In bibendum ex vel odio dignissim interdum. Quisque hendrerit sapien et porta condimentum. Vestibulum efficitur mauris tellus, eget vestibulum sapien vulputate ac. Proin et vulputate sapien. Duis tincidunt mauris vulputate porta venenatis. Sed dictum aliquet urna, sit amet fermentum velit pellentesque vitae. Nam sed nisi ultrices, volutpat quam et, malesuada sapien. Nunc gravida non orci at rhoncus. Sed vitae dui accumsan, venenatis lectus et, mattis tellus. Proin sed mauris eu mi congue lacinia.") 541 542 uploads := []struct { 543 name string 544 uploadEndpoint string 545 downloadEndpoint string 546 filepath string 547 reader io.Reader 548 contentType string 549 }{ 550 { 551 name: "bytes", 552 uploadEndpoint: "/bytes", 553 downloadEndpoint: "/bytes", 554 reader: bytes.NewReader(data), 555 contentType: "text/plain; charset=utf-8", 556 }, 557 { 558 name: "file", 559 uploadEndpoint: "/bzz", 560 downloadEndpoint: "/bzz", 561 reader: bytes.NewReader(data), 562 contentType: "text/plain; charset=utf-8", 563 }, 564 { 565 name: "dir", 566 uploadEndpoint: "/bzz", 567 downloadEndpoint: "/bzz", 568 filepath: "ipsum/lorem.txt", 569 reader: tarFiles(t, []f{ 570 { 571 data: data, 572 name: "lorem.txt", 573 dir: "ipsum", 574 header: http.Header{ 575 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 576 }, 577 }, 578 }), 579 contentType: api.ContentTypeTar, 580 }, 581 } 582 583 ranges := []struct { 584 name string 585 ranges [][2]int 586 }{ 587 { 588 name: "all", 589 ranges: [][2]int{{0, len(data)}}, 590 }, 591 { 592 name: "all without end", 593 ranges: [][2]int{{0, -1}}, 594 }, 595 { 596 name: "all without start", 597 ranges: [][2]int{{-1, len(data)}}, 598 }, 599 { 600 name: "head", 601 ranges: [][2]int{{0, 50}}, 602 }, 603 { 604 name: "tail", 605 ranges: [][2]int{{250, len(data)}}, 606 }, 607 { 608 name: "middle", 609 ranges: [][2]int{{10, 15}}, 610 }, 611 { 612 name: "multiple", 613 ranges: [][2]int{{10, 15}, {100, 125}}, 614 }, 615 { 616 name: "even more multiple parts", 617 ranges: [][2]int{{10, 15}, {100, 125}, {250, 252}, {261, 270}, {270, 280}}, 618 }, 619 } 620 621 for _, upload := range uploads { 622 upload := upload 623 t.Run(upload.name, func(t *testing.T) { 624 t.Parallel() 625 626 logger := log.Noop 627 client, _, _, _ := newTestServer(t, testServerOptions{ 628 Storer: mockstorer.New(), 629 Logger: logger, 630 Post: mockpost.New(mockpost.WithAcceptAll()), 631 }) 632 633 var resp api.BzzUploadResponse 634 635 testOpts := []jsonhttptest.Option{ 636 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 637 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 638 jsonhttptest.WithRequestBody(upload.reader), 639 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, upload.contentType), 640 jsonhttptest.WithUnmarshalJSONResponse(&resp), 641 jsonhttptest.WithNonEmptyResponseHeader(api.SwarmTagHeader), 642 } 643 if upload.name == "dir" { 644 testOpts = append(testOpts, jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True")) 645 } 646 647 jsonhttptest.Request(t, client, http.MethodPost, upload.uploadEndpoint, http.StatusCreated, 648 testOpts..., 649 ) 650 651 var downloadPath string 652 if upload.downloadEndpoint != "/bytes" { 653 downloadPath = upload.downloadEndpoint + "/" + resp.Reference.String() + "/" + upload.filepath 654 } else { 655 downloadPath = upload.downloadEndpoint + "/" + resp.Reference.String() 656 } 657 658 for _, tc := range ranges { 659 t.Run(tc.name, func(t *testing.T) { 660 rangeHeader, want := createRangeHeader(data, tc.ranges) 661 662 var body []byte 663 respHeaders := jsonhttptest.Request(t, client, http.MethodGet, 664 downloadPath, 665 http.StatusPartialContent, 666 jsonhttptest.WithRequestHeader(api.RangeHeader, rangeHeader), 667 jsonhttptest.WithPutResponseBody(&body), 668 ) 669 670 got := parseRangeParts(t, respHeaders.Get(api.ContentTypeHeader), body) 671 672 if len(got) != len(want) { 673 t.Fatalf("got %v parts, want %v parts", len(got), len(want)) 674 } 675 for i := 0; i < len(want); i++ { 676 if !bytes.Equal(got[i], want[i]) { 677 t.Errorf("part %v: got %q, want %q", i, string(got[i]), string(want[i])) 678 } 679 } 680 }) 681 } 682 }) 683 } 684 } 685 686 func createRangeHeader(data interface{}, ranges [][2]int) (header string, parts [][]byte) { 687 getLen := func() int { 688 switch data := data.(type) { 689 case []byte: 690 return len(data) 691 case interface{ Size() int }: 692 return data.Size() 693 default: 694 panic("unknown data type") 695 } 696 } 697 getRange := func(start, end int) []byte { 698 switch data := data.(type) { 699 case []byte: 700 return data[start:end] 701 case io.ReadSeeker: 702 buf := make([]byte, end-start) 703 _, err := data.Seek(int64(start), io.SeekStart) 704 if err != nil { 705 panic(err) 706 } 707 _, err = io.ReadFull(data, buf) 708 if err != nil { 709 panic(err) 710 } 711 return buf 712 default: 713 panic("unknown data type") 714 } 715 } 716 717 rangeStrs := make([]string, len(ranges)) 718 for i, r := range ranges { 719 start, end := r[0], r[1] 720 switch { 721 case start < 0: 722 // Range: <unit>=-<suffix-length>, the parameter is length 723 rangeStrs[i] = "-" + strconv.Itoa(end) 724 start = 0 725 case r[1] < 0: 726 // Range: <unit>=<range-start>- 727 rangeStrs[i] = strconv.Itoa(start) + "-" 728 end = getLen() 729 default: 730 // Range: <unit>=<range-start>-<range-end>, end is inclusive 731 rangeStrs[i] = fmt.Sprintf("%v-%v", start, end-1) 732 } 733 parts = append(parts, getRange(start, end)) 734 } 735 header = "bytes=" + strings.Join(rangeStrs, ", ") // nolint:staticcheck 736 return header, parts 737 } 738 739 func parseRangeParts(t *testing.T, contentType string, body []byte) (parts [][]byte) { 740 t.Helper() 741 742 mimetype, params, _ := mime.ParseMediaType(contentType) 743 if mimetype != "multipart/byteranges" { 744 parts = append(parts, body) 745 return 746 } 747 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"]) 748 for part, err := mr.NextPart(); err == nil; part, err = mr.NextPart() { 749 value, err := io.ReadAll(part) 750 if err != nil { 751 t.Fatal(err) 752 } 753 parts = append(parts, value) 754 } 755 return parts 756 } 757 758 func TestFeedIndirection(t *testing.T) { 759 t.Parallel() 760 761 // first, "upload" some content for the update 762 var ( 763 updateData = []byte("<h1>Swarm Feeds Hello World!</h1>") 764 logger = log.Noop 765 storer = mockstorer.New() 766 client, _, _, _ = newTestServer(t, testServerOptions{ 767 Storer: storer, 768 Logger: logger, 769 Post: mockpost.New(mockpost.WithAcceptAll()), 770 }) 771 ) 772 // tar all the test case files 773 tarReader := tarFiles(t, []f{ 774 { 775 data: updateData, 776 name: "index.html", 777 dir: "", 778 filePath: "./index.html", 779 }, 780 }) 781 782 var resp api.BzzUploadResponse 783 784 options := []jsonhttptest.Option{ 785 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 786 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 787 jsonhttptest.WithRequestBody(tarReader), 788 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 789 jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), 790 jsonhttptest.WithUnmarshalJSONResponse(&resp), 791 jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"), 792 } 793 794 // verify directory tar upload response 795 jsonhttptest.Request(t, client, http.MethodPost, "/bzz", http.StatusCreated, options...) 796 797 if resp.Reference.String() == "" { 798 t.Fatalf("expected file reference, did not got any") 799 } 800 801 // now use the "content" to mock the feed lookup 802 // also, use the mocked mantaray chunks that unmarshal 803 // into a real manifest with the mocked feed values when 804 // called from the bzz endpoint. then call the bzz endpoint with 805 // the pregenerated feed root manifest hash 806 807 feedUpdate := toChunk(t, 121212, resp.Reference.Bytes()) 808 809 var ( 810 look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil) 811 factory = newMockFactory(look) 812 bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path } 813 ctx = context.Background() 814 ) 815 client, _, _, _ = newTestServer(t, testServerOptions{ 816 Storer: storer, 817 Logger: logger, 818 Feeds: factory, 819 }) 820 err := storer.Cache().Put(ctx, feedUpdate) 821 if err != nil { 822 t.Fatal(err) 823 } 824 m, err := manifest.NewDefaultManifest( 825 loadsave.New(storer.ChunkStore(), storer.Cache(), pipelineFactory(storer.Cache(), false, 0)), 826 false, 827 ) 828 if err != nil { 829 t.Fatal(err) 830 } 831 emptyAddr := make([]byte, 32) 832 err = m.Add(ctx, manifest.RootPath, manifest.NewEntry(swarm.NewAddress(emptyAddr), map[string]string{ 833 api.FeedMetadataEntryOwner: "8d3766440f0d7b949a5e32995d09619a7f86e632", 834 api.FeedMetadataEntryTopic: "abcc", 835 api.FeedMetadataEntryType: "epoch", 836 })) 837 if err != nil { 838 t.Fatal(err) 839 } 840 manifRef, err := m.Store(ctx) 841 if err != nil { 842 t.Fatal(err) 843 } 844 845 jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), ""), http.StatusOK, 846 jsonhttptest.WithExpectedResponse(updateData), 847 jsonhttptest.WithExpectedContentLength(len(updateData)), 848 ) 849 } 850 851 func Test_bzzDownloadHandler_invalidInputs(t *testing.T) { 852 t.Parallel() 853 854 client, _, _, _ := newTestServer(t, testServerOptions{}) 855 856 tests := []struct { 857 name string 858 address string 859 want jsonhttp.StatusResponse 860 }{{ 861 name: "address - odd hex string", 862 address: "123", 863 want: jsonhttp.StatusResponse{ 864 Code: http.StatusBadRequest, 865 Message: "invalid path params", 866 Reasons: []jsonhttp.Reason{ 867 { 868 Field: "address", 869 Error: api.ErrHexLength.Error(), 870 }, 871 }, 872 }, 873 }, { 874 name: "address - invalid hex character", 875 address: "123G", 876 want: jsonhttp.StatusResponse{ 877 Code: http.StatusBadRequest, 878 Message: "invalid path params", 879 Reasons: []jsonhttp.Reason{ 880 { 881 Field: "address", 882 Error: api.HexInvalidByteError('G').Error(), 883 }, 884 }, 885 }, 886 }} 887 888 for _, tc := range tests { 889 tc := tc 890 t.Run(tc.name, func(t *testing.T) { 891 t.Parallel() 892 893 jsonhttptest.Request(t, client, http.MethodGet, fmt.Sprintf("/bzz/%s/abc", tc.address), tc.want.Code, 894 jsonhttptest.WithExpectedJSONResponse(tc.want), 895 ) 896 }) 897 } 898 } 899 900 func TestInvalidBzzParams(t *testing.T) { 901 t.Parallel() 902 903 var ( 904 fileUploadResource = "/bzz" 905 storerMock = mockstorer.New() 906 logger = log.Noop 907 existsFn = func(id []byte) (bool, error) { 908 return false, errors.New("error") 909 } 910 ) 911 912 t.Run("batch unusable", func(t *testing.T) { 913 t.Parallel() 914 915 tr := tarFiles(t, []f{ 916 { 917 data: []byte("robots text"), 918 name: "robots.txt", 919 dir: "", 920 header: http.Header{ 921 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 922 }, 923 }, 924 }) 925 clientBatchUnusable, _, _, _ := newTestServer(t, testServerOptions{ 926 Storer: storerMock, 927 Logger: logger, 928 Post: mockpost.New(mockpost.WithAcceptAll()), 929 BatchStore: mockbatchstore.New(), 930 }) 931 jsonhttptest.Request(t, clientBatchUnusable, http.MethodPost, fileUploadResource, http.StatusUnprocessableEntity, 932 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 933 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 934 jsonhttptest.WithRequestBody(tr), 935 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 936 ) 937 938 }) 939 940 t.Run("batch exists", func(t *testing.T) { 941 t.Parallel() 942 943 tr := tarFiles(t, []f{ 944 { 945 data: []byte("robots text"), 946 name: "robots.txt", 947 dir: "", 948 header: http.Header{ 949 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 950 }, 951 }, 952 }) 953 clientBatchExists, _, _, _ := newTestServer(t, testServerOptions{ 954 Storer: storerMock, 955 Logger: logger, 956 Post: mockpost.New(mockpost.WithAcceptAll()), 957 BatchStore: mockbatchstore.New(mockbatchstore.WithExistsFunc(existsFn)), 958 }) 959 jsonhttptest.Request(t, clientBatchExists, http.MethodPost, fileUploadResource, http.StatusBadRequest, 960 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 961 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 962 jsonhttptest.WithRequestBody(tr), 963 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 964 ) 965 966 }) 967 968 t.Run("batch not found", func(t *testing.T) { 969 t.Parallel() 970 971 tr := tarFiles(t, []f{ 972 { 973 data: []byte("robots text"), 974 name: "robots.txt", 975 dir: "", 976 header: http.Header{ 977 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 978 }, 979 }, 980 }) 981 clientBatchExists, _, _, _ := newTestServer(t, testServerOptions{ 982 Storer: storerMock, 983 Logger: logger, 984 Post: mockpost.New(), 985 }) 986 jsonhttptest.Request(t, clientBatchExists, http.MethodPost, fileUploadResource, http.StatusNotFound, 987 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 988 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 989 jsonhttptest.WithRequestBody(tr), 990 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 991 ) 992 }) 993 994 t.Run("upload, invalid tag", func(t *testing.T) { 995 t.Parallel() 996 997 tr := tarFiles(t, []f{ 998 { 999 data: []byte("robots text"), 1000 name: "robots.txt", 1001 dir: "", 1002 header: http.Header{ 1003 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 1004 }, 1005 }, 1006 }) 1007 clientInvalidTag, _, _, _ := newTestServer(t, testServerOptions{ 1008 Storer: storerMock, 1009 Logger: logger, 1010 Post: mockpost.New(mockpost.WithAcceptAll()), 1011 }) 1012 1013 jsonhttptest.Request(t, clientInvalidTag, http.MethodPost, fileUploadResource, http.StatusBadRequest, 1014 jsonhttptest.WithRequestHeader(api.SwarmTagHeader, "tag"), 1015 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 1016 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 1017 jsonhttptest.WithRequestBody(tr), 1018 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar)) 1019 }) 1020 1021 t.Run("upload, tag not found", func(t *testing.T) { 1022 t.Parallel() 1023 1024 tr := tarFiles(t, []f{ 1025 { 1026 data: []byte("robots text"), 1027 name: "robots.txt", 1028 dir: "", 1029 header: http.Header{ 1030 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 1031 }, 1032 }, 1033 }) 1034 clientTagExists, _, _, _ := newTestServer(t, testServerOptions{ 1035 Storer: storerMock, 1036 Logger: logger, 1037 Post: mockpost.New(mockpost.WithAcceptAll()), 1038 }) 1039 1040 jsonhttptest.Request(t, clientTagExists, http.MethodPost, fileUploadResource, http.StatusNotFound, 1041 jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(10000, 10)), 1042 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), 1043 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 1044 jsonhttptest.WithRequestBody(tr), 1045 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar)) 1046 }) 1047 1048 t.Run("address not found", func(t *testing.T) { 1049 t.Parallel() 1050 1051 client, _, _, _ := newTestServer(t, testServerOptions{ 1052 Storer: storerMock, 1053 Logger: logger, 1054 Post: mockpost.New(mockpost.WithAcceptAll()), 1055 }) 1056 1057 address := "f30c0aa7e9e2a0ef4c9b1b750ebfeaeb7c7c24da700bb089da19a46e3677824b" 1058 jsonhttptest.Request(t, client, http.MethodGet, fmt.Sprintf("/bzz/%s/", address), http.StatusNotFound) 1059 }) 1060 1061 } 1062 1063 // TestDirectUploadBzz tests that the direct upload endpoint give correct error message in dev mode 1064 func TestDirectUploadBzz(t *testing.T) { 1065 t.Parallel() 1066 1067 var ( 1068 fileUploadResource = "/bzz" 1069 storerMock = mockstorer.New() 1070 logger = log.Noop 1071 ) 1072 1073 tr := tarFiles(t, []f{ 1074 { 1075 data: []byte("robots text"), 1076 name: "robots.txt", 1077 dir: "", 1078 header: http.Header{ 1079 api.ContentTypeHeader: {"text/plain; charset=utf-8"}, 1080 }, 1081 }, 1082 }) 1083 clientBatchUnusable, _, _, _ := newTestServer(t, testServerOptions{ 1084 Storer: storerMock, 1085 Logger: logger, 1086 Post: mockpost.New(mockpost.WithAcceptAll()), 1087 BatchStore: mockbatchstore.New(), 1088 BeeMode: api.DevMode, 1089 }) 1090 jsonhttptest.Request(t, clientBatchUnusable, http.MethodPost, fileUploadResource, http.StatusBadRequest, 1091 jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "false"), 1092 jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), 1093 jsonhttptest.WithRequestBody(tr), 1094 jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), 1095 jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ 1096 Message: api.ErrUnsupportedDevNodeOperation.Error(), 1097 Code: http.StatusBadRequest, 1098 }), 1099 ) 1100 }