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