server_subs_test.mx raw
1 package server
2
3 import (
4 "bytes"
5 "testing"
6 "time"
7
8 "git.smesh.lol/morly/pkg/broadcast"
9 "git.smesh.lol/nostr/pkg/envelope"
10 "git.smesh.lol/nostr/pkg/event"
11 "git.smesh.lol/nostr/pkg/signer/p8k"
12 "git.smesh.lol/nostr/pkg/tag"
13 "git.smesh.lol/morly/pkg/relay/config"
14 )
15
16 // stSubSrv builds a server with a live broadcast worker (its own spawn domain)
17 // and a fake database. Callers must close the broadcaster.
18 func stSubSrv(c *config.C) (s *Server, b *broadcast.Broadcaster) {
19 s = stServer(c)
20 s.db = stFakeDB()
21 b = broadcast.New()
22 s.bcast = b
23 return
24 }
25
26 // stAuthEvent builds and signs a NIP-42 AUTH event. A nil tag slice omits that
27 // tag, so the missing-tag failure paths are reachable.
28 func stAuthEvent(t *testing.T, sg *p8k.Signer, k uint16, ch, rl []byte) (ev *event.E) {
29 t.Helper()
30 ev = event.New()
31 ev.CreatedAt = time.Now().Unix()
32 ev.Kind = k
33 var chT *tag.T
34 var rlT *tag.T
35 if ch != nil {
36 chT = tag.NewFromBytesSlice([]byte("challenge"), ch)
37 }
38 if rl != nil {
39 rlT = tag.NewFromBytesSlice([]byte("relay"), rl)
40 }
41 if chT != nil && rlT != nil {
42 ev.Tags = tag.NewS(chT, rlT)
43 } else if chT != nil {
44 ev.Tags = tag.NewS(chT)
45 } else if rlT != nil {
46 ev.Tags = tag.NewS(rlT)
47 }
48 if serr := ev.Sign(sg); serr != nil {
49 t.Fatal(serr)
50 return
51 }
52 return
53 }
54
55 func stAuthMsg(ev *event.E) (msg []byte) {
56 ar := &envelope.AuthResponse{Event: ev}
57 return ar.Marshal(nil)
58 }
59
60 func stNewSigner(t *testing.T) (sg *p8k.Signer) {
61 t.Helper()
62 sg = p8k.MustNew()
63 if gerr := sg.Generate(); gerr != nil {
64 t.Fatal(gerr)
65 return
66 }
67 return
68 }
69
70 // --- subscription registry ---
71
72 func TestHandleReqRegistersSubscription(t *testing.T) {
73 s, b := stSubSrv(stCfg())
74 defer b.Close()
75 fd := int32(4)
76 s.conns[fd] = &cstate{subs: map[string]*sub{}}
77
78 req := []byte("[\"REQ\",\"s1\",{}]")
79 s.handleReq(fd, req)
80 cs := s.conns[fd]
81 if len(cs.subs) != 1 {
82 t.Fatalf("subs = %d, want 1", len(cs.subs))
83 return
84 }
85 sb := cs.subs["s1"]
86 if sb == nil {
87 t.Fatal("subscription s1 must be registered")
88 return
89 }
90 if sb.id != "s1" {
91 t.Fatalf("subscription id = %s", sb.id)
92 return
93 }
94 if !bytes.Equal(sb.rawReq, req) {
95 t.Fatal("subscription must retain the raw REQ frame")
96 return
97 }
98 if len(s.dbPending) != 1 {
99 t.Fatalf("dbPending = %d, want a history backfill", len(s.dbPending))
100 return
101 }
102 }
103
104 func TestHandleReqSubscriptionCap(t *testing.T) {
105 c := stCfg()
106 c.MaxSubscriptions = 1
107 s, b := stSubSrv(c)
108 defer b.Close()
109 fd := int32(4)
110 s.conns[fd] = &cstate{subs: map[string]*sub{}}
111
112 s.handleReq(fd, []byte("[\"REQ\",\"s1\",{}]"))
113 s.handleReq(fd, []byte("[\"REQ\",\"s2\",{}]"))
114 if len(s.conns[fd].subs) != 1 {
115 t.Fatalf("cap must hold at 1, got %d", len(s.conns[fd].subs))
116 return
117 }
118 if _, has := s.conns[fd].subs["s2"]; has {
119 t.Fatal("s2 must have been refused")
120 return
121 }
122 // Re-subscribing an existing id is a replace, not a second slot.
123 s.handleReq(fd, []byte("[\"REQ\",\"s1\",{}]"))
124 if len(s.conns[fd].subs) != 1 {
125 t.Fatal("replacing an existing subscription must not grow the set")
126 return
127 }
128 }
129
130 func TestHandleReqAuthRequired(t *testing.T) {
131 c := stCfg()
132 c.AuthRequired = true
133 s, b := stSubSrv(c)
134 defer b.Close()
135 fd := int32(4)
136 cs := &cstate{subs: map[string]*sub{}}
137 s.conns[fd] = cs
138
139 s.handleReq(fd, []byte("[\"REQ\",\"s1\",{}]"))
140 if len(cs.subs) != 0 {
141 t.Fatal("an unauthed REQ must be refused under AuthRequired")
142 return
143 }
144
145 cs.authedPubkey = []byte{0xAB}
146 s.handleReq(fd, []byte("[\"REQ\",\"s1\",{}]"))
147 if len(cs.subs) != 1 {
148 t.Fatal("an authed REQ must be accepted")
149 return
150 }
151 }
152
153 func TestHandleReqMalformedAndTainted(t *testing.T) {
154 s, b := stSubSrv(stCfg())
155 defer b.Close()
156 fd := int32(4)
157 cs := &cstate{subs: map[string]*sub{}}
158 s.conns[fd] = cs
159
160 s.handleReq(99, []byte("[\"REQ\",\"s1\",{}]"))
161
162 s.handleReq(fd, []byte("[\"REQ\",]"))
163 if len(cs.subs) != 0 {
164 t.Fatal("a malformed REQ must not register")
165 return
166 }
167
168 // An undersized hex id taints the filter and jails the connection.
169 s.handleReq(fd, []byte("[\"REQ\",\"s2\",{\"ids\":[\"abcd\"]}]"))
170 if !cs.jailed {
171 t.Fatal("an undersized hex id must jail the connection")
172 return
173 }
174 if len(cs.subs) != 0 {
175 t.Fatal("a tainted REQ must not register")
176 return
177 }
178 }
179
180 func TestHandleClose(t *testing.T) {
181 s, b := stSubSrv(stCfg())
182 defer b.Close()
183 fd := int32(4)
184 cs := &cstate{subs: map[string]*sub{}}
185 s.conns[fd] = cs
186
187 s.handleReq(fd, []byte("[\"REQ\",\"s1\",{}]"))
188 if len(cs.subs) != 1 {
189 t.Fatal("setup: s1 must be registered")
190 return
191 }
192 s.handleClose(fd, []byte("[\"CLOSE\",\"s1\"]"))
193 if len(cs.subs) != 0 {
194 t.Fatal("CLOSE must remove the subscription")
195 return
196 }
197 // Unknown connection and malformed CLOSE are no-ops.
198 s.handleClose(99, []byte("[\"CLOSE\",\"s1\"]"))
199 s.handleClose(fd, []byte("[\"CLOSE\"]"))
200 }
201
202 func TestOnWSClose(t *testing.T) {
203 s, b := stSubSrv(stCfg())
204 defer b.Close()
205 fd := int32(4)
206 cs := &cstate{subs: map[string]*sub{}}
207 cs.subs["s1"] = &sub{id: "s1"}
208 cs.challenge = []byte("c")
209 cs.authedPubkey = []byte("p")
210 s.conns[fd] = cs
211 s.pendingReq[1] = fd
212 s.pendingReq[2] = 99
213 s.dbPending[3] = fd
214 s.dbPending[4] = 99
215
216 s.OnWSClose(fd)
217
218 if s.conns[fd] != nil {
219 t.Fatal("OnWSClose must drop the connection record")
220 return
221 }
222 if _, ok := s.pendingReq[1]; ok {
223 t.Fatal("pendingReq entries for the closed fd must be dropped")
224 return
225 }
226 if _, ok := s.pendingReq[2]; !ok {
227 t.Fatal("pendingReq entries for other fds must survive")
228 return
229 }
230 if _, ok := s.dbPending[3]; ok {
231 t.Fatal("dbPending entries for the closed fd must be dropped")
232 return
233 }
234 if _, ok := s.dbPending[4]; !ok {
235 t.Fatal("dbPending entries for other fds must survive")
236 return
237 }
238 }
239
240 // --- NIP-42 auth ---
241
242 func TestHandleAuthSuccess(t *testing.T) {
243 c := stCfg()
244 c.RelayURL = "wss://relay.example.com"
245 s, b := stSubSrv(c)
246 defer b.Close()
247 fd := int32(4)
248 cs := &cstate{subs: map[string]*sub{}}
249 cs.challenge = []byte("chal-123")
250 s.conns[fd] = cs
251 sg := stNewSigner(t)
252
253 ev := stAuthEvent(t, sg, 22242, []byte("chal-123"), []byte("relay.example.com"))
254 s.handleAuth(fd, stAuthMsg(ev))
255
256 if len(cs.authedPubkey) != 32 {
257 t.Fatalf("authedPubkey length = %d, want 32", len(cs.authedPubkey))
258 return
259 }
260 if !bytes.Equal(cs.authedPubkey, ev.Pubkey) {
261 t.Fatal("authedPubkey must be the auth event's pubkey")
262 return
263 }
264 }
265
266 func TestHandleAuthFailures(t *testing.T) {
267 c := stCfg()
268 c.RelayURL = "wss://relay.example.com"
269 s := stServer(c)
270 fd := int32(4)
271 cs := &cstate{subs: map[string]*sub{}}
272 cs.challenge = []byte("chal-123")
273 s.conns[fd] = cs
274 sg := stNewSigner(t)
275
276 // No connection, then an unparseable AUTH frame.
277 s.handleAuth(99, []byte("[\"AUTH\",]"))
278 s.handleAuth(fd, []byte("[\"AUTH\",]"))
279
280 // Wrong event kind.
281 s.handleAuth(fd, stAuthMsg(stAuthEvent(t, sg, 1, []byte("chal-123"), []byte("relay.example.com"))))
282 // Missing challenge tag.
283 s.handleAuth(fd, stAuthMsg(stAuthEvent(t, sg, 22242, nil, []byte("relay.example.com"))))
284 // Wrong challenge.
285 s.handleAuth(fd, stAuthMsg(stAuthEvent(t, sg, 22242, []byte("nope"), []byte("relay.example.com"))))
286 // Missing relay tag.
287 s.handleAuth(fd, stAuthMsg(stAuthEvent(t, sg, 22242, []byte("chal-123"), nil)))
288 // Relay URL mismatch.
289 s.handleAuth(fd, stAuthMsg(stAuthEvent(t, sg, 22242, []byte("chal-123"), []byte("other.example.com"))))
290
291 // Timestamp out of range.
292 old := stAuthEvent(t, sg, 22242, []byte("chal-123"), []byte("relay.example.com"))
293 old.CreatedAt = time.Now().Unix() - 1000
294 if serr := old.Sign(sg); serr != nil {
295 t.Fatal(serr)
296 return
297 }
298 s.handleAuth(fd, stAuthMsg(old))
299
300 // Invalid signature.
301 bad := stAuthEvent(t, sg, 22242, []byte("chal-123"), []byte("relay.example.com"))
302 bad.Sig[0] ^= 0xFF
303 s.handleAuth(fd, stAuthMsg(bad))
304
305 if len(cs.authedPubkey) != 0 {
306 t.Fatal("no failure path may authenticate the connection")
307 return
308 }
309 }
310
311 // --- broadcast domain ---
312
313 func TestHandleBroadcastFrameNoFrames(t *testing.T) {
314 s, b := stSubSrv(stCfg())
315 defer b.Close()
316 s.handleBroadcastFrame()
317 if !b.Valid() {
318 t.Fatal("the broadcast worker must stay alive")
319 return
320 }
321 }
322
323 func TestSendBroadcastFlags(t *testing.T) {
324 c := stCfg()
325 c.RelayURL = "wss://relay.example"
326 c.PrivilegedOpen = false
327 c.NIP70Enforce = true
328 c.MarmotOpen = true
329 s, b := stSubSrv(c)
330 defer b.Close()
331 s.sendBroadcast([]byte("[\"EVENT\",{}]"), 3)
332 }
333
334 // --- worker lifecycle ---
335
336 func TestWorkerStartupAndRespawn(t *testing.T) {
337 s := stServer(stCfg())
338
339 s.startIngestWorkers(1)
340 if s.workers.Len() != 1 || len(s.workerIn) != 1 {
341 t.Fatal("ingest pool must be started")
342 return
343 }
344 if !workerAlive(s.workerDone[0]) {
345 t.Fatal("ingest worker must be alive")
346 return
347 }
348 oldIngest := s.workerIn[0]
349 s.workers.Busy[0] = true
350 s.respawnIngestWorker(0)
351 if s.workers.Busy[0] {
352 t.Fatal("ingest respawn must clear the busy flag")
353 return
354 }
355 if !workerAlive(s.workerDone[0]) {
356 t.Fatal("respawned ingest worker must be alive")
357 return
358 }
359 close(oldIngest)
360 close(s.workerIn[0])
361
362 s.startProxyWorkers(1)
363 if s.proxyPool.Len() != 1 || len(s.proxyBusyTime) != 1 {
364 t.Fatal("proxy pool must be started")
365 return
366 }
367 oldProxy := s.proxyIn[0]
368 s.proxyPool.Busy[0] = true
369 s.proxyBusyTime[0] = 123
370 s.respawnProxyWorker(0)
371 if s.proxyPool.Busy[0] || s.proxyBusyTime[0] != 0 {
372 t.Fatal("proxy respawn must reset busy state")
373 return
374 }
375 if !workerAlive(s.proxyDone[0]) {
376 t.Fatal("respawned proxy worker must be alive")
377 return
378 }
379 close(oldProxy)
380 close(s.proxyIn[0])
381
382 s.startBlossomWorkers(1)
383 if s.blossomPool.Len() != 1 {
384 t.Fatal("blossom pool must be started")
385 return
386 }
387 oldBlossom := s.blossomIn[0]
388 s.blossomPool.Busy[0] = true
389 s.respawnBlossomWorker(0)
390 if s.blossomPool.Busy[0] {
391 t.Fatal("blossom respawn must clear the busy flag")
392 return
393 }
394 if !workerAlive(s.blossomDone[0]) {
395 t.Fatal("respawned blossom worker must be alive")
396 return
397 }
398 close(oldBlossom)
399 close(s.blossomIn[0])
400 }
401
402 func TestServerCloseIdempotent(t *testing.T) {
403 c := stCfg()
404 s, b := stSubSrv(c)
405 if b == nil {
406 t.Fatal("setup: broadcaster must exist")
407 return
408 }
409 s.Close()
410 s.Close()
411 if !s.closed {
412 t.Fatal("Close must set the closed flag")
413 return
414 }
415 // Close's effect on the broadcast worker is not asserted: in this build a
416 // spawn channel's close() is not observed by the child and the spawn done
417 // channel never signals the child's exit, so Valid() stays true forever.
418 // Reported; the closed flag (idempotence) is what Close can be held to.
419
420 // A server with no database still closes cleanly and idempotently.
421 s2 := stServer(stCfg())
422 s2.bcast = broadcast.New()
423 s2.Close()
424 s2.Close()
425 if !s2.closed {
426 t.Fatal("second server Close must set the closed flag")
427 return
428 }
429 }
430