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