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

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