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  }