client_test.go raw

   1  //go:build !js
   2  
   3  package ws
   4  
   5  import (
   6  	"context"
   7  	"encoding/json"
   8  	"io"
   9  	"net/http"
  10  	"net/http/httptest"
  11  	"sync"
  12  	"testing"
  13  	"time"
  14  
  15  	"next.orly.dev/pkg/nostr/encoders/event"
  16  	"next.orly.dev/pkg/nostr/encoders/filter"
  17  	"next.orly.dev/pkg/nostr/encoders/hex"
  18  	"next.orly.dev/pkg/nostr/encoders/kind"
  19  	"next.orly.dev/pkg/nostr/encoders/tag"
  20  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  21  	"next.orly.dev/pkg/nostr/utils"
  22  	"next.orly.dev/pkg/nostr/utils/normalize"
  23  	"github.com/stretchr/testify/assert"
  24  	"github.com/stretchr/testify/require"
  25  	"golang.org/x/net/websocket"
  26  	"next.orly.dev/pkg/lol/chk"
  27  )
  28  
  29  func TestPublish(t *testing.T) {
  30  	// test note to be sent over websocket
  31  	priv, pub := makeKeyPair(t)
  32  	textNote := &event.E{
  33  		Kind:      kind.TextNote.K,
  34  		Content:   []byte("hello"),
  35  		CreatedAt: 1672068534, // random fixed timestamp
  36  		Tags:      tag.NewS(tag.NewFromAny("foo", "bar")),
  37  		Pubkey:    pub,
  38  	}
  39  	sign := p8k.MustNew()
  40  	var err error
  41  	if err = sign.InitSec(priv); chk.E(err) {
  42  	}
  43  	err = textNote.Sign(sign)
  44  	assert.NoError(t, err)
  45  
  46  	// fake relay server
  47  	var mu sync.Mutex // guards published to satisfy go test -race
  48  	var published bool
  49  	ws := newWebsocketServer(
  50  		func(conn *websocket.Conn) {
  51  			mu.Lock()
  52  			published = true
  53  			mu.Unlock()
  54  			// verify the client sent exactly the textNote
  55  			var raw []json.RawMessage
  56  			err := websocket.JSON.Receive(conn, &raw)
  57  			assert.NoError(t, err)
  58  
  59  			event := parseEventMessage(t, raw)
  60  			assert.True(
  61  				t, utils.FastEqual(event.Serialize(), textNote.Serialize()),
  62  			)
  63  
  64  			// send back an ok nip-20 command result
  65  			res := []any{"OK", hex.Enc(textNote.ID), true, ""}
  66  			err = websocket.JSON.Send(conn, res)
  67  			assert.NoError(t, err)
  68  		},
  69  	)
  70  	defer ws.Close()
  71  
  72  	// connect a client and send the text note
  73  	rl := mustRelayConnect(t, ws.URL)
  74  	err = rl.Publish(context.Background(), textNote)
  75  	assert.NoError(t, err)
  76  
  77  	assert.True(t, published, "fake relay server saw no event")
  78  }
  79  
  80  // func TestPublishBlocked(t *testing.T) {
  81  // 	// test note to be sent over websocket
  82  // 	textNote := &event.E{
  83  // 		Kind: kind.TextNote, Content: []byte("hello"),
  84  // 		CreatedAt: timestamp.Now(),
  85  // 	}
  86  // 	textNote.ID = textNote.GetIDBytes()
  87  //
  88  // 	// fake relay server
  89  // 	ws := newWebsocketServer(
  90  // 		func(conn *websocket.Conn) {
  91  // 			// discard received message; not interested
  92  // 			var raw []json.RawMessage
  93  // 			err := websocket.JSON.Receive(conn, &raw)
  94  // 			assert.NoError(t, err)
  95  //
  96  // 			// send back a not ok nip-20 command result
  97  // 			res := []any{"OK", textNote.IdString(), false, "blocked"}
  98  // 			websocket.JSON.Send(conn, res)
  99  // 		},
 100  // 	)
 101  // 	defer ws.Close()
 102  //
 103  // 	// connect a client and send a text note
 104  // 	rl := mustRelayConnect(t, ws.URL)
 105  // 	err := rl.Publish(context.Background(), textNote)
 106  // 	assert.Error(t, err)
 107  // }
 108  
 109  func TestPublishWriteFailed(t *testing.T) {
 110  	// test note to be sent over websocket
 111  	textNote := &event.E{
 112  		Kind:      kind.TextNote.K,
 113  		Content:   []byte("hello"),
 114  		CreatedAt: time.Now().Unix(),
 115  	}
 116  	textNote.ID = textNote.GetIDBytes()
 117  	// fake relay server
 118  	ws := newWebsocketServer(
 119  		func(conn *websocket.Conn) {
 120  			// reject receive - force send error
 121  			conn.Close()
 122  		},
 123  	)
 124  	defer ws.Close()
 125  	// connect a client and send a text note
 126  	rl := mustRelayConnect(t, ws.URL)
 127  	// Force brief period of time so that publish always fails on closed socket.
 128  	time.Sleep(1 * time.Millisecond)
 129  	err := rl.Publish(context.Background(), textNote)
 130  	assert.Error(t, err)
 131  }
 132  
 133  func TestConnectContext(t *testing.T) {
 134  	// fake relay server
 135  	var mu sync.Mutex // guards connected to satisfy go test -race
 136  	var connected bool
 137  	ws := newWebsocketServer(
 138  		func(conn *websocket.Conn) {
 139  			mu.Lock()
 140  			connected = true
 141  			mu.Unlock()
 142  			io.ReadAll(conn) // discard all input
 143  		},
 144  	)
 145  	defer ws.Close()
 146  
 147  	// relay client
 148  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 149  	defer cancel()
 150  	r, err := RelayConnect(ctx, ws.URL)
 151  	assert.NoError(t, err)
 152  
 153  	defer r.Close()
 154  
 155  	mu.Lock()
 156  	defer mu.Unlock()
 157  	assert.True(t, connected, "fake relay server saw no client connect")
 158  }
 159  
 160  func TestConnectContextCanceled(t *testing.T) {
 161  	// fake relay server
 162  	ws := newWebsocketServer(discardingHandler)
 163  	defer ws.Close()
 164  
 165  	// relay client
 166  	ctx, cancel := context.WithCancel(context.Background())
 167  	cancel() // make ctx expired
 168  	_, err := RelayConnect(ctx, ws.URL)
 169  	assert.ErrorIs(t, err, context.Canceled)
 170  }
 171  
 172  func TestConnectWithOrigin(t *testing.T) {
 173  	// fake relay server
 174  	// default handler requires origin golang.org/x/net/websocket
 175  	ws := httptest.NewServer(websocket.Handler(discardingHandler))
 176  	defer ws.Close()
 177  
 178  	// relay client
 179  	r := NewRelay(
 180  		context.Background(), string(normalize.URL(ws.URL)),
 181  		WithRequestHeader(http.Header{"origin": {"https://example.com"}}),
 182  	)
 183  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 184  	defer cancel()
 185  	err := r.Connect(ctx)
 186  	assert.NoError(t, err)
 187  }
 188  
 189  func discardingHandler(conn *websocket.Conn) {
 190  	io.ReadAll(conn) // discard all input
 191  }
 192  
 193  func newWebsocketServer(handler func(*websocket.Conn)) *httptest.Server {
 194  	return httptest.NewServer(
 195  		&websocket.Server{
 196  			Handshake: anyOriginHandshake,
 197  			Handler:   handler,
 198  		},
 199  	)
 200  }
 201  
 202  // anyOriginHandshake is an alternative to default in golang.org/x/net/websocket
 203  // which checks for origin. nostr client sends no origin and it makes no difference
 204  // for the tests here anyway.
 205  var anyOriginHandshake = func(conf *websocket.Config, r *http.Request) error {
 206  	return nil
 207  }
 208  
 209  func makeKeyPair(t *testing.T) (sec, pub []byte) {
 210  	t.Helper()
 211  	sign := p8k.MustNew()
 212  	var err error
 213  	if err = sign.Generate(); chk.E(err) {
 214  		return
 215  	}
 216  	sec = sign.Sec()
 217  	pub = sign.Pub()
 218  	assert.NoError(t, err)
 219  
 220  	return
 221  }
 222  
 223  func mustRelayConnect(t *testing.T, url string) *Client {
 224  	t.Helper()
 225  
 226  	rl, err := RelayConnect(context.Background(), url)
 227  	require.NoError(t, err)
 228  
 229  	return rl
 230  }
 231  
 232  func parseEventMessage(t *testing.T, raw []json.RawMessage) *event.E {
 233  	t.Helper()
 234  
 235  	assert.Condition(
 236  		t, func() (success bool) {
 237  			return len(raw) >= 2
 238  		},
 239  	)
 240  
 241  	var typ string
 242  	err := json.Unmarshal(raw[0], &typ)
 243  	assert.NoError(t, err)
 244  	assert.Equal(t, "EVENT", typ)
 245  
 246  	event := &event.E{}
 247  	_, err = event.Unmarshal(raw[1])
 248  	require.NoError(t, err)
 249  
 250  	return event
 251  }
 252  
 253  func parseSubscriptionMessage(
 254  	t *testing.T, raw []json.RawMessage,
 255  ) (subid string, ff *filter.S) {
 256  	t.Helper()
 257  
 258  	assert.Greater(t, len(raw), 3)
 259  
 260  	var typ string
 261  	err := json.Unmarshal(raw[0], &typ)
 262  
 263  	assert.NoError(t, err)
 264  	assert.Equal(t, "REQ", typ)
 265  
 266  	var id string
 267  	err = json.Unmarshal(raw[1], &id)
 268  	assert.NoError(t, err)
 269  	ff = &filter.S{}
 270  	for _, b := range raw[2:] {
 271  		var f *filter.F
 272  		err = json.Unmarshal(b, &f)
 273  		assert.NoError(t, err)
 274  		*ff = append(*ff, f)
 275  	}
 276  	return id, ff
 277  }
 278