decred.org/dcrdex@v1.0.3/client/webserver/webserver_test.go (about)

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