github.com/ethersphere/bee/v2@v2.2.0/pkg/api/postage_test.go (about) 1 // Copyright 2021 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 "encoding/hex" 11 "errors" 12 "fmt" 13 "math/big" 14 "net/http" 15 "strconv" 16 "testing" 17 "time" 18 19 "github.com/ethereum/go-ethereum/common" 20 21 "github.com/ethersphere/bee/v2/pkg/api" 22 "github.com/ethersphere/bee/v2/pkg/bigint" 23 "github.com/ethersphere/bee/v2/pkg/jsonhttp" 24 "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" 25 "github.com/ethersphere/bee/v2/pkg/postage" 26 "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" 27 mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" 28 "github.com/ethersphere/bee/v2/pkg/postage/postagecontract" 29 contractMock "github.com/ethersphere/bee/v2/pkg/postage/postagecontract/mock" 30 postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing" 31 "github.com/ethersphere/bee/v2/pkg/sctx" 32 mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" 33 "github.com/ethersphere/bee/v2/pkg/transaction/backendmock" 34 ) 35 36 func TestPostageCreateStamp(t *testing.T) { 37 t.Parallel() 38 39 batchID := []byte{1, 2, 3, 4} 40 initialBalance := int64(1000) 41 depth := uint8(24) 42 label := "label" 43 txHash := common.HexToHash("0x1234") 44 createBatch := func(amount int64, depth uint8, label string) string { 45 return fmt.Sprintf("/stamps/%d/%d?label=%s", amount, depth, label) 46 } 47 48 t.Run("ok", func(t *testing.T) { 49 t.Parallel() 50 51 var immutable bool 52 contract := contractMock.New( 53 contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) { 54 if ib.Cmp(big.NewInt(initialBalance)) != 0 { 55 return common.Hash{}, nil, fmt.Errorf("called with wrong initial balance. wanted %d, got %d", initialBalance, ib) 56 } 57 immutable = i 58 if d != depth { 59 return common.Hash{}, nil, fmt.Errorf("called with wrong depth. wanted %d, got %d", depth, d) 60 } 61 if l != label { 62 return common.Hash{}, nil, fmt.Errorf("called with wrong label. wanted %s, got %s", label, l) 63 } 64 return txHash, batchID, nil 65 }), 66 ) 67 ts, _, _, _ := newTestServer(t, testServerOptions{ 68 PostageContract: contract, 69 }) 70 71 jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusCreated, 72 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 73 BatchID: batchID, 74 TxHash: txHash.String(), 75 }), 76 ) 77 78 if !immutable { 79 t.Fatalf("default batch should be immutable") 80 } 81 }) 82 83 t.Run("with-error", func(t *testing.T) { 84 t.Parallel() 85 86 contract := contractMock.New( 87 contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) { 88 return common.Hash{}, nil, errors.New("err") 89 }), 90 ) 91 ts, _, _, _ := newTestServer(t, testServerOptions{ 92 PostageContract: contract, 93 }) 94 95 jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusInternalServerError, 96 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 97 Code: http.StatusInternalServerError, 98 Message: "cannot create batch", 99 }), 100 ) 101 }) 102 103 t.Run("out-of-funds", func(t *testing.T) { 104 t.Parallel() 105 106 contract := contractMock.New( 107 contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) { 108 return common.Hash{}, nil, postagecontract.ErrInsufficientFunds 109 }), 110 ) 111 ts, _, _, _ := newTestServer(t, testServerOptions{ 112 PostageContract: contract, 113 }) 114 115 jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusBadRequest, 116 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 117 Code: http.StatusBadRequest, 118 Message: "out of funds", 119 }), 120 ) 121 }) 122 123 t.Run("depth less than bucket depth", func(t *testing.T) { 124 t.Parallel() 125 126 contract := contractMock.New( 127 contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) { 128 return common.Hash{}, nil, postagecontract.ErrInvalidDepth 129 }), 130 ) 131 ts, _, _, _ := newTestServer(t, testServerOptions{ 132 PostageContract: contract, 133 }) 134 135 jsonhttptest.Request(t, ts, http.MethodPost, "/stamps/1000/9", http.StatusBadRequest, 136 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 137 Code: http.StatusBadRequest, 138 Message: "invalid path params", 139 Reasons: []jsonhttp.Reason{ 140 { 141 Field: "depth", 142 Error: "want min:17", 143 }, 144 }, 145 }), 146 ) 147 }) 148 149 t.Run("mutable header", func(t *testing.T) { 150 t.Parallel() 151 152 var immutable bool 153 contract := contractMock.New( 154 contractMock.WithCreateBatchFunc(func(ctx context.Context, _ *big.Int, _ uint8, i bool, _ string) (common.Hash, []byte, error) { 155 immutable = i 156 return txHash, batchID, nil 157 }), 158 ) 159 ts, _, _, _ := newTestServer(t, testServerOptions{ 160 PostageContract: contract, 161 }) 162 163 jsonhttptest.Request(t, ts, http.MethodPost, "/stamps/1000/24", http.StatusCreated, 164 jsonhttptest.WithRequestHeader(api.ImmutableHeader, "false"), 165 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 166 BatchID: batchID, 167 TxHash: txHash.String(), 168 }), 169 ) 170 171 if immutable { 172 t.Fatalf("want false, got %v", immutable) 173 } 174 }) 175 176 t.Run("syncing in progress", func(t *testing.T) { 177 t.Parallel() 178 179 ts, _, _, _ := newTestServer(t, testServerOptions{ 180 SyncStatus: func() (bool, error) { return false, nil }, 181 }) 182 183 jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusServiceUnavailable, 184 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 185 Message: "syncing in progress", 186 Code: 503, 187 }), 188 ) 189 }) 190 t.Run("syncing failed", func(t *testing.T) { 191 t.Parallel() 192 193 ts, _, _, _ := newTestServer(t, testServerOptions{ 194 SyncStatus: func() (bool, error) { return true, errors.New("oops") }, 195 }) 196 197 jsonhttptest.Request(t, ts, http.MethodPost, createBatch(initialBalance, depth, label), http.StatusServiceUnavailable, 198 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 199 Message: "syncing failed", 200 Code: 503, 201 }), 202 ) 203 }) 204 } 205 206 func TestPostageGetStamps(t *testing.T) { 207 t.Parallel() 208 209 b := postagetesting.MustNewBatch(postagetesting.WithValue(20)) 210 211 si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true) 212 mp := mockpost.New(mockpost.WithIssuer(si)) 213 cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} 214 215 t.Run("single stamp", func(t *testing.T) { 216 t.Parallel() 217 218 bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) 219 ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) 220 221 jsonhttptest.Request(t, ts, http.MethodGet, "/stamps", http.StatusOK, 222 jsonhttptest.WithExpectedJSONResponse(&api.PostageStampsResponse{ 223 Stamps: []api.PostageStampResponse{ 224 { 225 BatchID: b.ID, 226 Utilization: si.Utilization(), 227 Usable: true, 228 Label: si.Label(), 229 Depth: si.Depth(), 230 Amount: bigint.Wrap(si.Amount()), 231 BucketDepth: si.BucketDepth(), 232 BlockNumber: si.BlockNumber(), 233 ImmutableFlag: si.ImmutableFlag(), 234 Exists: true, 235 BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. 236 }, 237 }, 238 }), 239 ) 240 }) 241 242 t.Run("single expired Stamp", func(t *testing.T) { 243 t.Parallel() 244 245 eb := postagetesting.MustNewBatch(postagetesting.WithValue(20)) 246 247 esi := postage.NewStampIssuer("", "", eb.ID, big.NewInt(3), 11, 10, 1000, true) 248 emp := mockpost.New(mockpost.WithIssuer(esi)) 249 err := emp.HandleStampExpiry(context.Background(), eb.ID) 250 if err != nil { 251 t.Fatal(err) 252 } 253 ecs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(15), CurrentPrice: big.NewInt(12)} 254 ebs := mock.New(mock.WithChainState(ecs)) 255 ts, _, _, _ := newTestServer(t, testServerOptions{Post: emp, BatchStore: ebs, BlockTime: 2 * time.Second}) 256 257 jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(eb.ID), http.StatusNotFound, 258 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 259 Message: "issuer does not exist", 260 Code: 404, 261 }), 262 ) 263 }) 264 } 265 266 // TestGetAllBatches tests that the endpoint that returns all living 267 // batches functions correctly. 268 func TestGetAllBatches(t *testing.T) { 269 t.Parallel() 270 271 b := postagetesting.MustNewBatch() 272 b.Value = big.NewInt(20) 273 si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true) 274 mp := mockpost.New(mockpost.WithIssuer(si)) 275 cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} 276 bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) 277 ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) 278 279 oneBatch := struct { 280 Batches []api.PostageBatchResponse `json:"batches"` 281 }{ 282 Batches: []api.PostageBatchResponse{ 283 { 284 BatchID: b.ID, 285 Value: bigint.Wrap(b.Value), 286 Start: b.Start, 287 Owner: b.Owner, 288 Depth: b.Depth, 289 BucketDepth: b.BucketDepth, 290 Immutable: b.Immutable, 291 BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. 292 }, 293 }, 294 } 295 296 t.Run("all stamps", func(t *testing.T) { 297 t.Parallel() 298 299 jsonhttptest.Request(t, ts, http.MethodGet, "/batches", http.StatusOK, 300 jsonhttptest.WithExpectedJSONResponse(oneBatch), 301 ) 302 }) 303 } 304 305 func TestPostageGetStamp(t *testing.T) { 306 t.Parallel() 307 308 b := postagetesting.MustNewBatch() 309 b.Value = big.NewInt(20) 310 si := postage.NewStampIssuer("", "", b.ID, big.NewInt(3), 11, 10, 1000, true) 311 mp := mockpost.New(mockpost.WithIssuer(si)) 312 cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} 313 bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) 314 ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) 315 316 t.Run("ok", func(t *testing.T) { 317 t.Parallel() 318 319 jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(b.ID), http.StatusOK, 320 jsonhttptest.WithExpectedJSONResponse(&api.PostageStampResponse{ 321 BatchID: b.ID, 322 Utilization: si.Utilization(), 323 Usable: true, 324 Label: si.Label(), 325 Depth: si.Depth(), 326 Amount: bigint.Wrap(si.Amount()), 327 BucketDepth: si.BucketDepth(), 328 BlockNumber: si.BlockNumber(), 329 ImmutableFlag: si.ImmutableFlag(), 330 Exists: true, 331 BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. 332 }), 333 ) 334 }) 335 } 336 337 func TestPostageGetBuckets(t *testing.T) { 338 t.Parallel() 339 340 si := postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true) 341 mp := mockpost.New(mockpost.WithIssuer(si)) 342 ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp}) 343 buckets := make([]api.BucketData, 1024) 344 for i := range buckets { 345 buckets[i] = api.BucketData{BucketID: uint32(i)} 346 } 347 348 t.Run("ok", func(t *testing.T) { 349 t.Parallel() 350 351 jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+batchOkStr+"/buckets", http.StatusOK, 352 jsonhttptest.WithExpectedJSONResponse(&api.PostageStampBucketsResponse{ 353 Depth: si.Depth(), 354 BucketDepth: si.BucketDepth(), 355 BucketUpperBound: si.BucketUpperBound(), 356 Buckets: buckets, 357 }), 358 ) 359 }) 360 361 t.Run("batch not found", func(t *testing.T) { 362 t.Parallel() 363 364 mpNotFound := mockpost.New() 365 tsNotFound, _, _, _ := newTestServer(t, testServerOptions{Post: mpNotFound}) 366 367 jsonhttptest.Request(t, tsNotFound, http.MethodGet, "/stamps/"+batchOkStr+"/buckets", http.StatusNotFound) 368 }) 369 370 } 371 372 func TestReserveState(t *testing.T) { 373 t.Parallel() 374 375 t.Run("ok", func(t *testing.T) { 376 t.Parallel() 377 378 ts, _, _, _ := newTestServer(t, testServerOptions{ 379 BatchStore: mock.New(mock.WithRadius(5)), 380 Storer: mockstorer.New(), 381 }) 382 jsonhttptest.Request(t, ts, http.MethodGet, "/reservestate", http.StatusOK, 383 jsonhttptest.WithExpectedJSONResponse(&api.ReserveStateResponse{ 384 Radius: 5, 385 }), 386 ) 387 }) 388 t.Run("empty", func(t *testing.T) { 389 t.Parallel() 390 391 ts, _, _, _ := newTestServer(t, testServerOptions{ 392 BatchStore: mock.New(), 393 Storer: mockstorer.New(), 394 }) 395 jsonhttptest.Request(t, ts, http.MethodGet, "/reservestate", http.StatusOK, 396 jsonhttptest.WithExpectedJSONResponse(&api.ReserveStateResponse{}), 397 ) 398 }) 399 } 400 func TestChainState(t *testing.T) { 401 t.Parallel() 402 403 t.Run("ok", func(t *testing.T) { 404 t.Parallel() 405 406 cs := &postage.ChainState{ 407 Block: 123456, 408 TotalAmount: big.NewInt(50), 409 CurrentPrice: big.NewInt(5), 410 } 411 ts, _, _, _ := newTestServer(t, testServerOptions{ 412 BatchStore: mock.New(mock.WithChainState(cs)), 413 BackendOpts: []backendmock.Option{backendmock.WithBlockNumberFunc(func(ctx context.Context) (uint64, error) { 414 return 1, nil 415 })}, 416 }) 417 jsonhttptest.Request(t, ts, http.MethodGet, "/chainstate", http.StatusOK, 418 jsonhttptest.WithExpectedJSONResponse(&api.ChainStateResponse{ 419 ChainTip: 1, 420 Block: 123456, 421 TotalAmount: bigint.Wrap(big.NewInt(50)), 422 CurrentPrice: bigint.Wrap(big.NewInt(5)), 423 }), 424 ) 425 }) 426 427 } 428 429 func TestPostageTopUpStamp(t *testing.T) { 430 t.Parallel() 431 432 txHash := common.HexToHash("0x1234") 433 topupAmount := int64(1000) 434 topupBatch := func(id string, amount int64) string { 435 return fmt.Sprintf("/stamps/topup/%s/%d", id, amount) 436 } 437 438 t.Run("ok", func(t *testing.T) { 439 t.Parallel() 440 441 contract := contractMock.New( 442 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 443 if !bytes.Equal(id, batchOk) { 444 return common.Hash{}, errors.New("incorrect batch ID in call") 445 } 446 if ib.Cmp(big.NewInt(topupAmount)) != 0 { 447 return common.Hash{}, fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib) 448 } 449 return txHash, nil 450 }), 451 ) 452 ts, _, _, _ := newTestServer(t, testServerOptions{ 453 PostageContract: contract, 454 }) 455 456 jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted, 457 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 458 BatchID: batchOk, 459 TxHash: txHash.String(), 460 }), 461 ) 462 }) 463 464 t.Run("with-custom-gas", func(t *testing.T) { 465 t.Parallel() 466 467 contract := contractMock.New( 468 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 469 if !bytes.Equal(id, batchOk) { 470 return common.Hash{}, errors.New("incorrect batch ID in call") 471 } 472 if ib.Cmp(big.NewInt(topupAmount)) != 0 { 473 return common.Hash{}, fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib) 474 } 475 if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 { 476 return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx)) 477 } 478 return txHash, nil 479 }), 480 ) 481 ts, _, _, _ := newTestServer(t, testServerOptions{ 482 PostageContract: contract, 483 }) 484 485 jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted, 486 jsonhttptest.WithRequestHeader(api.GasPriceHeader, "10000"), 487 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 488 BatchID: batchOk, 489 TxHash: txHash.String(), 490 }), 491 ) 492 }) 493 494 t.Run("with-error", func(t *testing.T) { 495 t.Parallel() 496 497 contract := contractMock.New( 498 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 499 return common.Hash{}, errors.New("err") 500 }), 501 ) 502 ts, _, _, _ := newTestServer(t, testServerOptions{ 503 PostageContract: contract, 504 }) 505 506 jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusInternalServerError, 507 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 508 Code: http.StatusInternalServerError, 509 Message: "cannot topup batch", 510 }), 511 ) 512 }) 513 514 t.Run("out-of-funds", func(t *testing.T) { 515 t.Parallel() 516 517 contract := contractMock.New( 518 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 519 return common.Hash{}, postagecontract.ErrInsufficientFunds 520 }), 521 ) 522 ts, _, _, _ := newTestServer(t, testServerOptions{ 523 PostageContract: contract, 524 }) 525 526 jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusPaymentRequired, 527 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 528 Code: http.StatusPaymentRequired, 529 Message: "out of funds", 530 }), 531 ) 532 }) 533 534 t.Run("gas limit header", func(t *testing.T) { 535 t.Parallel() 536 537 contract := contractMock.New( 538 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 539 if sctx.GetGasLimit(ctx) != 10000 { 540 return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasLimit(ctx)) 541 } 542 return txHash, nil 543 }), 544 ) 545 ts, _, _, _ := newTestServer(t, testServerOptions{ 546 PostageContract: contract, 547 }) 548 549 jsonhttptest.Request(t, ts, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted, 550 jsonhttptest.WithRequestHeader(api.GasLimitHeader, "10000"), 551 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 552 BatchID: batchOk, 553 TxHash: txHash.String(), 554 }), 555 ) 556 }) 557 } 558 559 func TestPostageDiluteStamp(t *testing.T) { 560 t.Parallel() 561 562 txHash := common.HexToHash("0x1234") 563 newBatchDepth := uint8(17) 564 diluteBatch := func(id string, depth uint8) string { 565 return fmt.Sprintf("/stamps/dilute/%s/%d", id, depth) 566 } 567 568 t.Run("ok", func(t *testing.T) { 569 t.Parallel() 570 571 contract := contractMock.New( 572 contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) { 573 if !bytes.Equal(id, batchOk) { 574 return common.Hash{}, errors.New("incorrect batch ID in call") 575 } 576 if newDepth != newBatchDepth { 577 return common.Hash{}, fmt.Errorf("called with wrong depth. wanted %d, got %d", newBatchDepth, newDepth) 578 } 579 return txHash, nil 580 }), 581 ) 582 ts, _, _, _ := newTestServer(t, testServerOptions{ 583 PostageContract: contract, 584 }) 585 586 jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted, 587 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 588 BatchID: batchOk, 589 TxHash: txHash.String(), 590 }), 591 ) 592 }) 593 594 t.Run("with-custom-gas", func(t *testing.T) { 595 t.Parallel() 596 597 contract := contractMock.New( 598 contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) { 599 if !bytes.Equal(id, batchOk) { 600 return common.Hash{}, errors.New("incorrect batch ID in call") 601 } 602 if newDepth != newBatchDepth { 603 return common.Hash{}, fmt.Errorf("called with wrong depth. wanted %d, got %d", newBatchDepth, newDepth) 604 } 605 if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 { 606 return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx)) 607 } 608 return txHash, nil 609 }), 610 ) 611 ts, _, _, _ := newTestServer(t, testServerOptions{ 612 PostageContract: contract, 613 }) 614 615 jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted, 616 jsonhttptest.WithRequestHeader(api.GasPriceHeader, "10000"), 617 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 618 BatchID: batchOk, 619 TxHash: txHash.String(), 620 }), 621 ) 622 }) 623 624 t.Run("with-error", func(t *testing.T) { 625 t.Parallel() 626 627 contract := contractMock.New( 628 contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) { 629 return common.Hash{}, errors.New("err") 630 }), 631 ) 632 ts, _, _, _ := newTestServer(t, testServerOptions{ 633 PostageContract: contract, 634 }) 635 636 jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusInternalServerError, 637 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 638 Code: http.StatusInternalServerError, 639 Message: "cannot dilute batch", 640 }), 641 ) 642 }) 643 644 t.Run("with depth error", func(t *testing.T) { 645 t.Parallel() 646 647 contract := contractMock.New( 648 contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) { 649 return common.Hash{}, postagecontract.ErrInvalidDepth 650 }), 651 ) 652 ts, _, _, _ := newTestServer(t, testServerOptions{ 653 PostageContract: contract, 654 }) 655 656 jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusBadRequest, 657 jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ 658 Code: http.StatusBadRequest, 659 Message: "invalid depth", 660 }), 661 ) 662 }) 663 664 t.Run("gas limit header", func(t *testing.T) { 665 t.Parallel() 666 667 contract := contractMock.New( 668 contractMock.WithDiluteBatchFunc(func(ctx context.Context, _ []byte, _ uint8) (common.Hash, error) { 669 if sctx.GetGasLimit(ctx) != 10000 { 670 return common.Hash{}, fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasLimit(ctx)) 671 } 672 return txHash, nil 673 }), 674 ) 675 ts, _, _, _ := newTestServer(t, testServerOptions{ 676 PostageContract: contract, 677 }) 678 679 jsonhttptest.Request(t, ts, http.MethodPatch, diluteBatch(batchOkStr, newBatchDepth), http.StatusAccepted, 680 jsonhttptest.WithRequestHeader(api.GasLimitHeader, "10000"), 681 jsonhttptest.WithExpectedJSONResponse(&api.PostageCreateResponse{ 682 BatchID: batchOk, 683 TxHash: txHash.String(), 684 }), 685 ) 686 687 }) 688 } 689 690 // Tests the postageAccessHandler middleware for any set of operations that are guarded 691 // by the postage semaphore 692 func TestPostageAccessHandler(t *testing.T) { 693 t.Parallel() 694 695 txHash := common.HexToHash("0x1234") 696 697 type operation struct { 698 name string 699 method string 700 url string 701 respCode int 702 resp interface{} 703 } 704 705 success := []operation{ 706 { 707 name: "create batch ok", 708 method: http.MethodPost, 709 url: "/stamps/1000/24?label=test", 710 respCode: http.StatusCreated, 711 resp: &api.PostageCreateResponse{ 712 BatchID: batchOk, 713 TxHash: txHash.String(), 714 }, 715 }, 716 { 717 name: "topup batch ok", 718 method: http.MethodPatch, 719 url: fmt.Sprintf("/stamps/topup/%s/10", batchOkStr), 720 respCode: http.StatusAccepted, 721 resp: &api.PostageCreateResponse{ 722 BatchID: batchOk, 723 TxHash: txHash.String(), 724 }, 725 }, 726 { 727 name: "dilute batch ok", 728 method: http.MethodPatch, 729 url: fmt.Sprintf("/stamps/dilute/%s/18", batchOkStr), 730 respCode: http.StatusAccepted, 731 resp: &api.PostageCreateResponse{ 732 BatchID: batchOk, 733 TxHash: txHash.String(), 734 }, 735 }, 736 } 737 738 failure := []operation{ 739 { 740 name: "create batch not ok", 741 method: http.MethodPost, 742 url: "/stamps/1000/24?label=test", 743 respCode: http.StatusTooManyRequests, 744 resp: &jsonhttp.StatusResponse{ 745 Code: http.StatusTooManyRequests, 746 Message: "simultaneous on-chain operations not supported", 747 }, 748 }, 749 { 750 name: "topup batch not ok", 751 method: http.MethodPatch, 752 url: fmt.Sprintf("/stamps/topup/%s/10", batchOkStr), 753 respCode: http.StatusTooManyRequests, 754 resp: &jsonhttp.StatusResponse{ 755 Code: http.StatusTooManyRequests, 756 Message: "simultaneous on-chain operations not supported", 757 }, 758 }, 759 { 760 name: "dilute batch not ok", 761 method: http.MethodPatch, 762 url: fmt.Sprintf("/stamps/dilute/%s/18", batchOkStr), 763 respCode: http.StatusTooManyRequests, 764 resp: &jsonhttp.StatusResponse{ 765 Code: http.StatusTooManyRequests, 766 Message: "simultaneous on-chain operations not supported", 767 }, 768 }, 769 } 770 771 for _, op1 := range success { 772 for _, op2 := range failure { 773 op1 := op1 774 op2 := op2 775 t.Run(op1.name+"-"+op2.name, func(t *testing.T) { 776 t.Parallel() 777 778 wait, done := make(chan struct{}), make(chan struct{}) 779 contract := contractMock.New( 780 contractMock.WithCreateBatchFunc(func(ctx context.Context, ib *big.Int, d uint8, i bool, l string) (common.Hash, []byte, error) { 781 <-wait 782 return txHash, batchOk, nil 783 }), 784 contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) (common.Hash, error) { 785 <-wait 786 return txHash, nil 787 }), 788 contractMock.WithDiluteBatchFunc(func(ctx context.Context, id []byte, newDepth uint8) (common.Hash, error) { 789 <-wait 790 return txHash, nil 791 }), 792 ) 793 794 ts, _, _, _ := newTestServer(t, testServerOptions{ 795 PostageContract: contract, 796 }) 797 798 go func() { 799 defer close(done) 800 801 jsonhttptest.Request(t, ts, op1.method, op1.url, op1.respCode, jsonhttptest.WithExpectedJSONResponse(op1.resp)) 802 }() 803 804 time.Sleep(time.Millisecond * 100) 805 806 jsonhttptest.Request(t, ts, op2.method, op2.url, op2.respCode, jsonhttptest.WithExpectedJSONResponse(op2.resp)) 807 808 close(wait) 809 <-done 810 }) 811 } 812 } 813 } 814 815 //nolint:tparallel 816 func Test_postageCreateHandler_invalidInputs(t *testing.T) { 817 t.Parallel() 818 819 client, _, _, _ := newTestServer(t, testServerOptions{}) 820 821 tests := []struct { 822 name string 823 amount string 824 depth string 825 want jsonhttp.StatusResponse 826 }{{ 827 name: "amount - invalid value", 828 amount: "a", 829 depth: "1", 830 want: jsonhttp.StatusResponse{ 831 Code: http.StatusBadRequest, 832 Message: "invalid path params", 833 Reasons: []jsonhttp.Reason{ 834 { 835 Field: "amount", 836 Error: "invalid value", 837 }, 838 }, 839 }, 840 }, { 841 name: "depth - invalid value", 842 amount: "1", 843 depth: "a", 844 want: jsonhttp.StatusResponse{ 845 Code: http.StatusBadRequest, 846 Message: "invalid path params", 847 Reasons: []jsonhttp.Reason{ 848 { 849 Field: "depth", 850 Error: strconv.ErrSyntax.Error(), 851 }, 852 }, 853 }, 854 }} 855 856 //nolint:paralleltest 857 for _, tc := range tests { 858 t.Run(tc.name, func(t *testing.T) { 859 jsonhttptest.Request(t, client, http.MethodPost, "/stamps/"+tc.amount+"/"+tc.depth, tc.want.Code, 860 jsonhttptest.WithExpectedJSONResponse(tc.want), 861 ) 862 }) 863 } 864 } 865 866 func Test_postageGetStampBucketsHandler_invalidInputs(t *testing.T) { 867 t.Parallel() 868 869 client, _, _, _ := newTestServer(t, testServerOptions{}) 870 871 tests := []struct { 872 name string 873 batchID string 874 want jsonhttp.StatusResponse 875 }{{ 876 name: "batch_id - odd hex string", 877 batchID: "123", 878 want: jsonhttp.StatusResponse{ 879 Code: http.StatusBadRequest, 880 Message: "invalid path params", 881 Reasons: []jsonhttp.Reason{ 882 { 883 Field: "batch_id", 884 Error: api.ErrHexLength.Error(), 885 }, 886 }, 887 }, 888 }, { 889 name: "batch_id - invalid hex character", 890 batchID: "123G", 891 want: jsonhttp.StatusResponse{ 892 Code: http.StatusBadRequest, 893 Message: "invalid path params", 894 Reasons: []jsonhttp.Reason{ 895 { 896 Field: "batch_id", 897 Error: api.HexInvalidByteError('G').Error(), 898 }, 899 }, 900 }, 901 }, { 902 name: "batch_id - invalid length", 903 batchID: "1234", 904 want: jsonhttp.StatusResponse{ 905 Code: http.StatusBadRequest, 906 Message: "invalid path params", 907 Reasons: []jsonhttp.Reason{ 908 { 909 Field: "batch_id", 910 Error: "want len:32", 911 }, 912 }, 913 }, 914 }} 915 916 for _, tc := range tests { 917 tc := tc 918 t.Run(tc.name, func(t *testing.T) { 919 t.Parallel() 920 921 jsonhttptest.Request(t, client, http.MethodGet, "/stamps/"+tc.batchID+"/buckets", tc.want.Code, 922 jsonhttptest.WithExpectedJSONResponse(tc.want), 923 ) 924 }) 925 } 926 } 927 928 func Test_postageGetStampHandler_invalidInputs(t *testing.T) { 929 t.Parallel() 930 931 client, _, _, _ := newTestServer(t, testServerOptions{}) 932 933 tests := []struct { 934 name string 935 batchID string 936 want jsonhttp.StatusResponse 937 }{{ 938 name: "batch_id - odd hex string", 939 batchID: "123", 940 want: jsonhttp.StatusResponse{ 941 Code: http.StatusBadRequest, 942 Message: "invalid path params", 943 Reasons: []jsonhttp.Reason{ 944 { 945 Field: "batch_id", 946 Error: api.ErrHexLength.Error(), 947 }, 948 }, 949 }, 950 }, { 951 name: "batch_id - invalid hex character", 952 batchID: "123G", 953 want: jsonhttp.StatusResponse{ 954 Code: http.StatusBadRequest, 955 Message: "invalid path params", 956 Reasons: []jsonhttp.Reason{ 957 { 958 Field: "batch_id", 959 Error: api.HexInvalidByteError('G').Error(), 960 }, 961 }, 962 }, 963 }, { 964 name: "batch_id - invalid length", 965 batchID: "1234", 966 want: jsonhttp.StatusResponse{ 967 Code: http.StatusBadRequest, 968 Message: "invalid path params", 969 Reasons: []jsonhttp.Reason{ 970 { 971 Field: "batch_id", 972 Error: "want len:32", 973 }, 974 }, 975 }, 976 }} 977 978 for _, tc := range tests { 979 tc := tc 980 t.Run(tc.name, func(t *testing.T) { 981 t.Parallel() 982 983 jsonhttptest.Request(t, client, http.MethodGet, "/stamps/"+tc.batchID, tc.want.Code, 984 jsonhttptest.WithExpectedJSONResponse(tc.want), 985 ) 986 }) 987 } 988 } 989 990 //nolint:tparallel 991 func Test_postageTopUpHandler_invalidInputs(t *testing.T) { 992 t.Parallel() 993 994 client, _, _, _ := newTestServer(t, testServerOptions{}) 995 996 tests := []struct { 997 name string 998 batchID string 999 amount string 1000 want jsonhttp.StatusResponse 1001 }{{ 1002 name: "batch_id - odd hex string", 1003 batchID: "123", 1004 amount: "1", 1005 want: jsonhttp.StatusResponse{ 1006 Code: http.StatusBadRequest, 1007 Message: "invalid path params", 1008 Reasons: []jsonhttp.Reason{ 1009 { 1010 Field: "batch_id", 1011 Error: api.ErrHexLength.Error(), 1012 }, 1013 }, 1014 }, 1015 }, { 1016 name: "batch_id - invalid hex character", 1017 batchID: "123G", 1018 amount: "1", 1019 want: jsonhttp.StatusResponse{ 1020 Code: http.StatusBadRequest, 1021 Message: "invalid path params", 1022 Reasons: []jsonhttp.Reason{ 1023 { 1024 Field: "batch_id", 1025 Error: api.HexInvalidByteError('G').Error(), 1026 }, 1027 }, 1028 }, 1029 }, { 1030 name: "batch_id - invalid length", 1031 batchID: "1234", 1032 amount: "1", 1033 want: jsonhttp.StatusResponse{ 1034 Code: http.StatusBadRequest, 1035 Message: "invalid path params", 1036 Reasons: []jsonhttp.Reason{ 1037 { 1038 Field: "batch_id", 1039 Error: "want len:32", 1040 }, 1041 }, 1042 }, 1043 }, { 1044 name: "amount - invalid value", 1045 batchID: hex.EncodeToString([]byte{31: 0}), 1046 amount: "a", 1047 want: jsonhttp.StatusResponse{ 1048 Code: http.StatusBadRequest, 1049 Message: "invalid path params", 1050 Reasons: []jsonhttp.Reason{ 1051 { 1052 Field: "amount", 1053 Error: "invalid value", 1054 }, 1055 }, 1056 }, 1057 }} 1058 1059 //nolint:paralleltest 1060 for _, tc := range tests { 1061 t.Run(tc.name, func(t *testing.T) { 1062 jsonhttptest.Request(t, client, http.MethodPatch, "/stamps/topup/"+tc.batchID+"/"+tc.amount, tc.want.Code, 1063 jsonhttptest.WithExpectedJSONResponse(tc.want), 1064 ) 1065 }) 1066 } 1067 } 1068 1069 //nolint:tparallel 1070 func Test_postageDiluteHandler_invalidInputs(t *testing.T) { 1071 t.Parallel() 1072 1073 client, _, _, _ := newTestServer(t, testServerOptions{}) 1074 1075 tests := []struct { 1076 name string 1077 batchID string 1078 depth string 1079 want jsonhttp.StatusResponse 1080 }{{ 1081 name: "batch_id - odd hex string", 1082 batchID: "123", 1083 depth: "1", 1084 want: jsonhttp.StatusResponse{ 1085 Code: http.StatusBadRequest, 1086 Message: "invalid path params", 1087 Reasons: []jsonhttp.Reason{ 1088 { 1089 Field: "batch_id", 1090 Error: api.ErrHexLength.Error(), 1091 }, 1092 }, 1093 }, 1094 }, { 1095 name: "batch_id - invalid hex character", 1096 batchID: "123G", 1097 depth: "1", 1098 want: jsonhttp.StatusResponse{ 1099 Code: http.StatusBadRequest, 1100 Message: "invalid path params", 1101 Reasons: []jsonhttp.Reason{ 1102 { 1103 Field: "batch_id", 1104 Error: api.HexInvalidByteError('G').Error(), 1105 }, 1106 }, 1107 }, 1108 }, { 1109 name: "batch_id - invalid length", 1110 batchID: "1234", 1111 depth: "1", 1112 want: jsonhttp.StatusResponse{ 1113 Code: http.StatusBadRequest, 1114 Message: "invalid path params", 1115 Reasons: []jsonhttp.Reason{ 1116 { 1117 Field: "batch_id", 1118 Error: "want len:32", 1119 }, 1120 }, 1121 }, 1122 }, { 1123 name: "depth - invalid syntax", 1124 batchID: hex.EncodeToString([]byte{31: 0}), 1125 depth: "a", 1126 want: jsonhttp.StatusResponse{ 1127 Code: http.StatusBadRequest, 1128 Message: "invalid path params", 1129 Reasons: []jsonhttp.Reason{ 1130 { 1131 Field: "depth", 1132 Error: strconv.ErrSyntax.Error(), 1133 }, 1134 }, 1135 }, 1136 }} 1137 1138 //nolint:paralleltest 1139 for _, tc := range tests { 1140 t.Run(tc.name, func(t *testing.T) { 1141 jsonhttptest.Request(t, client, http.MethodPatch, "/stamps/dilute/"+tc.batchID+"/"+tc.depth, tc.want.Code, 1142 jsonhttptest.WithExpectedJSONResponse(tc.want), 1143 ) 1144 }) 1145 } 1146 }