decred.org/dcrdex@v1.0.3/client/webserver/webserver_test.go (about) 1 //go:build !live 2 3 package webserver 4 5 import ( 6 "bytes" 7 "context" 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "math/rand" 14 "net" 15 "net/http" 16 "os" 17 "strings" 18 "testing" 19 "time" 20 21 "decred.org/dcrdex/client/asset" 22 "decred.org/dcrdex/client/core" 23 "decred.org/dcrdex/client/db" 24 "decred.org/dcrdex/client/mnemonic" 25 "decred.org/dcrdex/client/orderbook" 26 "decred.org/dcrdex/dex" 27 "decred.org/dcrdex/dex/encode" 28 "decred.org/dcrdex/dex/order" 29 "github.com/go-chi/chi/v5" 30 ) 31 32 var ( 33 tErr = fmt.Errorf("expected dummy error") 34 tLogger dex.Logger 35 tCtx context.Context 36 ) 37 38 type tCoin struct { 39 id []byte 40 confs uint32 41 confsErr error 42 } 43 44 func (c *tCoin) ID() dex.Bytes { 45 return c.id 46 } 47 48 func (c *tCoin) String() string { 49 return hex.EncodeToString(c.id) 50 } 51 52 func (c *tCoin) TxID() string { 53 return hex.EncodeToString(c.id) 54 } 55 56 func (c *tCoin) Value() uint64 { 57 return 0 58 } 59 60 func (c *tCoin) Confirmations(context.Context) (uint32, error) { 61 return c.confs, c.confsErr 62 } 63 64 type TCore struct { 65 balanceErr error 66 syncFeed core.BookFeed 67 syncErr error 68 postBondErr error 69 loginErr error 70 logoutErr error 71 initErr error 72 isInited bool 73 getDEXConfigErr error 74 createWalletErr error 75 openWalletErr error 76 closeWalletErr error 77 rescanWalletErr error 78 sendErr error 79 notHas bool 80 notRunning bool 81 notOpen bool 82 rateSourceErr error 83 estFee uint64 84 estFeeErr error 85 validAddr bool 86 walletDisabled bool 87 walletStatusErr error 88 deletedRecords int 89 deleteRecordsErr error 90 tradeErr error 91 notes []*db.Notification 92 notesErr error 93 } 94 95 func (c *TCore) Network() dex.Network { return dex.Mainnet } 96 func (c *TCore) Exchanges() map[string]*core.Exchange { return nil } 97 func (c *TCore) Exchange(host string) (*core.Exchange, error) { return nil, nil } 98 func (c *TCore) GetDEXConfig(dexAddr string, certI any) (*core.Exchange, error) { 99 return nil, c.getDEXConfigErr // TODO along with test for apiUser / Exchanges() / User() 100 } 101 func (c *TCore) AddDEX(appPW []byte, dexAddr string, certI any) error { 102 return nil 103 } 104 func (c *TCore) DiscoverAccount(dexAddr string, pw []byte, certI any) (*core.Exchange, bool, error) { 105 return nil, false, nil 106 } 107 func (c *TCore) PostBond(r *core.PostBondForm) (*core.PostBondResult, error) { 108 return nil, c.postBondErr 109 } 110 func (c *TCore) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) { 111 return 1, nil 112 } 113 func (c *TCore) UpdateBondOptions(form *core.BondOptionsForm) error { 114 return c.postBondErr 115 } 116 func (c *TCore) BondsFeeBuffer(assetID uint32) (uint64, error) { 117 return 222, nil 118 } 119 func (c *TCore) ToggleRateSourceStatus(src string, disable bool) error { 120 return c.rateSourceErr 121 } 122 func (c *TCore) FiatRateSources() map[string]bool { 123 return nil 124 } 125 126 func (c *TCore) InitializeClient(pw []byte, seed *string) (string, error) { 127 var mnemonicSeed string 128 if seed == nil { 129 _, mnemonicSeed = mnemonic.New() 130 } 131 return mnemonicSeed, c.initErr 132 } 133 func (c *TCore) Login(pw []byte) error { return c.loginErr } 134 func (c *TCore) IsInitialized() bool { return c.isInited } 135 func (c *TCore) SyncBook(dex string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) { 136 return nil, c.syncFeed, c.syncErr 137 } 138 func (c *TCore) Book(dex string, base, quote uint32) (*core.OrderBook, error) { 139 return &core.OrderBook{}, nil 140 } 141 func (c *TCore) AssetBalance(assetID uint32) (*core.WalletBalance, error) { return nil, c.balanceErr } 142 func (c *TCore) WalletState(assetID uint32) *core.WalletState { 143 if c.notHas { 144 return nil 145 } 146 return &core.WalletState{ 147 Symbol: unbip(assetID), 148 AssetID: assetID, 149 Open: !c.notOpen, 150 Running: !c.notRunning, 151 } 152 } 153 func (c *TCore) CreateWallet(appPW, walletPW []byte, form *core.WalletForm) error { 154 return c.createWalletErr 155 } 156 func (c *TCore) RescanWallet(assetID uint32, force bool) error { return c.rescanWalletErr } 157 func (c *TCore) OpenWallet(assetID uint32, pw []byte) error { return c.openWalletErr } 158 func (c *TCore) CloseWallet(assetID uint32) error { return c.closeWalletErr } 159 func (c *TCore) ConnectWallet(assetID uint32) error { return nil } 160 func (c *TCore) Wallets() []*core.WalletState { return nil } 161 func (c *TCore) WalletSettings(uint32) (map[string]string, error) { return nil, nil } 162 func (c *TCore) ReconfigureWallet(aPW, nPW []byte, form *core.WalletForm) error { 163 return nil 164 } 165 func (c *TCore) ToggleWalletStatus(assetID uint32, disable bool) error { 166 if c.walletStatusErr != nil { 167 return c.walletStatusErr 168 } 169 c.walletDisabled = disable 170 return c.walletStatusErr 171 } 172 func (c *TCore) ChangeAppPass(appPW, newAppPW []byte) error { return nil } 173 func (c *TCore) ResetAppPass(newAppPW []byte, seed string) error { return nil } 174 func (c *TCore) SetWalletPassword(appPW []byte, assetID uint32, newPW []byte) error { return nil } 175 func (c *TCore) NewDepositAddress(assetID uint32) (string, error) { return "", nil } 176 func (c *TCore) AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) { 177 return nil, nil 178 } 179 func (c *TCore) User() *core.User { return nil } 180 func (c *TCore) SupportedAssets() map[uint32]*core.SupportedAsset { 181 return make(map[uint32]*core.SupportedAsset) 182 } 183 func (c *TCore) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) { 184 return &tCoin{id: []byte{0xde, 0xc7, 0xed}}, c.sendErr 185 } 186 func (c *TCore) ValidateAddress(address string, assetID uint32) (bool, error) { 187 return c.validAddr, nil 188 } 189 func (c *TCore) EstimateSendTxFee(addr string, assetID uint32, value uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) { 190 return c.estFee, true, c.estFeeErr 191 } 192 func (c *TCore) Trade(pw []byte, form *core.TradeForm) (*core.Order, error) { 193 if c.tradeErr != nil { 194 return nil, c.tradeErr 195 } 196 return trade(form), nil 197 } 198 func (c *TCore) TradeAsync(pw []byte, form *core.TradeForm) (*core.InFlightOrder, error) { 199 if c.tradeErr != nil { 200 return nil, c.tradeErr 201 } 202 return &core.InFlightOrder{ 203 Order: trade(form), 204 TemporaryID: uint64(rand.Int63()), 205 }, nil 206 } 207 func trade(form *core.TradeForm) *core.Order { 208 oType := order.LimitOrderType 209 if !form.IsLimit { 210 oType = order.MarketOrderType 211 } 212 return &core.Order{ 213 Type: oType, 214 Stamp: uint64(time.Now().UnixMilli()), 215 Rate: form.Rate, 216 Qty: form.Qty, 217 Sell: form.Sell, 218 } 219 } 220 func (c *TCore) Cancel(oid dex.Bytes) error { return nil } 221 222 func (c *TCore) NotificationFeed() *core.NoteFeed { 223 return &core.NoteFeed{ 224 C: make(chan core.Notification, 1), 225 } 226 } 227 228 func (c *TCore) AckNotes(ids []dex.Bytes) {} 229 230 func (c *TCore) Logout() error { return c.logoutErr } 231 232 func (c *TCore) Orders(*core.OrderFilter) ([]*core.Order, error) { return nil, nil } 233 func (c *TCore) Order(oid dex.Bytes) (*core.Order, error) { return nil, nil } 234 func (c *TCore) MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) { 235 return nil, nil 236 } 237 func (c *TCore) MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) { 238 return nil, nil 239 } 240 func (c *TCore) PreOrder(*core.TradeForm) (*core.OrderEstimate, error) { 241 return nil, nil 242 } 243 func (c *TCore) AccountExport(pw []byte, host string) (*core.Account, []*db.Bond, error) { 244 return nil, nil, nil 245 } 246 func (c *TCore) AccountImport(pw []byte, account *core.Account, bonds []*db.Bond) error { 247 return nil 248 } 249 func (c *TCore) ToggleAccountStatus(pw []byte, host string, disable bool) error { return nil } 250 251 func (c *TCore) ExportSeed(pw []byte) (string, error) { 252 return "seed words here", nil 253 } 254 func (c *TCore) WalletLogFilePath(uint32) (string, error) { 255 return "", nil 256 } 257 func (c *TCore) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { 258 return "", nil 259 } 260 func (c *TCore) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { 261 return 0, nil 262 } 263 func (c *TCore) PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) { 264 return nil, nil 265 } 266 func (c *TCore) RecoverWallet(uint32, []byte, bool) error { 267 return nil 268 } 269 func (c *TCore) UpdateCert(string, []byte) error { 270 return nil 271 } 272 func (c *TCore) UpdateDEXHost(string, string, []byte, any) (*core.Exchange, error) { 273 return nil, nil 274 } 275 func (c *TCore) WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) { 276 return nil, nil 277 } 278 func (c *TCore) DeleteArchivedRecordsWithBackup(endDateTime *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) { 279 return "/path/to/records", c.deletedRecords, c.deleteRecordsErr 280 } 281 func (c *TCore) WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) { 282 return nil, nil 283 } 284 func (c *TCore) AddWalletPeer(assetID uint32, address string) error { 285 return nil 286 } 287 func (c *TCore) RemoveWalletPeer(assetID uint32, address string) error { 288 return nil 289 } 290 func (c *TCore) Notifications(n int) (notes, pokes []*db.Notification, _ error) { 291 return c.notes, []*db.Notification{}, c.notesErr 292 } 293 func (c *TCore) ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConfirm func()) (string, error) { 294 return "", nil 295 } 296 func (c *TCore) UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) { 297 return "", nil 298 } 299 func (c *TCore) ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) { 300 return 0, nil 301 } 302 func (c *TCore) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) { 303 return nil, nil 304 } 305 306 func (c *TCore) SetVSP(assetID uint32, addr string) error { 307 return nil 308 } 309 310 func (c *TCore) PurchaseTickets(assetID uint32, appPW []byte, n int) error { 311 return nil 312 } 313 314 func (c *TCore) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error { 315 return nil 316 } 317 318 func (c *TCore) ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) { 319 return nil, nil 320 } 321 322 func (c *TCore) TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) { 323 return nil, nil 324 } 325 326 func (c *TCore) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 327 return nil, nil 328 } 329 330 func (c *TCore) FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) { 331 return nil, nil 332 } 333 334 func (c *TCore) ConfigureFundsMixer(appPW []byte, assetID uint32, enabled bool) error { 335 return nil 336 } 337 338 func (c *TCore) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) { 339 return nil, 0, nil 340 } 341 342 func (*TCore) SetLanguage(string) error { return nil } 343 func (*TCore) Language() string { return "en-US" } 344 345 func (*TCore) TakeAction(assetID uint32, actionID string, actionB json.RawMessage) error { return nil } 346 347 func (*TCore) ExtensionModeConfig() *core.ExtensionModeConfig { 348 return nil 349 } 350 351 type TWriter struct { 352 b []byte 353 } 354 355 func (*TWriter) Header() http.Header { 356 return http.Header{} 357 } 358 359 func (w *TWriter) Write(b []byte) (int, error) { 360 w.b = b 361 return len(b), nil 362 } 363 364 func (w *TWriter) WriteHeader(int) {} 365 366 type TReader struct { 367 msg []byte 368 err error 369 } 370 371 func (r *TReader) Read(p []byte) (n int, err error) { 372 if r.err != nil { 373 return 0, r.err 374 } 375 if len(r.msg) == 0 { 376 return 0, io.EOF 377 } 378 copy(p, r.msg) 379 if len(p) < len(r.msg) { 380 r.msg = r.msg[:len(p)] 381 return len(p), nil 382 } 383 l := len(r.msg) 384 r.msg = nil 385 return l, io.EOF 386 } 387 388 func (r *TReader) Close() error { return nil } 389 390 func newTServer(t *testing.T, start bool) (*WebServer, *TCore, func()) { 391 t.Helper() 392 c := &TCore{} 393 var shutdown func() 394 ctx, killCtx := context.WithCancel(tCtx) 395 s, err := New(&Config{ 396 Core: c, 397 Addr: "127.0.0.1:0", 398 Logger: tLogger, 399 }) 400 if err != nil { 401 t.Fatalf("error creating server: %v", err) 402 } 403 404 if start { 405 cm := dex.NewConnectionMaster(s) 406 err := cm.Connect(ctx) 407 if err != nil { 408 t.Fatalf("Error starting WebServer: %v", err) 409 } 410 shutdown = func() { 411 killCtx() 412 cm.Disconnect() 413 } 414 } else { 415 shutdown = killCtx 416 } 417 return s, c, shutdown 418 } 419 420 func ensureResponse(t *testing.T, f func(w http.ResponseWriter, r *http.Request), want string, reader *TReader, writer *TWriter, body any, cookies map[string]string) { 421 t.Helper() 422 var err error 423 reader.msg, err = json.Marshal(body) 424 if err != nil { 425 t.Fatalf("error marshalling request body: %v", err) 426 } 427 req, err := http.NewRequest("GET", "/", reader) 428 if err != nil { 429 t.Fatalf("error creating request: %v", err) 430 } 431 for name, value := range cookies { 432 cookie := http.Cookie{ 433 Name: name, 434 Value: value, 435 } 436 req.AddCookie(&cookie) 437 } 438 f(writer, req) 439 if len(writer.b) == 0 { 440 t.Fatalf("no response") 441 } 442 // Drop the line feed. 443 errMsg := string(writer.b[:len(writer.b)-1]) 444 if errMsg != want { 445 t.Fatalf("wrong response. expected %s, got %s", want, errMsg) 446 } 447 writer.b = nil 448 } 449 450 func TestMain(m *testing.M) { 451 tLogger = dex.StdOutLogger("TEST", dex.LevelTrace) 452 var shutdown func() 453 tCtx, shutdown = context.WithCancel(context.Background()) 454 doIt := func() int { 455 // Not counted as coverage, must test Archiver constructor explicitly. 456 defer shutdown() 457 return m.Run() 458 } 459 os.Exit(doIt()) 460 } 461 462 func TestNew_siteError(t *testing.T) { 463 cwd, err := os.Getwd() 464 if err != nil { 465 t.Fatalf("cannot get current directory: %v", err) 466 } 467 468 // Change to a directory with no "site" or "../../webserver/site" folder. 469 dir := t.TempDir() 470 defer os.Chdir(cwd) // leave the temp dir before trying to delete it 471 472 if err = os.Chdir(dir); err != nil { 473 t.Fatalf("Cannot cd to %q", dir) 474 } 475 476 c := &TCore{} 477 _, err = New(&Config{ 478 Core: c, 479 Addr: "127.0.0.1:0", 480 Logger: tLogger, 481 NoEmbed: true, // this tests locating on-disk files, not the embedded ones 482 }) 483 if err == nil || !strings.HasPrefix(err.Error(), "no HTML template files found") { 484 t.Errorf("Should have failed to start with no site folder.") 485 } 486 } 487 488 func TestConnectStart(t *testing.T) { 489 _, _, shutdown := newTServer(t, true) 490 defer shutdown() 491 } 492 493 func TestConnectBindError(t *testing.T) { 494 s0, _, shutdown := newTServer(t, true) 495 defer shutdown() 496 497 tAddr := s0.addr 498 s, err := New(&Config{ 499 Core: &TCore{}, 500 Addr: tAddr, 501 Logger: tLogger, 502 }) 503 if err != nil { 504 t.Fatalf("error creating server: %v", err) 505 } 506 507 cm := dex.NewConnectionMaster(s) 508 err = cm.Connect(tCtx) 509 if err == nil { 510 shutdown() // shutdown both servers with shared context 511 cm.Disconnect() 512 t.Fatalf("should have failed to bind") 513 } 514 } 515 516 func TestAPILogin(t *testing.T) { 517 writer := new(TWriter) 518 var body any 519 reader := new(TReader) 520 s, tCore, shutdown := newTServer(t, false) 521 defer shutdown() 522 523 ensure := func(want string) { 524 t.Helper() 525 ensureResponse(t, s.apiLogin, want, reader, writer, body, nil) 526 } 527 528 goodBody := &loginForm{ 529 Pass: encode.PassBytes("def"), 530 } 531 body = goodBody 532 ensure(`{"ok":true,"notes":null,"pokes":[]}`) 533 534 tCore.notes = []*db.Notification{{ 535 TopicID: core.TopicAccountUnlockError, 536 }} 537 ensure(`{"ok":true,"notes":[{"type":"","topic":"AccountUnlockError","subject":"","details":"","severity":0,"stamp":0,"acked":false,"id":""}],"pokes":[]}`) 538 539 tCore.notes = nil 540 tCore.notesErr = errors.New("") 541 ensure(`{"ok":true,"notes":null,"pokes":[]}`) 542 543 // Login error 544 tCore.loginErr = tErr 545 ensure(fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr)) 546 tCore.loginErr = nil 547 } 548 549 func TestSend(t *testing.T) { 550 writer := new(TWriter) 551 var body any 552 reader := new(TReader) 553 s, tCore, shutdown := newTServer(t, false) 554 defer shutdown() 555 556 isOK := func() bool { 557 reader.msg, _ = json.Marshal(body) 558 req, err := http.NewRequest("GET", "/", reader) 559 if err != nil { 560 t.Fatalf("error creating request: %v", err) 561 } 562 s.apiSend(writer, req) 563 if len(writer.b) == 0 { 564 t.Fatalf("no response") 565 } 566 resp := &standardResponse{} 567 err = json.Unmarshal(writer.b, resp) 568 if err != nil { 569 t.Fatalf("json unmarshal error: %v", err) 570 } 571 return resp.OK 572 } 573 574 body = &sendForm{ 575 Pass: encode.PassBytes("dummyAppPass"), 576 } 577 578 // initial success 579 if !isOK() { 580 t.Fatalf("not ok: %s", string(writer.b)) 581 } 582 583 // no wallet 584 tCore.notHas = true 585 if isOK() { 586 t.Fatalf("no error for missing wallet") 587 } 588 tCore.notHas = false 589 590 // Send/Withdraw error 591 tCore.sendErr = tErr 592 if isOK() { 593 t.Fatalf("no error for Send/Withdraw error") 594 } 595 tCore.sendErr = nil 596 597 // re-success 598 if !isOK() { 599 t.Fatalf("not ok afterwards: %s", string(writer.b)) 600 } 601 } 602 603 func TestAPIInit(t *testing.T) { 604 writer := new(TWriter) 605 var body any 606 reader := new(TReader) 607 s, tCore, shutdown := newTServer(t, false) 608 defer shutdown() 609 610 ensure := func(f func(http.ResponseWriter, *http.Request), want string) { 611 t.Helper() 612 ensureResponse(t, f, want, reader, writer, body, nil) 613 } 614 615 body = struct{}{} 616 617 // Success but uninitialized 618 ensure(s.apiIsInitialized, `{"ok":true,"initialized":false}`) 619 620 // Now initialized 621 tCore.isInited = true 622 ensure(s.apiIsInitialized, `{"ok":true,"initialized":true}`) 623 624 // Initialization error 625 tCore.initErr = tErr 626 ensure(s.apiInit, fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr)) 627 tCore.initErr = nil 628 } 629 630 // TODO: TestAPIGetDEXInfo 631 632 func TestAPINewWallet(t *testing.T) { 633 writer := new(TWriter) 634 var body any 635 reader := new(TReader) 636 s, tCore, shutdown := newTServer(t, false) 637 defer shutdown() 638 639 ensure := func(want string) { 640 ensureResponse(t, s.apiNewWallet, want, reader, writer, body, nil) 641 } 642 643 body = &newWalletForm{ 644 Pass: encode.PassBytes("abc"), 645 AppPW: encode.PassBytes("dummyAppPass"), 646 } 647 tCore.notHas = true 648 ensure(`{"ok":true}`) 649 650 tCore.notHas = false 651 ensure(`{"ok":false,"msg":"already have a wallet for btc"}`) 652 tCore.notHas = true 653 654 tCore.createWalletErr = tErr 655 ensure(fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr)) 656 tCore.createWalletErr = nil 657 658 tCore.notHas = false 659 } 660 661 func TestAPILogout(t *testing.T) { 662 writer := new(TWriter) 663 reader := new(TReader) 664 s, tCore, shutdown := newTServer(t, false) 665 defer shutdown() 666 667 ensure := func(want string) { 668 ensureResponse(t, s.apiLogout, want, reader, writer, nil, nil) 669 } 670 ensure(`{"ok":true}`) 671 672 // Logout error 673 tCore.logoutErr = tErr 674 ensure(fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr)) 675 tCore.logoutErr = nil 676 } 677 678 func TestApiGetBalance(t *testing.T) { 679 writer := new(TWriter) 680 reader := new(TReader) 681 s, tCore, shutdown := newTServer(t, false) 682 defer shutdown() 683 684 ensure := func(want string) { 685 ensureResponse(t, s.apiGetBalance, want, reader, writer, struct{}{}, nil) 686 } 687 ensure(`{"ok":true,"balance":null}`) 688 689 // Logout error 690 tCore.balanceErr = tErr 691 ensure(fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr)) 692 tCore.balanceErr = nil 693 } 694 695 type tHTTPHandler struct { 696 req *http.Request 697 } 698 699 func (h *tHTTPHandler) ServeHTTP(_ http.ResponseWriter, req *http.Request) { 700 h.req = req 701 } 702 703 func TestOrderIDCtx(t *testing.T) { 704 hexOID := hex.EncodeToString(encode.RandomBytes(32)) 705 req := (&http.Request{}).WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, &chi.Context{ 706 URLParams: chi.RouteParams{ 707 Keys: []string{"oid"}, 708 Values: []string{hexOID}, 709 }, 710 })) 711 712 tNextHandler := &tHTTPHandler{} 713 handlerFunc := orderIDCtx(tNextHandler) 714 handlerFunc.ServeHTTP(nil, req) 715 716 reqCtx := tNextHandler.req.Context() 717 untypedOID := reqCtx.Value(ctxOID) 718 if untypedOID == nil { 719 t.Fatalf("oid not embedded in request context") 720 } 721 oidStr, ok := untypedOID.(string) 722 if !ok { 723 t.Fatalf("string type assertion failed") 724 } 725 726 if oidStr != hexOID { 727 t.Fatalf("wrong value embedded in request context. wanted %s, got %s", hexOID, oidStr) 728 } 729 } 730 731 func TestGetOrderIDCtx(t *testing.T) { 732 oid := encode.RandomBytes(32) 733 hexOID := hex.EncodeToString(oid) 734 735 r := (&http.Request{}).WithContext(context.WithValue(context.Background(), ctxOID, hexOID)) 736 737 bytesOut, err := getOrderIDCtx(r) 738 if err != nil { 739 t.Fatalf("getOrderIDCtx error: %v", err) 740 } 741 if len(bytesOut) == 0 { 742 t.Fatalf("empty oid") 743 } 744 if !bytes.Equal(oid, bytesOut) { 745 t.Fatalf("wrong bytes. wanted %x, got %s", oid, bytesOut) 746 } 747 748 // Test some negative paths 749 for name, v := range map[string]any{ 750 "nil": nil, 751 "int": 5, 752 "wrong length": "abc", 753 "not hex": "zyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxwzyxw", 754 } { 755 r := (&http.Request{}).WithContext(context.WithValue(context.Background(), ctxOID, v)) 756 _, err := getOrderIDCtx(r) 757 if err == nil { 758 t.Fatalf("no error for %v", name) 759 } 760 } 761 } 762 763 func TestPasswordCache(t *testing.T) { 764 s, tCore, shutdown := newTServer(t, false) 765 defer shutdown() 766 767 password := encode.PassBytes("def") 768 authToken1 := s.authorize() 769 authToken2 := s.authorize() 770 771 key1, err := s.cacheAppPassword(password, authToken1) 772 if err != nil { 773 t.Fatalf("error caching password: %v", err) 774 } 775 776 key2, err := s.cacheAppPassword(password, authToken2) 777 if err != nil { 778 t.Fatalf("error caching password: %v", err) 779 } 780 781 retrievedPW, err := s.getCachedPassword(key1, authToken1) 782 if err != nil { 783 t.Fatalf("error getting password: %v", err) 784 } 785 if !bytes.Equal(password, retrievedPW) { 786 t.Fatalf("retrieved PW not same: %v - %v", password, retrievedPW) 787 } 788 789 retrievedPW, err = s.getCachedPassword(key2, authToken2) 790 if err != nil { 791 t.Fatalf("error getting password: %v", err) 792 } 793 if !bytes.Equal(password, retrievedPW) { 794 t.Fatalf("retrieved PW not same: %v - %v", password, retrievedPW) 795 } 796 797 // test new wallet request first without the cookies populated, then with 798 writer := new(TWriter) 799 reader := new(TReader) 800 body := &newWalletForm{ 801 Pass: encode.PassBytes(""), 802 } 803 want := `{"ok":false,"msg":"app pass cannot be empty"}` 804 tCore.notHas = true 805 ensureResponse(t, s.apiNewWallet, want, reader, writer, body, nil) 806 807 want = `{"ok":true}` 808 ensureResponse(t, s.apiNewWallet, want, reader, writer, body, map[string]string{ 809 authCK: authToken1, 810 pwKeyCK: hex.EncodeToString(key1), 811 }) 812 813 s.apiLogout(writer, nil) 814 815 if len(s.cachedPasswords) != 0 { 816 t.Fatal("logout should clear all cached passwords") 817 } 818 } 819 820 func TestAPI_ToggleRatesource(t *testing.T) { 821 s, tCore, shutdown := newTServer(t, false) 822 defer shutdown() 823 824 writer := new(TWriter) 825 reader := new(TReader) 826 827 type rateSourceForm struct { 828 Disable bool `json:"disable"` 829 Source string `json:"source"` 830 } 831 832 // Test enabling fiat rate sources. 833 enableTests := []struct { 834 name, source, want string 835 wantErr error 836 }{{ 837 name: "Invalid rate source", 838 source: "binance", 839 wantErr: errors.New("cannot enable unknown fiat rate source"), 840 want: `{"ok":false,"msg":"cannot enable unknown fiat rate source"}`, 841 }, { 842 name: "ok valid source", 843 source: "dcrdata", 844 want: `{"ok":true}`, 845 }, { 846 name: "ok already initialized", 847 source: "dcrdata", 848 want: `{"ok":true}`, 849 }} 850 851 for _, test := range enableTests { 852 body := &rateSourceForm{ 853 Disable: false, 854 Source: test.source, 855 } 856 tCore.rateSourceErr = test.wantErr 857 ensureResponse(t, s.apiToggleRateSource, test.want, reader, writer, body, nil) 858 } 859 860 // Test disabling fiat rate sources. 861 disableTests := []struct { 862 name, source, want string 863 wantErr error 864 }{{ 865 name: "Invalid rate source", 866 source: "binance", 867 wantErr: errors.New("cannot disable unknown fiat rate source"), 868 want: `{"ok":false,"msg":"cannot disable unknown fiat rate source"}`, 869 }, { 870 name: "ok valid source", 871 source: "Messari", 872 want: `{"ok":true}`, 873 }, { 874 name: "ok already disabled/not initialized", 875 source: "Messari", 876 want: `{"ok":true}`, 877 }} 878 879 for _, test := range disableTests { 880 body := &rateSourceForm{ 881 Disable: true, 882 Source: test.source, 883 } 884 tCore.rateSourceErr = test.wantErr 885 ensureResponse(t, s.apiToggleRateSource, test.want, reader, writer, body, nil) 886 } 887 } 888 889 func TestAPIValidateAddress(t *testing.T) { 890 s, tCore, shutdown := newTServer(t, false) 891 defer shutdown() 892 893 writer := new(TWriter) 894 reader := new(TReader) 895 testID := uint32(42) 896 897 body := &struct { 898 Addr string `json:"addr"` 899 AssetID *uint32 `json:"assetID"` 900 }{ 901 Addr: "addr", 902 AssetID: &testID, 903 } 904 905 want := `{"ok":true}` 906 tCore.validAddr = true 907 ensureResponse(t, s.apiValidateAddress, want, reader, writer, body, nil) 908 909 want = `{"ok":false}` 910 tCore.validAddr = false 911 ensureResponse(t, s.apiValidateAddress, want, reader, writer, body, nil) 912 } 913 914 func TestAPIEstimateSendTxFee(t *testing.T) { 915 s, tCore, shutdown := newTServer(t, false) 916 defer shutdown() 917 918 writer := new(TWriter) 919 reader := new(TReader) 920 testID := uint32(42) 921 922 body := &sendTxFeeForm{ 923 Addr: "addr", 924 Value: 1e8, 925 Subtract: false, 926 AssetID: &testID, 927 } 928 929 want := `{"ok":true,"txfee":10000,"validaddress":true}` 930 tCore.estFee = 10000 931 ensureResponse(t, s.apiEstimateSendTxFee, want, reader, writer, body, nil) 932 933 want = fmt.Sprintf(`{"ok":false,"msg":"%s"}`, tErr) 934 tCore.estFeeErr = tErr 935 ensureResponse(t, s.apiEstimateSendTxFee, want, reader, writer, body, nil) 936 } 937 938 func TestAPIToggleWalletStatus(t *testing.T) { 939 s, tCore, shutdown := newTServer(t, false) 940 defer shutdown() 941 writer := new(TWriter) 942 reader := new(TReader) 943 944 var body *walletStatusForm 945 ensure := func(want string) { 946 ensureResponse(t, s.apiToggleWalletStatus, want, reader, writer, body, nil) 947 } 948 949 body = &walletStatusForm{ 950 Disable: true, 951 AssetID: 12, 952 } 953 954 ensure(`{"ok":true}`) 955 if !tCore.walletDisabled { 956 t.Fatal("Expected wallet to be disabled") 957 } 958 959 tCore.walletStatusErr = errors.New("wallet not found") 960 ensure(`{"ok":false,"msg":"wallet not found"}`) 961 962 tCore.walletDisabled = false 963 body.Disable = false 964 tCore.walletStatusErr = nil 965 ensure(`{"ok":true}`) 966 if tCore.walletDisabled { 967 t.Fatal("Expected wallet to be enabled") 968 } 969 } 970 971 func TestAPIDeleteArchivedRecords(t *testing.T) { 972 s, tCore, shutdown := newTServer(t, false) 973 defer shutdown() 974 writer := new(TWriter) 975 reader := new(TReader) 976 977 var body *deleteRecordsForm 978 ensure := func(want string) { 979 ensureResponse(t, s.apiDeleteArchivedRecords, want, reader, writer, body, nil) 980 } 981 982 body = &deleteRecordsForm{ 983 OlderThanMs: time.Now().UnixMilli(), 984 } 985 986 tCore.deletedRecords = 23 987 ensure(`{"ok":true,"archivedRecordsDeleted":23,"archivedRecordsPath":"/path/to/records"}`) 988 989 tCore.deleteRecordsErr = tErr 990 ensure(`{"ok":false,"msg":"expected dummy error"}`) 991 } 992 993 func TestAPITrade(t *testing.T) { 994 testTrade(t, false) 995 } 996 997 func TestAPITradeAsync(t *testing.T) { 998 testTrade(t, true) 999 } 1000 1001 func testTrade(t *testing.T, async bool) { 1002 s, tCore, shutdown := newTServer(t, false) 1003 defer shutdown() 1004 writer := new(TWriter) 1005 reader := new(TReader) 1006 1007 body := &tradeForm{ 1008 Pass: []byte("random"), 1009 Order: new(core.TradeForm), 1010 } 1011 1012 ensure := func(want string) { 1013 if async { 1014 ensureResponse(t, s.apiTradeAsync, want, reader, writer, body, nil) 1015 } else { 1016 ensureResponse(t, s.apiTrade, want, reader, writer, body, nil) 1017 } 1018 } 1019 1020 tCore.tradeErr = tErr 1021 ensure(`{"ok":false,"msg":"expected dummy error"}`) 1022 } 1023 1024 func Test_prepareAddr(t *testing.T) { 1025 tests := []struct { 1026 name string 1027 addr net.Addr 1028 allowInCSP bool 1029 want string 1030 }{{ 1031 name: "OK: IPv4", 1032 addr: &net.TCPAddr{ 1033 IP: net.IPv4(127, 0, 0, 1), 1034 Port: 7232, 1035 }, 1036 want: "127.0.0.1:7232", 1037 allowInCSP: true, 1038 }, { 1039 name: "IPv6 loopback", 1040 addr: &net.TCPAddr{ 1041 IP: net.IPv6loopback, 1042 Port: 7232, 1043 }, 1044 want: "[::1]:7232", 1045 }, { 1046 name: "OK: IPv6 unspecified", 1047 addr: &net.TCPAddr{ 1048 IP: net.IPv6unspecified, 1049 Port: 7232, 1050 }, 1051 want: "127.0.0.1:7232", 1052 allowInCSP: true, 1053 }, { 1054 name: "OK: zero IPv4", 1055 addr: &net.TCPAddr{ 1056 IP: net.IPv4zero, 1057 Port: 7232, 1058 }, 1059 want: "127.0.0.1:7232", 1060 allowInCSP: true, 1061 }, { 1062 name: "others", 1063 addr: &net.UDPAddr{ 1064 IP: []byte{}, 1065 Port: 7232, 1066 }, 1067 want: ":7232", 1068 }} 1069 1070 for _, test := range tests { 1071 gotAddr, allowInCSP := prepareAddr(test.addr) 1072 if gotAddr != test.want { 1073 t.Fatalf("%s: address: got %s, want %s", test.name, gotAddr, test.want) 1074 } 1075 if allowInCSP != test.allowInCSP { 1076 t.Fatalf("%s: allow in CSP: got %v, want %v", test.name, allowInCSP, test.allowInCSP) 1077 } 1078 } 1079 }