decred.org/dcrdex@v1.0.5/server/admin/server_test.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package admin 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/elliptic" 10 "crypto/sha256" 11 "encoding/hex" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "net/http" 16 "net/http/httptest" 17 "os" 18 "path/filepath" 19 "reflect" 20 "strings" 21 "sync" 22 "sync/atomic" 23 "testing" 24 "time" 25 26 "decred.org/dcrdex/dex" 27 "decred.org/dcrdex/dex/msgjson" 28 "decred.org/dcrdex/dex/order" 29 "decred.org/dcrdex/server/account" 30 "decred.org/dcrdex/server/asset" 31 "decred.org/dcrdex/server/auth" 32 "decred.org/dcrdex/server/db" 33 dexsrv "decred.org/dcrdex/server/dex" 34 "decred.org/dcrdex/server/market" 35 "github.com/decred/dcrd/certgen" 36 "github.com/decred/slog" 37 "github.com/go-chi/chi/v5" 38 ) 39 40 func init() { 41 log = slog.NewBackend(os.Stdout).Logger("TEST") 42 log.SetLevel(slog.LevelTrace) 43 } 44 45 type TMarket struct { 46 running bool 47 dur uint64 48 suspend *market.SuspendEpoch 49 startEpoch int64 50 activeEpoch int64 51 resumeEpoch int64 52 resumeTime time.Time 53 persist bool 54 } 55 56 type TCore struct { 57 markets map[string]*TMarket 58 accounts []*db.Account 59 accountsErr error 60 account *db.Account 61 accountErr error 62 penalizeErr error 63 unbanErr error 64 book []*order.LimitOrder 65 bookErr error 66 epochOrders []order.Order 67 epochOrdersErr error 68 marketMatches []*dexsrv.MatchData 69 marketMatchesErr error 70 dataEnabled uint32 71 } 72 73 func (c *TCore) ConfigMsg() json.RawMessage { return nil } 74 75 func (c *TCore) Suspend(tSusp time.Time, persistBooks bool) map[string]*market.SuspendEpoch { 76 return nil 77 } 78 func (c *TCore) ResumeMarket(name string, tRes time.Time) (startEpoch int64, startTime time.Time, err error) { 79 tMkt := c.markets[name] 80 if tMkt == nil { 81 err = fmt.Errorf("unknown market %s", name) 82 return 83 } 84 tMkt.resumeEpoch = 1 + tRes.UnixMilli()/int64(tMkt.dur) 85 tMkt.resumeTime = time.UnixMilli(tMkt.resumeEpoch * int64(tMkt.dur)) 86 return tMkt.resumeEpoch, tMkt.resumeTime, nil 87 } 88 func (c *TCore) SuspendMarket(name string, tSusp time.Time, persistBooks bool) (suspEpoch *market.SuspendEpoch, err error) { 89 tMkt := c.markets[name] 90 if tMkt == nil { 91 err = fmt.Errorf("unknown market %s", name) 92 return 93 } 94 tMkt.persist = persistBooks 95 tMkt.suspend.Idx = tSusp.UnixMilli() 96 tMkt.suspend.End = tSusp.Add(time.Millisecond) 97 return tMkt.suspend, nil 98 } 99 100 func (c *TCore) market(name string) *TMarket { 101 if c.markets == nil { 102 return nil 103 } 104 return c.markets[name] 105 } 106 107 func (c *TCore) MarketStatus(mktName string) *market.Status { 108 mkt := c.market(mktName) 109 if mkt == nil { 110 return nil 111 } 112 var suspendEpoch int64 113 if mkt.suspend != nil { 114 suspendEpoch = mkt.suspend.Idx 115 } 116 return &market.Status{ 117 Running: mkt.running, 118 EpochDuration: mkt.dur, 119 ActiveEpoch: mkt.activeEpoch, 120 StartEpoch: mkt.startEpoch, 121 SuspendEpoch: suspendEpoch, 122 PersistBook: mkt.persist, 123 } 124 } 125 126 func (c *TCore) Asset(id uint32) (*asset.BackedAsset, error) { return nil, fmt.Errorf("not tested") } 127 func (c *TCore) SetFeeRateScale(assetID uint32, scale float64) {} 128 func (c *TCore) ScaleFeeRate(assetID uint32, rate uint64) uint64 { return 1 } 129 130 func (c *TCore) BookOrders(_, _ uint32) ([]*order.LimitOrder, error) { 131 return c.book, c.bookErr 132 } 133 134 func (c *TCore) EpochOrders(_, _ uint32) ([]order.Order, error) { 135 return c.epochOrders, c.epochOrdersErr 136 } 137 138 func (c *TCore) MarketMatchesStreaming(base, quote uint32, includeInactive bool, N int64, f func(*dexsrv.MatchData) error) (int, error) { 139 if c.marketMatchesErr != nil { 140 return 0, c.marketMatchesErr 141 } 142 for _, mm := range c.marketMatches { 143 if err := f(mm); err != nil { 144 return 0, err 145 } 146 } 147 return len(c.marketMatches), nil 148 } 149 150 func (c *TCore) MarketStatuses() map[string]*market.Status { 151 mktStatuses := make(map[string]*market.Status, len(c.markets)) 152 for name, mkt := range c.markets { 153 var suspendEpoch int64 154 if mkt.suspend != nil { 155 suspendEpoch = mkt.suspend.Idx 156 } 157 mktStatuses[name] = &market.Status{ 158 Running: mkt.running, 159 EpochDuration: mkt.dur, 160 ActiveEpoch: mkt.activeEpoch, 161 StartEpoch: mkt.startEpoch, 162 SuspendEpoch: suspendEpoch, 163 PersistBook: mkt.persist, 164 } 165 } 166 return mktStatuses 167 } 168 169 func (c *TCore) MarketRunning(mktName string) (found, running bool) { 170 mkt := c.market(mktName) 171 if mkt == nil { 172 return 173 } 174 return true, mkt.running 175 } 176 177 func (c *TCore) EnableDataAPI(yes bool) { 178 var v uint32 179 if yes { 180 v = 1 181 } 182 atomic.StoreUint32(&c.dataEnabled, v) 183 } 184 185 type tResponseWriter struct { 186 b []byte 187 code int 188 } 189 190 func (w *tResponseWriter) Header() http.Header { 191 return make(http.Header) 192 } 193 func (w *tResponseWriter) Write(msg []byte) (int, error) { 194 w.b = msg 195 return len(msg), nil 196 } 197 func (w *tResponseWriter) WriteHeader(statusCode int) { 198 w.code = statusCode 199 } 200 201 func (c *TCore) Accounts() ([]*db.Account, error) { return c.accounts, c.accountsErr } 202 func (c *TCore) AccountInfo(_ account.AccountID) (*db.Account, error) { 203 return c.account, c.accountErr 204 } 205 func (c *TCore) UserMatchFails(aid account.AccountID, n int) ([]*auth.MatchFail, error) { 206 return nil, nil 207 } 208 func (c *TCore) Penalize(_ account.AccountID, _ account.Rule, _ string) error { 209 return c.penalizeErr 210 } 211 func (c *TCore) Unban(_ account.AccountID) error { 212 return c.unbanErr 213 } 214 func (c *TCore) ForgiveMatchFail(_ account.AccountID, _ order.MatchID) (bool, bool, error) { 215 return false, false, nil // TODO: tests 216 } 217 func (c *TCore) CreatePrepaidBonds(n int, strength uint32, durSecs int64) ([][]byte, error) { 218 return nil, nil 219 } 220 func (c *TCore) AccountMatchOutcomesN(user account.AccountID, n int) ([]*auth.MatchOutcome, error) { 221 return nil, nil 222 } 223 func (c *TCore) Notify(_ account.AccountID, _ *msgjson.Message) {} 224 func (c *TCore) NotifyAll(_ *msgjson.Message) {} 225 226 // genCertPair generates a key/cert pair to the paths provided. 227 func genCertPair(certFile, keyFile string) error { 228 log.Infof("Generating TLS certificates...") 229 230 org := "dcrdex autogenerated cert" 231 validUntil := time.Now().Add(10 * 365 * 24 * time.Hour) 232 cert, key, err := certgen.NewTLSCertPair(elliptic.P521(), org, 233 validUntil, nil) 234 if err != nil { 235 return err 236 } 237 238 // Write cert and key files. 239 if err = os.WriteFile(certFile, cert, 0644); err != nil { 240 return err 241 } 242 if err = os.WriteFile(keyFile, key, 0600); err != nil { 243 os.Remove(certFile) 244 return err 245 } 246 247 log.Infof("Done generating TLS certificates") 248 return nil 249 } 250 251 var tPort = 5555 252 253 // If start is true, the Server's Run goroutine is started, and the shutdown 254 // func must be called when finished with the Server. 255 func newTServer(t *testing.T, start bool, authSHA [32]byte) (*Server, func()) { 256 tmp := t.TempDir() 257 258 cert, key := filepath.Join(tmp, "tls.cert"), filepath.Join(tmp, "tls.key") 259 err := genCertPair(cert, key) 260 if err != nil { 261 t.Fatal(err) 262 } 263 264 s, err := NewServer(&SrvConfig{ 265 Core: new(TCore), 266 Addr: fmt.Sprintf("localhost:%d", tPort), 267 Cert: cert, 268 Key: key, 269 AuthSHA: authSHA, 270 }) 271 if err != nil { 272 t.Fatalf("error creating Server: %v", err) 273 } 274 if !start { 275 return s, func() {} 276 } 277 278 ctx, cancel := context.WithCancel(context.Background()) 279 var wg sync.WaitGroup 280 wg.Add(1) 281 go func() { 282 s.Run(ctx) 283 wg.Done() 284 }() 285 shutdown := func() { 286 cancel() 287 wg.Wait() 288 } 289 return s, shutdown 290 } 291 292 func TestPing(t *testing.T) { 293 w := httptest.NewRecorder() 294 apiPing(w, nil) 295 if w.Code != 200 { 296 t.Fatalf("apiPing returned code %d, expected 200", w.Code) 297 } 298 299 resp := w.Result() 300 ctHdr := resp.Header.Get("Content-Type") 301 wantCt := "application/json; charset=utf-8" 302 if ctHdr != wantCt { 303 t.Errorf("Content-Type incorrect. got %q, expected %q", ctHdr, wantCt) 304 } 305 306 // JSON strings are double quoted. Each value is terminated with a newline. 307 expectedBody := `"` + pongStr + `"` + "\n" 308 if w.Body == nil { 309 t.Fatalf("got empty body") 310 } 311 gotBody := w.Body.String() 312 if gotBody != expectedBody { 313 t.Errorf("apiPong response said %q, expected %q", gotBody, expectedBody) 314 } 315 } 316 317 func TestMarkets(t *testing.T) { 318 core := &TCore{ 319 markets: make(map[string]*TMarket), 320 } 321 srv := &Server{ 322 core: core, 323 } 324 325 mux := chi.NewRouter() 326 mux.Get("/markets", srv.apiMarkets) 327 328 // No markets. 329 w := httptest.NewRecorder() 330 r, _ := http.NewRequest(http.MethodGet, "https://localhost/markets", nil) 331 r.RemoteAddr = "localhost" 332 333 mux.ServeHTTP(w, r) 334 335 if w.Code != http.StatusOK { 336 t.Fatalf("apiMarkets returned code %d, expected %d", w.Code, http.StatusOK) 337 } 338 respBody := w.Body.String() 339 if respBody != "{}\n" { 340 t.Errorf("incorrect response body: %q", respBody) 341 } 342 343 // A market. 344 dur := uint64(1234) 345 idx := int64(12345) 346 tMkt := &TMarket{ 347 running: true, 348 dur: dur, 349 startEpoch: 12340, 350 activeEpoch: 12343, 351 } 352 core.markets["dcr_btc"] = tMkt 353 354 w = httptest.NewRecorder() 355 r, _ = http.NewRequest(http.MethodGet, "https://localhost/markets", nil) 356 r.RemoteAddr = "localhost" 357 358 mux.ServeHTTP(w, r) 359 360 if w.Code != http.StatusOK { 361 t.Fatalf("apiMarkets returned code %d, expected %d", w.Code, http.StatusOK) 362 } 363 364 exp := `{ 365 "dcr_btc": { 366 "running": true, 367 "epochlen": 1234, 368 "activeepoch": 12343, 369 "startepoch": 12340 370 } 371 } 372 ` 373 if exp != w.Body.String() { 374 t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp) 375 } 376 377 var mktStatuses map[string]*MarketStatus 378 err := json.Unmarshal(w.Body.Bytes(), &mktStatuses) 379 if err != nil { 380 t.Fatalf("Failed to unmarshal result: %v", err) 381 } 382 383 wantMktStatuses := map[string]*MarketStatus{ 384 "dcr_btc": { 385 Running: true, 386 EpochDuration: 1234, 387 ActiveEpoch: 12343, 388 StartEpoch: 12340, 389 }, 390 } 391 if len(wantMktStatuses) != len(mktStatuses) { 392 t.Fatalf("got %d market statuses, wanted %d", len(mktStatuses), len(wantMktStatuses)) 393 } 394 for name, stat := range mktStatuses { 395 wantStat := wantMktStatuses[name] 396 if wantStat == nil { 397 t.Fatalf("market %s not expected", name) 398 } 399 if !reflect.DeepEqual(wantStat, stat) { 400 log.Errorf("incorrect market status. got %v, expected %v", stat, wantStat) 401 } 402 } 403 404 // Set suspend data. 405 tMkt.suspend = &market.SuspendEpoch{Idx: 12345, End: time.UnixMilli(int64(dur) * idx)} 406 tMkt.persist = true 407 408 w = httptest.NewRecorder() 409 r, _ = http.NewRequest(http.MethodGet, "https://localhost/markets", nil) 410 r.RemoteAddr = "localhost" 411 412 mux.ServeHTTP(w, r) 413 414 if w.Code != http.StatusOK { 415 t.Fatalf("apiMarkets returned code %d, expected %d", w.Code, http.StatusOK) 416 } 417 418 exp = `{ 419 "dcr_btc": { 420 "running": true, 421 "epochlen": 1234, 422 "activeepoch": 12343, 423 "startepoch": 12340, 424 "finalepoch": 12345, 425 "persistbook": true 426 } 427 } 428 ` 429 if exp != w.Body.String() { 430 t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp) 431 } 432 433 mktStatuses = nil 434 err = json.Unmarshal(w.Body.Bytes(), &mktStatuses) 435 if err != nil { 436 t.Fatalf("Failed to unmarshal result: %v", err) 437 } 438 439 persist := true 440 wantMktStatuses = map[string]*MarketStatus{ 441 "dcr_btc": { 442 Running: true, 443 EpochDuration: 1234, 444 ActiveEpoch: 12343, 445 StartEpoch: 12340, 446 SuspendEpoch: 12345, 447 PersistBook: &persist, 448 }, 449 } 450 if len(wantMktStatuses) != len(mktStatuses) { 451 t.Fatalf("got %d market statuses, wanted %d", len(mktStatuses), len(wantMktStatuses)) 452 } 453 for name, stat := range mktStatuses { 454 wantStat := wantMktStatuses[name] 455 if wantStat == nil { 456 t.Fatalf("market %s not expected", name) 457 } 458 if !reflect.DeepEqual(wantStat, stat) { 459 log.Errorf("incorrect market status. got %v, expected %v", stat, wantStat) 460 } 461 } 462 } 463 464 func TestMarketInfo(t *testing.T) { 465 466 core := &TCore{ 467 markets: make(map[string]*TMarket), 468 } 469 srv := &Server{ 470 core: core, 471 } 472 473 mux := chi.NewRouter() 474 mux.Get("/market/{"+marketNameKey+"}", srv.apiMarketInfo) 475 476 // Request a non-existent market. 477 w := httptest.NewRecorder() 478 name := "dcr_btc" 479 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+name, nil) 480 r.RemoteAddr = "localhost" 481 482 mux.ServeHTTP(w, r) 483 484 if w.Code != http.StatusBadRequest { 485 t.Fatalf("apiMarketInfo returned code %d, expected %d", w.Code, http.StatusBadRequest) 486 } 487 respBody := w.Body.String() 488 if respBody != fmt.Sprintf("unknown market %q\n", name) { 489 t.Errorf("incorrect response body: %q", respBody) 490 } 491 492 tMkt := &TMarket{} 493 core.markets[name] = tMkt 494 495 // Not running market. 496 w = httptest.NewRecorder() 497 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name, nil) 498 r.RemoteAddr = "localhost" 499 500 mux.ServeHTTP(w, r) 501 502 if w.Code != http.StatusOK { 503 t.Fatalf("apiMarketInfo returned code %d, expected %d", w.Code, http.StatusOK) 504 } 505 mktStatus := new(MarketStatus) 506 err := json.Unmarshal(w.Body.Bytes(), &mktStatus) 507 if err != nil { 508 t.Fatalf("Failed to unmarshal result: %v", err) 509 } 510 if mktStatus.Name != name { 511 t.Errorf("incorrect market name %q, expected %q", mktStatus.Name, name) 512 } 513 if mktStatus.Running { 514 t.Errorf("market should not have been reported as running") 515 } 516 517 // Flip the market on. 518 core.markets[name].running = true 519 core.markets[name].suspend = &market.SuspendEpoch{Idx: 1324, End: time.Now()} 520 w = httptest.NewRecorder() 521 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name, nil) 522 r.RemoteAddr = "localhost" 523 524 mux.ServeHTTP(w, r) 525 526 if w.Code != http.StatusOK { 527 t.Fatalf("apiMarketInfo returned code %d, expected %d", w.Code, http.StatusOK) 528 } 529 mktStatus = new(MarketStatus) 530 err = json.Unmarshal(w.Body.Bytes(), &mktStatus) 531 if err != nil { 532 t.Fatalf("Failed to unmarshal result: %v", err) 533 } 534 if mktStatus.Name != name { 535 t.Errorf("incorrect market name %q, expected %q", mktStatus.Name, name) 536 } 537 if !mktStatus.Running { 538 t.Errorf("market should have been reported as running") 539 } 540 } 541 542 func TestMarketOrderBook(t *testing.T) { 543 core := new(TCore) 544 core.markets = make(map[string]*TMarket) 545 srv := &Server{ 546 core: core, 547 } 548 tMkt := &TMarket{ 549 dur: 1234, 550 startEpoch: 12340, 551 activeEpoch: 12343, 552 } 553 core.markets["dcr_btc"] = tMkt 554 mux := chi.NewRouter() 555 mux.Get("/market/{"+marketNameKey+"}/orderbook", srv.apiMarketOrderBook) 556 tests := []struct { 557 name, mkt string 558 running bool 559 book []*order.LimitOrder 560 bookErr error 561 wantCode int 562 }{{ 563 name: "ok", 564 mkt: "dcr_btc", 565 running: true, 566 book: []*order.LimitOrder{}, 567 wantCode: http.StatusOK, 568 }, { 569 name: "no market", 570 mkt: "btc_dcr", 571 wantCode: http.StatusBadRequest, 572 }, { 573 name: "core.OrderBook error", 574 mkt: "dcr_btc", 575 running: true, 576 bookErr: errors.New(""), 577 wantCode: http.StatusInternalServerError, 578 }} 579 for _, test := range tests { 580 core.book = test.book 581 core.bookErr = test.bookErr 582 tMkt.running = test.running 583 w := httptest.NewRecorder() 584 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+test.mkt+"/orderbook", nil) 585 r.RemoteAddr = "localhost" 586 587 mux.ServeHTTP(w, r) 588 589 if w.Code != test.wantCode { 590 t.Fatalf("%q: apiMarketOrderBook returned code %d, expected %d", test.name, w.Code, test.wantCode) 591 } 592 if w.Code == http.StatusOK { 593 res := new(*msgjson.BookOrderNote) 594 if err := json.Unmarshal(w.Body.Bytes(), res); err != nil { 595 t.Errorf("%q: unexpected response %v: %v", test.name, w.Body.String(), err) 596 } 597 } 598 } 599 } 600 601 func TestMarketEpochOrders(t *testing.T) { 602 core := new(TCore) 603 core.markets = make(map[string]*TMarket) 604 srv := &Server{ 605 core: core, 606 } 607 tMkt := &TMarket{ 608 dur: 1234, 609 startEpoch: 12340, 610 activeEpoch: 12343, 611 } 612 core.markets["dcr_btc"] = tMkt 613 mux := chi.NewRouter() 614 mux.Get("/market/{"+marketNameKey+"}/epochorders", srv.apiMarketEpochOrders) 615 tests := []struct { 616 name, mkt string 617 running bool 618 orders []order.Order 619 epochOrdersErr error 620 wantCode int 621 }{{ 622 name: "ok", 623 mkt: "dcr_btc", 624 running: true, 625 orders: []order.Order{}, 626 wantCode: http.StatusOK, 627 }, { 628 name: "no market", 629 mkt: "btc_dcr", 630 wantCode: http.StatusBadRequest, 631 }, { 632 name: "core.EpochOrders error", 633 mkt: "dcr_btc", 634 running: true, 635 epochOrdersErr: errors.New(""), 636 wantCode: http.StatusInternalServerError, 637 }} 638 for _, test := range tests { 639 core.epochOrders = test.orders 640 core.epochOrdersErr = test.epochOrdersErr 641 tMkt.running = test.running 642 w := httptest.NewRecorder() 643 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+test.mkt+"/epochorders", nil) 644 r.RemoteAddr = "localhost" 645 646 mux.ServeHTTP(w, r) 647 648 if w.Code != test.wantCode { 649 t.Fatalf("%q: apiMarketEpochOrders returned code %d, expected %d", test.name, w.Code, test.wantCode) 650 } 651 if w.Code == http.StatusOK { 652 res := new(*msgjson.BookOrderNote) 653 if err := json.Unmarshal(w.Body.Bytes(), res); err != nil { 654 t.Errorf("%q: unexpected response %v: %v", test.name, w.Body.String(), err) 655 } 656 } 657 } 658 } 659 660 func TestMarketMatches(t *testing.T) { 661 core := new(TCore) 662 core.markets = make(map[string]*TMarket) 663 srv := &Server{ 664 core: core, 665 } 666 tMkt := &TMarket{ 667 dur: 1234, 668 startEpoch: 12340, 669 activeEpoch: 12343, 670 } 671 core.markets["dcr_btc"] = tMkt 672 mux := chi.NewRouter() 673 mux.Get("/market/{"+marketNameKey+"}/matches", srv.apiMarketMatches) 674 tests := []struct { 675 name, mkt, token string 676 running, tokenValue bool 677 marketMatches []*dexsrv.MatchData 678 marketMatchesErr error 679 wantCode int 680 }{{ 681 name: "ok no token", 682 mkt: "dcr_btc", 683 running: true, 684 marketMatches: []*dexsrv.MatchData{}, 685 wantCode: http.StatusOK, 686 }, { 687 name: "ok with token", 688 mkt: "dcr_btc", 689 running: true, 690 token: "?" + includeInactiveKey + "=true", 691 marketMatches: []*dexsrv.MatchData{}, 692 wantCode: http.StatusOK, 693 }, { 694 name: "bad token", 695 mkt: "dcr_btc", 696 running: true, 697 token: "?" + includeInactiveKey + "=blue", 698 marketMatches: []*dexsrv.MatchData{}, 699 wantCode: http.StatusBadRequest, 700 }, { 701 name: "no market", 702 mkt: "btc_dcr", 703 wantCode: http.StatusBadRequest, 704 }, { 705 name: "core.MarketMatchesStreaming error", 706 mkt: "dcr_btc", 707 running: true, 708 marketMatchesErr: errors.New("boom"), 709 wantCode: http.StatusInternalServerError, 710 }} 711 for _, test := range tests { 712 core.marketMatches = test.marketMatches 713 core.marketMatchesErr = test.marketMatchesErr 714 tMkt.running = test.running 715 w := httptest.NewRecorder() 716 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+test.mkt+"/matches"+test.token, nil) 717 r.RemoteAddr = "localhost" 718 719 mux.ServeHTTP(w, r) 720 721 if w.Code != test.wantCode { 722 t.Fatalf("%q: apiMarketMatches returned code %d, expected %d", test.name, w.Code, test.wantCode) 723 } 724 if w.Code != http.StatusOK { 725 continue 726 } 727 dec := json.NewDecoder(w.Body) 728 729 var resi dexsrv.MatchData 730 mustDec := func() { 731 t.Helper() 732 if err := dec.Decode(&resi); err != nil { 733 t.Fatalf("%q: Failed to decode element: %v", test.name, err) 734 } 735 } 736 if len(test.marketMatches) > 0 { 737 mustDec() 738 } 739 for dec.More() { 740 mustDec() 741 } 742 } 743 } 744 745 func TestResume(t *testing.T) { 746 core := &TCore{ 747 markets: make(map[string]*TMarket), 748 } 749 srv := &Server{ 750 core: core, 751 } 752 753 mux := chi.NewRouter() 754 mux.Get("/market/{"+marketNameKey+"}/resume", srv.apiResume) 755 756 // Non-existent market 757 name := "dcr_btc" 758 w := httptest.NewRecorder() 759 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/resume", nil) 760 r.RemoteAddr = "localhost" 761 762 mux.ServeHTTP(w, r) 763 764 if w.Code != http.StatusBadRequest { 765 t.Fatalf("apiResume returned code %d, expected %d", w.Code, http.StatusBadRequest) 766 } 767 768 // With the market, but already running 769 tMkt := &TMarket{ 770 running: true, 771 dur: 6000, 772 } 773 core.markets[name] = tMkt 774 775 w = httptest.NewRecorder() 776 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/resume", nil) 777 r.RemoteAddr = "localhost" 778 779 mux.ServeHTTP(w, r) 780 781 if w.Code != http.StatusBadRequest { 782 t.Fatalf("apiResume returned code %d, expected %d", w.Code, http.StatusOK) 783 } 784 wantMsg := "market \"dcr_btc\" running\n" 785 if w.Body.String() != wantMsg { 786 t.Errorf("expected body %q, got %q", wantMsg, w.Body) 787 } 788 789 // Now stopped. 790 tMkt.running = false 791 w = httptest.NewRecorder() 792 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/resume", nil) 793 r.RemoteAddr = "localhost" 794 795 mux.ServeHTTP(w, r) 796 797 if w.Code != http.StatusOK { 798 t.Fatalf("apiResume returned code %d, expected %d", w.Code, http.StatusOK) 799 } 800 resRes := new(ResumeResult) 801 err := json.Unmarshal(w.Body.Bytes(), &resRes) 802 if err != nil { 803 t.Fatalf("Failed to unmarshal result: %v", err) 804 } 805 if resRes.Market != name { 806 t.Errorf("incorrect market name %q, expected %q", resRes.Market, name) 807 } 808 809 if resRes.StartTime.IsZero() { 810 t.Errorf("start time not set") 811 } 812 if resRes.StartEpoch == 0 { 813 t.Errorf("start epoch not sest") 814 } 815 816 // reset 817 tMkt.resumeEpoch = 0 818 tMkt.resumeTime = time.Time{} 819 820 // Time in past 821 w = httptest.NewRecorder() 822 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/resume?t=12", nil) 823 r.RemoteAddr = "localhost" 824 825 mux.ServeHTTP(w, r) 826 827 if w.Code != http.StatusBadRequest { 828 t.Fatalf("apiResume returned code %d, expected %d", w.Code, http.StatusOK) 829 } 830 resp := w.Body.String() 831 wantPrefix := "specified market resume time is in the past" 832 if !strings.HasPrefix(resp, wantPrefix) { 833 t.Errorf("Expected error message starting with %q, got %q", wantPrefix, resp) 834 } 835 836 // Bad suspend time (not a time) 837 w = httptest.NewRecorder() 838 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/resume?t=QWERT", nil) 839 r.RemoteAddr = "localhost" 840 841 mux.ServeHTTP(w, r) 842 843 if w.Code != http.StatusBadRequest { 844 t.Fatalf("apiResume returned code %d, expected %d", w.Code, http.StatusBadRequest) 845 } 846 resp = w.Body.String() 847 wantPrefix = "invalid resume time" 848 if !strings.HasPrefix(resp, wantPrefix) { 849 t.Errorf("Expected error message starting with %q, got %q", wantPrefix, resp) 850 } 851 } 852 853 func TestSuspend(t *testing.T) { 854 core := &TCore{ 855 markets: make(map[string]*TMarket), 856 } 857 srv := &Server{ 858 core: core, 859 } 860 861 mux := chi.NewRouter() 862 mux.Get("/market/{"+marketNameKey+"}/suspend", srv.apiSuspend) 863 864 // Non-existent market 865 name := "dcr_btc" 866 w := httptest.NewRecorder() 867 r, _ := http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend", nil) 868 r.RemoteAddr = "localhost" 869 870 mux.ServeHTTP(w, r) 871 872 if w.Code != http.StatusBadRequest { 873 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusBadRequest) 874 } 875 876 // With the market, but not running 877 tMkt := &TMarket{ 878 suspend: &market.SuspendEpoch{}, 879 } 880 core.markets[name] = tMkt 881 882 w = httptest.NewRecorder() 883 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend", nil) 884 r.RemoteAddr = "localhost" 885 886 mux.ServeHTTP(w, r) 887 888 if w.Code != http.StatusBadRequest { 889 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusOK) 890 } 891 wantMsg := "market \"dcr_btc\" not running\n" 892 if w.Body.String() != wantMsg { 893 t.Errorf("expected body %q, got %q", wantMsg, w.Body) 894 } 895 896 // Now running. 897 tMkt.running = true 898 w = httptest.NewRecorder() 899 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend", nil) 900 r.RemoteAddr = "localhost" 901 902 mux.ServeHTTP(w, r) 903 904 if w.Code != http.StatusOK { 905 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusOK) 906 } 907 suspRes := new(SuspendResult) 908 err := json.Unmarshal(w.Body.Bytes(), &suspRes) 909 if err != nil { 910 t.Fatalf("Failed to unmarshal result: %v", err) 911 } 912 if suspRes.Market != name { 913 t.Errorf("incorrect market name %q, expected %q", suspRes.Market, name) 914 } 915 916 var zeroTime time.Time 917 wantIdx := zeroTime.UnixMilli() 918 if suspRes.FinalEpoch != wantIdx { 919 t.Errorf("incorrect final epoch index. got %d, expected %d", 920 suspRes.FinalEpoch, tMkt.suspend.Idx) 921 } 922 923 wantFinal := zeroTime.Add(time.Millisecond) 924 if !suspRes.SuspendTime.Equal(wantFinal) { 925 t.Errorf("incorrect suspend time. got %v, expected %v", 926 suspRes.SuspendTime, tMkt.suspend.End) 927 } 928 929 // Specify a time in the past. 930 w = httptest.NewRecorder() 931 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend?t=12", nil) 932 r.RemoteAddr = "localhost" 933 934 mux.ServeHTTP(w, r) 935 936 if w.Code != http.StatusBadRequest { 937 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusBadRequest) 938 } 939 resp := w.Body.String() 940 wantPrefix := "specified market suspend time is in the past" 941 if !strings.HasPrefix(resp, wantPrefix) { 942 t.Errorf("Expected error message starting with %q, got %q", wantPrefix, resp) 943 } 944 945 // Bad suspend time (not a time) 946 w = httptest.NewRecorder() 947 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend?t=QWERT", nil) 948 r.RemoteAddr = "localhost" 949 950 mux.ServeHTTP(w, r) 951 952 if w.Code != http.StatusBadRequest { 953 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusBadRequest) 954 } 955 resp = w.Body.String() 956 wantPrefix = "invalid suspend time" 957 if !strings.HasPrefix(resp, wantPrefix) { 958 t.Errorf("Expected error message starting with %q, got %q", wantPrefix, resp) 959 } 960 961 // Good suspend time, one minute in the future 962 w = httptest.NewRecorder() 963 tMsFuture := time.Now().Add(time.Minute).UnixMilli() 964 r, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost/market/%v/suspend?t=%d", name, tMsFuture), nil) 965 r.RemoteAddr = "localhost" 966 967 mux.ServeHTTP(w, r) 968 969 if w.Code != http.StatusOK { 970 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusOK) 971 } 972 suspRes = new(SuspendResult) 973 err = json.Unmarshal(w.Body.Bytes(), &suspRes) 974 if err != nil { 975 t.Fatalf("Failed to unmarshal result: %v", err) 976 } 977 978 if suspRes.FinalEpoch != tMsFuture { 979 t.Errorf("incorrect final epoch index. got %d, expected %d", 980 suspRes.FinalEpoch, tMsFuture) 981 } 982 983 wantFinal = time.UnixMilli(tMsFuture + 1) 984 if !suspRes.SuspendTime.Equal(wantFinal) { 985 t.Errorf("incorrect suspend time. got %v, expected %v", 986 suspRes.SuspendTime, wantFinal) 987 } 988 989 if !tMkt.persist { 990 t.Errorf("market persist was false") 991 } 992 993 // persist=true (OK) 994 w = httptest.NewRecorder() 995 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend?persist=true", nil) 996 r.RemoteAddr = "localhost" 997 998 mux.ServeHTTP(w, r) 999 1000 if w.Code != http.StatusOK { 1001 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusOK) 1002 } 1003 1004 if !tMkt.persist { 1005 t.Errorf("market persist was false") 1006 } 1007 1008 // persist=0 (OK) 1009 w = httptest.NewRecorder() 1010 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend?persist=0", nil) 1011 r.RemoteAddr = "localhost" 1012 1013 mux.ServeHTTP(w, r) 1014 1015 if w.Code != http.StatusOK { 1016 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusOK) 1017 } 1018 1019 if tMkt.persist { 1020 t.Errorf("market persist was true") 1021 } 1022 1023 // invalid persist 1024 w = httptest.NewRecorder() 1025 r, _ = http.NewRequest(http.MethodGet, "https://localhost/market/"+name+"/suspend?persist=blahblahblah", nil) 1026 r.RemoteAddr = "localhost" 1027 1028 mux.ServeHTTP(w, r) 1029 1030 if w.Code != http.StatusBadRequest { 1031 t.Fatalf("apiSuspend returned code %d, expected %d", w.Code, http.StatusBadRequest) 1032 } 1033 resp = w.Body.String() 1034 wantPrefix = "invalid persist book boolean" 1035 if !strings.HasPrefix(resp, wantPrefix) { 1036 t.Errorf("Expected error message starting with %q, got %q", wantPrefix, resp) 1037 } 1038 } 1039 1040 func TestAuthMiddleware(t *testing.T) { 1041 pass := "password123" 1042 authSHA := sha256.Sum256([]byte(pass)) 1043 s, _ := newTServer(t, false, authSHA) 1044 am := s.authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1045 w.WriteHeader(http.StatusOK) 1046 })) 1047 1048 r, _ := http.NewRequest(http.MethodGet, "", nil) 1049 r.RemoteAddr = "localhost" 1050 1051 wantAuthError := func(name string, want bool) { 1052 w := &tResponseWriter{} 1053 am.ServeHTTP(w, r) 1054 if w.code != http.StatusUnauthorized && w.code != http.StatusOK { 1055 t.Fatalf("unexpected HTTP error %d for test \"%s\"", w.code, name) 1056 } 1057 switch want { 1058 case true: 1059 if w.code != http.StatusUnauthorized { 1060 t.Fatalf("Expected unauthorized HTTP error for test \"%s\"", name) 1061 } 1062 case false: 1063 if w.code != http.StatusOK { 1064 t.Fatalf("Expected OK HTTP status for test \"%s\"", name) 1065 } 1066 } 1067 } 1068 1069 tests := []struct { 1070 name, user, pass string 1071 wantErr bool 1072 }{{ 1073 name: "user and correct password", 1074 user: "user", 1075 pass: pass, 1076 }, { 1077 name: "only correct password", 1078 pass: pass, 1079 }, { 1080 name: "only user", 1081 user: "user", 1082 wantErr: true, 1083 }, { 1084 name: "no user or password", 1085 wantErr: true, 1086 }, { 1087 name: "wrong password", 1088 user: "user", 1089 pass: pass[1:], 1090 wantErr: true, 1091 }} 1092 for _, test := range tests { 1093 r.SetBasicAuth(test.user, test.pass) 1094 wantAuthError(test.name, test.wantErr) 1095 } 1096 } 1097 1098 func TestAccountInfo(t *testing.T) { 1099 core := new(TCore) 1100 srv := &Server{ 1101 core: core, 1102 } 1103 1104 acctIDStr := "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc" 1105 1106 mux := chi.NewRouter() 1107 mux.Route("/account/{"+accountIDKey+"}", func(rm chi.Router) { 1108 rm.Get("/", srv.apiAccountInfo) 1109 }) 1110 1111 // No account. 1112 w := httptest.NewRecorder() 1113 r, _ := http.NewRequest(http.MethodGet, "https://localhost/account/"+acctIDStr, nil) 1114 r.RemoteAddr = "localhost" 1115 1116 mux.ServeHTTP(w, r) 1117 1118 if w.Code != http.StatusOK { 1119 t.Fatalf("apiAccounts returned code %d, expected %d", w.Code, http.StatusOK) 1120 } 1121 respBody := w.Body.String() 1122 if respBody != "null\n" { 1123 t.Errorf("incorrect response body: %q", respBody) 1124 } 1125 1126 accountIDSlice, err := hex.DecodeString(acctIDStr) 1127 if err != nil { 1128 t.Fatal(err) 1129 } 1130 var accountID account.AccountID 1131 copy(accountID[:], accountIDSlice) 1132 pubkey, err := hex.DecodeString("0204988a498d5d19514b217e872b4dbd1cf071d365c4879e64ed5919881c97eb19") 1133 if err != nil { 1134 t.Fatal(err) 1135 } 1136 1137 // An account. 1138 core.account = &db.Account{ 1139 AccountID: accountID, 1140 Pubkey: dex.Bytes(pubkey), 1141 } 1142 1143 w = httptest.NewRecorder() 1144 r, _ = http.NewRequest(http.MethodGet, "https://localhost/account/"+acctIDStr, nil) 1145 r.RemoteAddr = "localhost" 1146 1147 mux.ServeHTTP(w, r) 1148 1149 if w.Code != http.StatusOK { 1150 t.Fatalf("apiAccount returned code %d, expected %d", w.Code, http.StatusOK) 1151 } 1152 1153 exp := `{ 1154 "accountid": "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc", 1155 "pubkey": "0204988a498d5d19514b217e872b4dbd1cf071d365c4879e64ed5919881c97eb19" 1156 } 1157 ` 1158 if exp != w.Body.String() { 1159 t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp) 1160 } 1161 1162 // ok, upper case account id 1163 w = httptest.NewRecorder() 1164 r, _ = http.NewRequest(http.MethodGet, "https://localhost/account/"+strings.ToUpper(acctIDStr), nil) 1165 r.RemoteAddr = "localhost" 1166 1167 mux.ServeHTTP(w, r) 1168 1169 if w.Code != http.StatusOK { 1170 t.Fatalf("apiAccount returned code %d, expected %d", w.Code, http.StatusOK) 1171 } 1172 if exp != w.Body.String() { 1173 t.Errorf("unexpected response %q, wanted %q", w.Body.String(), exp) 1174 } 1175 1176 // acct id is not hex 1177 w = httptest.NewRecorder() 1178 r, _ = http.NewRequest(http.MethodGet, "https://localhost/account/nothex", nil) 1179 r.RemoteAddr = "localhost" 1180 1181 mux.ServeHTTP(w, r) 1182 1183 if w.Code != http.StatusBadRequest { 1184 t.Fatalf("apiAccount returned code %d, expected %d", w.Code, http.StatusBadRequest) 1185 } 1186 1187 // acct id wrong length 1188 w = httptest.NewRecorder() 1189 r, _ = http.NewRequest(http.MethodGet, "https://localhost/account/"+acctIDStr[2:], nil) 1190 r.RemoteAddr = "localhost" 1191 1192 mux.ServeHTTP(w, r) 1193 1194 if w.Code != http.StatusBadRequest { 1195 t.Fatalf("apiAccount returned code %d, expected %d", w.Code, http.StatusBadRequest) 1196 } 1197 1198 // core.Account error 1199 core.accountErr = errors.New("error") 1200 1201 w = httptest.NewRecorder() 1202 r, _ = http.NewRequest(http.MethodGet, "https://localhost/account/"+acctIDStr, nil) 1203 r.RemoteAddr = "localhost" 1204 1205 mux.ServeHTTP(w, r) 1206 1207 if w.Code != http.StatusInternalServerError { 1208 t.Fatalf("apiAccount returned code %d, expected %d", w.Code, http.StatusInternalServerError) 1209 } 1210 } 1211 1212 func TestAPITimeMarshalJSON(t *testing.T) { 1213 now := APITime{time.Now()} 1214 b, err := json.Marshal(now) 1215 if err != nil { 1216 t.Fatalf("unable to marshal api time: %v", err) 1217 } 1218 var res APITime 1219 if err := json.Unmarshal(b, &res); err != nil { 1220 t.Fatalf("unable to unmarshal api time: %v", err) 1221 } 1222 if !res.Equal(now.Time) { 1223 t.Fatal("unmarshalled time not equal") 1224 } 1225 } 1226 1227 func TestNotify(t *testing.T) { 1228 core := new(TCore) 1229 srv := &Server{ 1230 core: core, 1231 } 1232 mux := chi.NewRouter() 1233 mux.Route("/account/{"+accountIDKey+"}/notify", func(rm chi.Router) { 1234 rm.Post("/", srv.apiNotify) 1235 }) 1236 acctIDStr := "0a9912205b2cbab0c25c2de30bda9074de0ae23b065489a99199bad763f102cc" 1237 msgStr := "Hello world.\nAll your base are belong to us." 1238 tests := []struct { 1239 name, txt, acctID string 1240 wantCode int 1241 }{{ 1242 name: "ok", 1243 acctID: acctIDStr, 1244 txt: msgStr, 1245 wantCode: http.StatusOK, 1246 }, { 1247 name: "ok at max size", 1248 acctID: acctIDStr, 1249 txt: string(make([]byte, maxUInt16)), 1250 wantCode: http.StatusOK, 1251 }, { 1252 name: "message too long", 1253 acctID: acctIDStr, 1254 txt: string(make([]byte, maxUInt16+1)), 1255 wantCode: http.StatusBadRequest, 1256 }, { 1257 name: "account id not hex", 1258 acctID: "nothex", 1259 txt: msgStr, 1260 wantCode: http.StatusBadRequest, 1261 }, { 1262 name: "account id wrong length", 1263 acctID: acctIDStr[2:], 1264 txt: msgStr, 1265 wantCode: http.StatusBadRequest, 1266 }, { 1267 name: "no message", 1268 acctID: acctIDStr, 1269 wantCode: http.StatusBadRequest, 1270 }} 1271 for _, test := range tests { 1272 w := httptest.NewRecorder() 1273 br := bytes.NewReader([]byte(test.txt)) 1274 r, _ := http.NewRequest("POST", "https://localhost/account/"+test.acctID+"/notify", br) 1275 r.RemoteAddr = "localhost" 1276 1277 mux.ServeHTTP(w, r) 1278 1279 if w.Code != test.wantCode { 1280 t.Fatalf("%q: apiNotify returned code %d, expected %d", test.name, w.Code, test.wantCode) 1281 } 1282 } 1283 } 1284 1285 func TestNotifyAll(t *testing.T) { 1286 core := new(TCore) 1287 srv := &Server{ 1288 core: core, 1289 } 1290 mux := chi.NewRouter() 1291 mux.Route("/notifyall", func(rm chi.Router) { 1292 rm.Post("/", srv.apiNotifyAll) 1293 }) 1294 tests := []struct { 1295 name, txt string 1296 wantCode int 1297 }{{ 1298 name: "ok", 1299 txt: "Hello world.\nAll your base are belong to us.", 1300 wantCode: http.StatusOK, 1301 }, { 1302 name: "ok at max size", 1303 txt: string(make([]byte, maxUInt16)), 1304 wantCode: http.StatusOK, 1305 }, { 1306 name: "message too long", 1307 txt: string(make([]byte, maxUInt16+1)), 1308 wantCode: http.StatusBadRequest, 1309 }, { 1310 name: "no message", 1311 wantCode: http.StatusBadRequest, 1312 }} 1313 for _, test := range tests { 1314 w := httptest.NewRecorder() 1315 br := bytes.NewReader([]byte(test.txt)) 1316 r, _ := http.NewRequest("POST", "https://localhost/notifyall", br) 1317 r.RemoteAddr = "localhost" 1318 1319 mux.ServeHTTP(w, r) 1320 1321 if w.Code != test.wantCode { 1322 t.Fatalf("%q: apiNotifyAll returned code %d, expected %d", test.name, w.Code, test.wantCode) 1323 } 1324 } 1325 } 1326 1327 func TestEnableDataAPI(t *testing.T) { 1328 core := new(TCore) 1329 srv := &Server{ 1330 core: core, 1331 } 1332 mux := chi.NewRouter() 1333 mux.Route("/enabledataapi/{yes}", func(rm chi.Router) { 1334 rm.Post("/", srv.apiEnableDataAPI) 1335 }) 1336 1337 tests := []struct { 1338 name, yes string 1339 wantCode int 1340 wantEnabled uint32 1341 }{{ 1342 name: "ok 1", 1343 yes: "1", 1344 wantCode: http.StatusOK, 1345 wantEnabled: 1, 1346 }, { 1347 name: "ok true", 1348 yes: "true", 1349 wantCode: http.StatusOK, 1350 wantEnabled: 1, 1351 }, { 1352 name: "message too long", 1353 yes: "mabye", 1354 wantCode: http.StatusBadRequest, 1355 wantEnabled: 0, 1356 }} 1357 for _, test := range tests { 1358 w := httptest.NewRecorder() 1359 br := bytes.NewReader([]byte{}) 1360 r, _ := http.NewRequest("POST", "https://localhost/enabledataapi/"+test.yes, br) 1361 r.RemoteAddr = "localhost" 1362 1363 mux.ServeHTTP(w, r) 1364 1365 if w.Code != test.wantCode { 1366 t.Fatalf("%q: apiEnableDataAPI returned code %d, expected %d", test.name, w.Code, test.wantCode) 1367 } 1368 1369 if test.wantEnabled != atomic.LoadUint32(&core.dataEnabled) { 1370 t.Fatalf("%q: apiEnableDataAPI expected dataEnabled = %d, got %d", test.name, test.wantEnabled, atomic.LoadUint32(&core.dataEnabled)) 1371 } 1372 1373 atomic.StoreUint32(&core.dataEnabled, 0) 1374 } 1375 1376 }