decred.org/dcrdex@v1.0.5/client/websocket/websocket_test.go (about)

     1  //go:build !live
     2  
     3  package websocket
     4  
     5  import (
     6  	"context"
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"decred.org/dcrdex/client/core"
    15  	"decred.org/dcrdex/client/orderbook"
    16  	"decred.org/dcrdex/dex"
    17  	"decred.org/dcrdex/dex/msgjson"
    18  )
    19  
    20  var (
    21  	tCtx  context.Context
    22  	unbip = dex.BipIDSymbol
    23  )
    24  
    25  type TCore struct {
    26  	syncFeed   core.BookFeed
    27  	syncErr    error
    28  	notHas     bool
    29  	notRunning bool
    30  	notOpen    bool
    31  }
    32  
    33  func (c *TCore) SyncBook(dex string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) {
    34  	return nil, c.syncFeed, c.syncErr
    35  }
    36  func (c *TCore) WalletState(assetID uint32) *core.WalletState {
    37  	if c.notHas {
    38  		return nil
    39  	}
    40  	return &core.WalletState{
    41  		Symbol:  unbip(assetID),
    42  		AssetID: assetID,
    43  		Open:    !c.notOpen,
    44  		Running: !c.notRunning,
    45  	}
    46  }
    47  func (c *TCore) AckNotes(ids []dex.Bytes) {}
    48  
    49  type TConn struct {
    50  	msg       []byte
    51  	reads     [][]byte      // data for ReadMessage
    52  	respReady chan []byte   // signal from WriteMessage
    53  	close     chan struct{} // Close tells ReadMessage to return with error
    54  }
    55  
    56  var readTimeout = 10 * time.Second // ReadMessage must not return constantly with nothing
    57  
    58  func (c *TConn) ReadMessage() (int, []byte, error) {
    59  	if len(c.reads) > 0 {
    60  		var read []byte
    61  		// pop front
    62  		read, c.reads = c.reads[0], c.reads[1:]
    63  		return len(read), read, nil
    64  	}
    65  
    66  	select {
    67  	case <-c.close: // receive from nil channel blocks
    68  		return 0, nil, fmt.Errorf("closed")
    69  	case <-time.After(readTimeout):
    70  		return 0, nil, fmt.Errorf("read timeout")
    71  	}
    72  }
    73  
    74  func (c *TConn) addRead(read []byte) {
    75  	// push back
    76  	c.reads = append(c.reads, read)
    77  }
    78  
    79  func (c *TConn) SetWriteDeadline(t time.Time) error {
    80  	return nil
    81  }
    82  
    83  func (c *TConn) WriteControl(messageType int, data []byte, deadline time.Time) error {
    84  	return nil
    85  }
    86  
    87  func (c *TConn) SetReadDeadline(t time.Time) error {
    88  	return nil
    89  }
    90  
    91  func (c *TConn) SetReadLimit(int64) {}
    92  
    93  func (c *TConn) WriteMessage(_ int, msg []byte) error {
    94  	c.msg = msg
    95  	select {
    96  	case c.respReady <- msg:
    97  	default:
    98  	}
    99  	return nil
   100  }
   101  
   102  func (c *TConn) Close() error {
   103  	// If the test has a non-nil close channel, signal close.
   104  	select {
   105  	case c.close <- struct{}{}:
   106  	default:
   107  	}
   108  	return nil
   109  }
   110  
   111  type tLink struct {
   112  	cl   *wsClient
   113  	conn *TConn
   114  }
   115  
   116  func newLink() *tLink {
   117  	conn := &TConn{
   118  		respReady: make(chan []byte, 1),
   119  		close:     make(chan struct{}, 1),
   120  	}
   121  	ipk := dex.IPKey{16, 16, 120, 120 /* ipv6 1010:7878:: */}
   122  	cl := newWSClient(ipk.String(), conn, func(*msgjson.Message) *msgjson.Error { return nil }, dex.StdOutLogger("ws_TEST", dex.LevelTrace))
   123  	return &tLink{
   124  		cl:   cl,
   125  		conn: conn,
   126  	}
   127  }
   128  
   129  func newTServer() (*Server, *TCore) {
   130  	c := &TCore{}
   131  	return New(c, dex.StdOutLogger("TEST", dex.LevelTrace)), c
   132  }
   133  
   134  type tBookFeed struct{}
   135  
   136  func (*tBookFeed) Next() <-chan *core.BookUpdate {
   137  	return make(chan *core.BookUpdate, 1)
   138  }
   139  func (*tBookFeed) Close() {}
   140  func (*tBookFeed) Candles(dur string) error {
   141  	return nil
   142  }
   143  
   144  func TestMain(m *testing.M) {
   145  	var shutdown func()
   146  	tCtx, shutdown = context.WithCancel(context.Background())
   147  	doIt := func() int {
   148  		// Not counted as coverage, must test Archiver constructor explicitly.
   149  		defer shutdown()
   150  		return m.Run()
   151  	}
   152  	os.Exit(doIt())
   153  }
   154  
   155  func TestLoadMarket(t *testing.T) {
   156  	srv, tCore := newTServer()
   157  	// NOTE that Server is not running. We are just using the handleMessage
   158  	// method to route to the proper handler with a configured logger and Core.
   159  
   160  	link := newLink()
   161  	linkWg, err := link.cl.Connect(tCtx)
   162  	if err != nil {
   163  		t.Fatalf("WSLink Start: %v", err)
   164  	}
   165  
   166  	// This test is not running WebServer or calling handleWS/websocketHandler,
   167  	// so manually stop the marketSyncer started by wsLoadMarket and the WSLink
   168  	// before returning from this test.
   169  	defer func() {
   170  		link.cl.shutDownFeed()
   171  		link.cl.Disconnect()
   172  		linkWg.Wait()
   173  	}()
   174  
   175  	params := &marketLoad{
   176  		Host:  "abc",
   177  		Base:  uint32(1),
   178  		Quote: uint32(2),
   179  	}
   180  
   181  	subscription, _ := msgjson.NewRequest(1, "loadmarket", params)
   182  
   183  	ensureGood := func() {
   184  		t.Helper()
   185  		// Create a new feed for every request because a Close()d feed cannot be
   186  		// reused.
   187  		tCore.syncFeed = &tBookFeed{}
   188  		msgErr := srv.handleMessage(link.cl, subscription)
   189  		if msgErr != nil {
   190  			t.Fatalf("'loadmarket' error: %d: %s", msgErr.Code, msgErr.Message)
   191  		}
   192  		if link.cl.feed.loop == nil {
   193  			t.Fatalf("nil book feed waiter after 'loadmarket'")
   194  		}
   195  	}
   196  
   197  	// Initial success.
   198  	ensureGood()
   199  
   200  	// Unsubscribe.
   201  	unsub, _ := msgjson.NewRequest(2, "unmarket", nil)
   202  	msgErr := srv.handleMessage(link.cl, unsub)
   203  	if msgErr != nil {
   204  		t.Fatalf("'unmarket' error: %d: %s", msgErr.Code, msgErr.Message)
   205  	}
   206  
   207  	if link.cl.feed != nil {
   208  		t.Fatalf("non-nil book feed waiter after 'unmarket'")
   209  	}
   210  
   211  	// Make sure a sync error propagates.
   212  	tCore.syncErr = fmt.Errorf("expected dummy error")
   213  	msgErr = srv.handleMessage(link.cl, subscription)
   214  	if msgErr == nil {
   215  		t.Fatalf("no handleMessage error from Sync error")
   216  	}
   217  	tCore.syncErr = nil
   218  
   219  	// Success again.
   220  	ensureGood()
   221  }
   222  
   223  func TestHandleMessage(t *testing.T) {
   224  	link := newLink()
   225  	srv, _ := newTServer()
   226  
   227  	// NOTE: link is not started because the handlers in this test do not
   228  	// actually use it.
   229  
   230  	var msg *msgjson.Message
   231  
   232  	ensureErr := func(name string, wantCode int) {
   233  		got := srv.handleMessage(link.cl, msg)
   234  		if got == nil {
   235  			t.Fatalf("%s: no error", name)
   236  		}
   237  		if wantCode != got.Code {
   238  			t.Fatalf("%s, wanted %d, got %d", name, wantCode, got.Code)
   239  		}
   240  	}
   241  
   242  	// Send a response, which is unsupported on the web server.
   243  	msg, _ = msgjson.NewResponse(1, nil, nil)
   244  	ensureErr("bad route", msgjson.UnknownMessageType)
   245  
   246  	// Unknown route.
   247  	msg, _ = msgjson.NewRequest(1, "123", nil)
   248  	ensureErr("bad route", msgjson.UnknownMessageType)
   249  
   250  	// Set the route correctly.
   251  	wsHandlers["123"] = func(*Server, *wsClient, *msgjson.Message) *msgjson.Error {
   252  		return nil
   253  	}
   254  
   255  	rpcErr := srv.handleMessage(link.cl, msg)
   256  	if rpcErr != nil {
   257  		t.Fatalf("error for good message: %d: %s", rpcErr.Code, rpcErr.Message)
   258  	}
   259  }
   260  
   261  func TestClientMap(t *testing.T) {
   262  	srv, _ := newTServer()
   263  	resp := make(chan []byte, 1)
   264  	conn := &TConn{
   265  		respReady: resp,
   266  		close:     make(chan struct{}, 1),
   267  	}
   268  	// msg.ID == 0 gets an error response, which can be discarded.
   269  	read, _ := json.Marshal(msgjson.Message{ID: 0})
   270  	conn.addRead(read)
   271  
   272  	// Create the context that the http request handler would receive.
   273  	ctx, shutdown := context.WithCancel(context.Background())
   274  	var wg sync.WaitGroup
   275  	wg.Add(1)
   276  	go func() {
   277  		ipk := dex.IPKey{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255 /* ipv4 */, 127, 0, 0, 1}
   278  		srv.connect(ctx, conn, ipk.String())
   279  		wg.Done()
   280  	}()
   281  
   282  	// When a response to our dummy message is received, the client should be in
   283  	// RPCServer's client map.
   284  	<-resp
   285  
   286  	var cl *wsClient
   287  	srv.clientsMtx.Lock()
   288  	i := len(srv.clients)
   289  	if i != 1 {
   290  		t.Fatalf("expected 1 client in server map, found %d", i)
   291  	}
   292  	for _, c := range srv.clients {
   293  		cl = c
   294  		break
   295  	}
   296  	srv.clientsMtx.Unlock()
   297  
   298  	// Close the server and make sure the connection is closed.
   299  	shutdown()
   300  	wg.Wait() // websocketHandler since it's using log
   301  	if !cl.Off() {
   302  		t.Fatal("connection not closed on server shutdown")
   303  	}
   304  }