relay_test.go raw
1 package relay
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "io"
11 "math/rand"
12 "net/http"
13 "os"
14 "path/filepath"
15 "strings"
16 "sync"
17 "testing"
18 "time"
19
20 "git.smesh.lol/torly/pkg/blossom"
21 "git.smesh.lol/torly/pkg/config"
22 "git.smesh.lol/torly/pkg/nostr"
23 )
24
25 type testRelay struct {
26 addr string
27 cfg config.Config
28 tree *Tree
29 }
30
31 func startTestRelay(t *testing.T, authRequired bool) *testRelay {
32 t.Helper()
33 return startTestRelayWith(t, authRequired, nil)
34 }
35
36 func startTestRelayWith(t *testing.T, authRequired bool, mod func(*config.Config)) *testRelay {
37 t.Helper()
38 dir := t.TempDir()
39 cfg := config.Config{
40 AppName: "torly-test",
41 Description: "test relay",
42 ListenHost: "127.0.0.1",
43 DataDir: dir,
44 AuthRequired: authRequired,
45 BlossomEnabled: true,
46 BlossomMaxBlob: 4 << 20,
47 MaxEventBytes: 128 << 10,
48 MaxSubsPerConn: 16,
49 MaxFilters: 8,
50 MaxLimit: 1000,
51 QueryLimit: 1000,
52 MaxFutureSecs: 900,
53 ConnWorkers: 8,
54 VerifyWorkers: 2,
55 ExpiryInterval: 60,
56 }
57 if mod != nil {
58 mod(&cfg)
59 }
60 var tree *Tree
61 var err error
62 for attempt := 0; attempt < 40; attempt++ {
63 cfg.Port = 23000 + rand.Intn(7000)
64 cfg.RelayURL = fmt.Sprintf("ws://127.0.0.1:%d/", cfg.Port)
65 tree, err = Start(cfg)
66 if err == nil {
67 break
68 }
69 }
70 if tree == nil {
71 t.Fatalf("could not start relay: %v", err)
72 }
73 t.Cleanup(tree.Close)
74 return &testRelay{addr: tree.Addr(), cfg: cfg, tree: tree}
75 }
76
77 func countFilter(t *testing.T, addr, filter string) (int, bool) {
78 t.Helper()
79 c := dialWS(t, addr)
80 defer c.close()
81 c.expect(`["AUTH"`)
82 c.writeText([]byte(`["COUNT","c",` + filter + `]`))
83 msg := c.expect(`["COUNT","c"`)
84 var parts []json.RawMessage
85 if err := json.Unmarshal([]byte(msg), &parts); err != nil {
86 t.Fatal(err)
87 }
88 var payload struct {
89 Count int `json:"count"`
90 Approximate bool `json:"approximate"`
91 }
92 if err := json.Unmarshal(parts[2], &payload); err != nil {
93 t.Fatal(err)
94 }
95 return payload.Count, payload.Approximate
96 }
97
98 func testEvent(t *testing.T, seed byte, kind int, tags nostr.Tags, content string) *nostr.Event {
99 t.Helper()
100 var sk [32]byte
101 sk[31] = seed
102 ev := &nostr.Event{Kind: kind, CreatedAt: time.Now().Unix(), Tags: tags, Content: content}
103 if err := ev.Sign(sk); err != nil {
104 t.Fatal(err)
105 }
106 return ev
107 }
108
109 func TestNIP11AndHealth(t *testing.T) {
110 tr := startTestRelay(t, false)
111 req, _ := http.NewRequest("GET", "http://"+tr.addr+"/", nil)
112 req.Header.Set("Accept", "application/nostr+json")
113 resp, err := http.DefaultClient.Do(req)
114 if err != nil {
115 t.Fatal(err)
116 }
117 defer resp.Body.Close()
118 if resp.StatusCode != 200 {
119 t.Fatalf("nip11 status: %d", resp.StatusCode)
120 }
121 var doc config.NIP11
122 if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
123 t.Fatal(err)
124 }
125 if doc.Name != "torly-test" || len(doc.SupportedNIPs) == 0 || doc.Software == "" || doc.PubKey == "" {
126 t.Fatalf("unexpected NIP-11: %+v", doc)
127 }
128
129 resp2, err := http.Get("http://" + tr.addr + "/health")
130 if err != nil {
131 t.Fatal(err)
132 }
133 defer resp2.Body.Close()
134 body, _ := io.ReadAll(resp2.Body)
135 if !strings.Contains(string(body), `"status":"ok"`) {
136 t.Fatalf("health: %s", body)
137 }
138 }
139
140 func TestPublishQueryCount(t *testing.T) {
141 tr := startTestRelay(t, false)
142 c := dialWS(t, tr.addr)
143 defer c.close()
144 c.expect(`["AUTH"`)
145
146 ev := testEvent(t, 1, 1, nostr.Tags{{"t", "integration"}}, "hello integration world")
147 raw := ev.AppendJSON(nil)
148 msg := append([]byte(`["EVENT",`), raw...)
149 msg = append(msg, ']')
150 if err := c.writeText(msg); err != nil {
151 t.Fatal(err)
152 }
153 ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
154 if !strings.Contains(ok, "true") {
155 t.Fatalf("publish rejected: %s", ok)
156 }
157
158 if err := c.writeText([]byte(`["REQ","sub1",{"kinds":[1],"#t":["integration"]}]`)); err != nil {
159 t.Fatal(err)
160 }
161 got := c.expect(`["EVENT","sub1"`)
162 if !strings.Contains(got, nostr.Hex(ev.ID[:])) {
163 t.Fatalf("wrong event: %s", got)
164 }
165 c.expect(`["EOSE","sub1"`)
166
167 if err := c.writeText([]byte(`["COUNT","c1",{"kinds":[1],"#t":["integration"]}]`)); err != nil {
168 t.Fatal(err)
169 }
170 count := c.expect(`["COUNT","c1"`)
171 if !strings.Contains(count, `"count":1`) {
172 t.Fatalf("count: %s", count)
173 }
174
175 // A tampered signature must be rejected.
176 bad := testEvent(t, 1, 1, nil, "bad signature")
177 badSig := bad.Sig
178 badSig[0] ^= 1
179 bad.Sig = badSig
180 raw = bad.AppendJSON(nil)
181 msg = append([]byte(`["EVENT",`), raw...)
182 msg = append(msg, ']')
183 c.writeText(msg)
184 rejected := c.expect(`["OK","` + nostr.Hex(bad.ID[:]) + `"`)
185 if !strings.Contains(rejected, "false") || !strings.Contains(rejected, "invalid") {
186 t.Fatalf("bad signature accepted: %s", rejected)
187 }
188 }
189
190 func TestGraphQuery(t *testing.T) {
191 tr := startTestRelay(t, false)
192 c := dialWS(t, tr.addr)
193 defer c.close()
194 c.expect(`["AUTH"`)
195
196 pubOf := func(seed byte) string {
197 var sk [32]byte
198 sk[31] = seed
199 pub, err := nostr.PubKeyHex(sk)
200 if err != nil {
201 t.Fatal(err)
202 }
203 return pub
204 }
205 alice, bob, carol := pubOf(11), pubOf(12), pubOf(13)
206 contacts := testEvent(t, 11, 3, nostr.Tags{{"p", bob}, {"p", carol}}, "")
207 msg := append([]byte(`["EVENT",`), contacts.AppendJSON(nil)...)
208 msg = append(msg, ']')
209 c.writeText(msg)
210 if ok := c.expect(`["OK","` + nostr.Hex(contacts.ID[:]) + `"`); !strings.Contains(ok, "true") {
211 t.Fatalf("publish: %s", ok)
212 }
213
214 c.writeText([]byte(`["REQ","g",{"_graph":["` + alice + `",1,"PP","OUT"]}]`))
215 raw := c.expect(`["EVENT","g"`)
216 parts, err := nostr.SplitArray([]byte(raw))
217 if err != nil {
218 t.Fatal(err)
219 }
220 result, err := nostr.ParseEvent(parts[2])
221 if err != nil {
222 t.Fatal(err)
223 }
224 if err := result.Verify(); err != nil {
225 t.Fatalf("graph result signature: %v", err)
226 }
227 if result.Kind != nostr.KindGraphResult {
228 t.Fatalf("kind = %d", result.Kind)
229 }
230 if edge, _ := result.Tags.First("edge"); edge != "pp" {
231 t.Fatalf("edge tag = %q", edge)
232 }
233 if seed, _ := result.Tags.First("seed"); seed != alice {
234 t.Fatalf("seed tag = %q", seed)
235 }
236 var content struct {
237 PubkeysByDepth [][]string `json:"pubkeys_by_depth"`
238 TotalPubkeys int `json:"total_pubkeys"`
239 }
240 if err := json.Unmarshal([]byte(result.Content), &content); err != nil {
241 t.Fatalf("content %q: %v", result.Content, err)
242 }
243 if content.TotalPubkeys != 2 || len(content.PubkeysByDepth) != 1 {
244 t.Fatalf("content: %+v", content)
245 }
246 got := map[string]bool{content.PubkeysByDepth[0][0]: true, content.PubkeysByDepth[0][1]: true}
247 if !got[bob] || !got[carol] {
248 t.Fatalf("follows = %v", content.PubkeysByDepth)
249 }
250 c.expect(`["EOSE","g"`)
251
252 c.writeText([]byte(`["REQ","bad",{"_graph":["short",1,"pp","out"]}]`))
253 if closed := c.expect(`["CLOSED","bad"`); !strings.Contains(closed, "invalid") {
254 t.Fatalf("invalid graph query: %s", closed)
255 }
256 c.writeText([]byte(`["COUNT","cg",{"_graph":["` + alice + `",1,"pp","out"]}]`))
257 if closed := c.expect(`["CLOSED","cg"`); !strings.Contains(closed, "_graph") {
258 t.Fatalf("graph count: %s", closed)
259 }
260 }
261
262 func TestLiveDelivery(t *testing.T) {
263 tr := startTestRelay(t, false)
264 sub := dialWS(t, tr.addr)
265 defer sub.close()
266 sub.expect(`["AUTH"`)
267 sub.writeText([]byte(`["REQ","live",{"kinds":[1],"#t":["live"]}]`))
268 sub.expect(`["EOSE","live"`)
269
270 pub := dialWS(t, tr.addr)
271 defer pub.close()
272 pub.expect(`["AUTH"`)
273 ev := testEvent(t, 2, 1, nostr.Tags{{"t", "live"}}, "broadcast me")
274 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
275 msg = append(msg, ']')
276 pub.writeText(msg)
277 pub.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
278 got := sub.expect(`["EVENT","live"`)
279 if !strings.Contains(got, nostr.Hex(ev.ID[:])) {
280 t.Fatalf("live event missing: %s", got)
281 }
282 }
283
284 func TestAuthRequired(t *testing.T) {
285 tr := startTestRelay(t, true)
286 c := dialWS(t, tr.addr)
287 defer c.close()
288 challengeMsg := c.expect(`["AUTH"`)
289 var parts []json.RawMessage
290 if err := json.Unmarshal([]byte(challengeMsg), &parts); err != nil {
291 t.Fatal(err)
292 }
293 var challenge string
294 json.Unmarshal(parts[1], &challenge)
295
296 ev := testEvent(t, 3, 1, nil, "needs auth")
297 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
298 msg = append(msg, ']')
299 c.writeText(msg)
300 rejected := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
301 if !strings.Contains(rejected, "auth-required") {
302 t.Fatalf("expected auth-required, got %s", rejected)
303 }
304
305 auth := testEvent(t, 3, 22242, nostr.Tags{
306 {"relay", tr.cfg.RelayURL},
307 {"challenge", challenge},
308 }, "")
309 authMsg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
310 authMsg = append(authMsg, ']')
311 c.writeText(authMsg)
312 if ok := c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`); !strings.Contains(ok, "true") {
313 t.Fatalf("auth rejected: %s", ok)
314 }
315
316 c.writeText(msg)
317 if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
318 t.Fatalf("authenticated publish rejected: %s", ok)
319 }
320 }
321
322 func TestBlossom(t *testing.T) {
323 tr := startTestRelay(t, false)
324 body := []byte("blossom integration payload")
325 sum := sha256.Sum256(body)
326 hash := hex.EncodeToString(sum[:])
327
328 var sk [32]byte
329 sk[31] = 9
330 pub, _ := nostr.PubKeyHex(sk)
331
332 authEvent := func(action string) string {
333 ev := &nostr.Event{
334 Kind: 24242,
335 CreatedAt: time.Now().Unix(),
336 Tags: nostr.Tags{
337 {"t", action},
338 {"x", hash},
339 {"expiration", fmt.Sprint(time.Now().Unix() + 300)},
340 },
341 }
342 if err := ev.Sign(sk); err != nil {
343 t.Fatal(err)
344 }
345 return "Nostr " + base64.StdEncoding.EncodeToString(ev.AppendJSON(nil))
346 }
347
348 req, _ := http.NewRequest("PUT", "http://"+tr.addr+"/upload", bytes.NewReader(body))
349 req.Header.Set("Authorization", authEvent("upload"))
350 req.Header.Set("Content-Type", "text/plain")
351 resp, err := http.DefaultClient.Do(req)
352 if err != nil {
353 t.Fatal(err)
354 }
355 raw, _ := io.ReadAll(resp.Body)
356 resp.Body.Close()
357 if resp.StatusCode != 200 {
358 t.Fatalf("upload status %d: %s", resp.StatusCode, raw)
359 }
360 var desc blossom.Descriptor
361 if err := json.Unmarshal(raw, &desc); err != nil {
362 t.Fatal(err)
363 }
364 if desc.SHA256 != hash || desc.Size != int64(len(body)) {
365 t.Fatalf("descriptor: %+v", desc)
366 }
367
368 resp, err = http.Get("http://" + tr.addr + "/" + hash)
369 if err != nil {
370 t.Fatal(err)
371 }
372 downloaded, _ := io.ReadAll(resp.Body)
373 resp.Body.Close()
374 if resp.StatusCode != 200 || !bytes.Equal(downloaded, body) {
375 t.Fatalf("download: status %d body %q", resp.StatusCode, downloaded)
376 }
377
378 resp, err = http.Get("http://" + tr.addr + "/list/" + pub)
379 if err != nil {
380 t.Fatal(err)
381 }
382 listBody, _ := io.ReadAll(resp.Body)
383 resp.Body.Close()
384 if !strings.Contains(string(listBody), hash) {
385 t.Fatalf("list: %s", listBody)
386 }
387
388 del, _ := http.NewRequest("DELETE", "http://"+tr.addr+"/"+hash, nil)
389 del.Header.Set("Authorization", authEvent("delete"))
390 resp, err = http.DefaultClient.Do(del)
391 if err != nil {
392 t.Fatal(err)
393 }
394 resp.Body.Close()
395 if resp.StatusCode != 204 {
396 t.Fatalf("delete status: %d", resp.StatusCode)
397 }
398 resp, err = http.Get("http://" + tr.addr + "/" + hash)
399 if err != nil {
400 t.Fatal(err)
401 }
402 resp.Body.Close()
403 if resp.StatusCode != 404 {
404 t.Fatalf("blob survived delete: %d", resp.StatusCode)
405 }
406 }
407
408 func TestUnauthenticatedBlossomUploadRejected(t *testing.T) {
409 tr := startTestRelay(t, false)
410 req, _ := http.NewRequest("PUT", "http://"+tr.addr+"/upload", strings.NewReader("nope"))
411 req.Header.Set("Content-Type", "text/plain")
412 resp, err := http.DefaultClient.Do(req)
413 if err != nil {
414 t.Fatal(err)
415 }
416 defer resp.Body.Close()
417 if resp.StatusCode != 401 {
418 t.Fatalf("upload without auth: status %d", resp.StatusCode)
419 }
420 }
421
422 func TestConcurrentPublish(t *testing.T) {
423 tr := startTestRelay(t, false)
424 const conns = 6
425 const perConn = 4
426
427 clients := make([]*testWS, conns)
428 for i := range clients {
429 clients[i] = dialWS(t, tr.addr)
430 defer clients[i].close()
431 clients[i].expect(`["AUTH"`)
432 }
433
434 var wg sync.WaitGroup
435 errs := make(chan error, conns)
436 for g := 0; g < conns; g++ {
437 wg.Add(1)
438 go func(g int) {
439 defer wg.Done()
440 c := clients[g]
441 for i := 0; i < perConn; i++ {
442 ev := testEvent(t, byte(g*perConn+i+1), 1, nostr.Tags{{"t", "concurrent"}}, "parallel publish")
443 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
444 msg = append(msg, ']')
445 if err := c.writeText(msg); err != nil {
446 errs <- err
447 return
448 }
449 c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
450 }
451 }(g)
452 }
453 wg.Wait()
454 close(errs)
455 for err := range errs {
456 t.Fatalf("publish: %v", err)
457 }
458
459 n, approx := countFilter(t, tr.addr, `{"#t":["concurrent"]}`)
460 if n != conns*perConn || approx {
461 t.Fatalf("count = %d (approximate=%v), want %d", n, approx, conns*perConn)
462 }
463 }
464
465 func TestDuplicatePublish(t *testing.T) {
466 tr := startTestRelay(t, false)
467 c := dialWS(t, tr.addr)
468 defer c.close()
469 c.expect(`["AUTH"`)
470 ev := testEvent(t, 4, 1, nil, "duplicate")
471 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
472 msg = append(msg, ']')
473 c.writeText(msg)
474 if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
475 t.Fatalf("first publish: %s", ok)
476 }
477 c.writeText(msg)
478 dup := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
479 if !strings.Contains(dup, "true") || !strings.Contains(dup, "duplicate") {
480 t.Fatalf("duplicate response: %s", dup)
481 }
482 }
483
484 func TestEphemeralNotStored(t *testing.T) {
485 tr := startTestRelay(t, false)
486 sub := dialWS(t, tr.addr)
487 defer sub.close()
488 sub.expect(`["AUTH"`)
489 sub.writeText([]byte(`["REQ","eph",{"kinds":[20001]}]`))
490 sub.expect(`["EOSE","eph"`)
491
492 pub := dialWS(t, tr.addr)
493 defer pub.close()
494 pub.expect(`["AUTH"`)
495 ev := testEvent(t, 5, 20001, nostr.Tags{{"t", "ephemeral"}}, "do not store")
496 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
497 msg = append(msg, ']')
498 pub.writeText(msg)
499 if ok := pub.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
500 t.Fatalf("ephemeral rejected: %s", ok)
501 }
502 sub.expect(`["EVENT","eph"`)
503 if n, _ := countFilter(t, tr.addr, `{"kinds":[20001]}`); n != 0 {
504 t.Fatalf("ephemeral stored: n=%d", n)
505 }
506 }
507
508 func TestProtectedEventRequiresAuth(t *testing.T) {
509 tr := startTestRelay(t, false)
510 c := dialWS(t, tr.addr)
511 defer c.close()
512 challenge := challengeFrom(t, c.expect(`["AUTH"`))
513
514 ev := testEvent(t, 6, 1, nostr.Tags{{"-"}}, "protected")
515 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
516 msg = append(msg, ']')
517 c.writeText(msg)
518 if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "auth-required") {
519 t.Fatalf("protected event accepted without auth: %s", ok)
520 }
521
522 auth := testEvent(t, 6, 22242, nostr.Tags{{"relay", tr.cfg.RelayURL}, {"challenge", challenge}}, "")
523 authMsg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
524 authMsg = append(authMsg, ']')
525 c.writeText(authMsg)
526 c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`)
527
528 c.writeText(msg)
529 if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
530 t.Fatalf("protected event rejected for its author: %s", ok)
531 }
532 }
533
534 func TestAuthWrongChallenge(t *testing.T) {
535 tr := startTestRelay(t, true)
536 c := dialWS(t, tr.addr)
537 defer c.close()
538 c.expect(`["AUTH"`)
539 auth := testEvent(t, 7, 22242, nostr.Tags{{"relay", tr.cfg.RelayURL}, {"challenge", "wrong"}}, "")
540 msg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
541 msg = append(msg, ']')
542 c.writeText(msg)
543 got := c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`)
544 if !strings.Contains(got, "false") || !strings.Contains(got, "auth-required") {
545 t.Fatalf("wrong challenge accepted: %s", got)
546 }
547 }
548
549 func challengeFrom(t *testing.T, msg string) string {
550 t.Helper()
551 var parts []json.RawMessage
552 if err := json.Unmarshal([]byte(msg), &parts); err != nil {
553 t.Fatal(err)
554 }
555 var challenge string
556 if err := json.Unmarshal(parts[1], &challenge); err != nil {
557 t.Fatal(err)
558 }
559 return challenge
560 }
561
562 func TestMalformedMessages(t *testing.T) {
563 tr := startTestRelay(t, false)
564 c := dialWS(t, tr.addr)
565 defer c.close()
566 c.expect(`["AUTH"`)
567
568 c.writeText([]byte(`this is not json`))
569 c.expect(`["NOTICE"`)
570
571 c.writeText([]byte(`["NOPE"]`))
572 notice := c.expect(`["NOTICE"`)
573 if !strings.Contains(notice, "unknown command") {
574 t.Fatalf("unknown command notice: %s", notice)
575 }
576
577 c.writeText([]byte(`["REQ","onlyid"]`))
578 c.expect(`["NOTICE"`)
579
580 c.writeText([]byte(`["REQ","badsub",{"kinds":"nope"}]`))
581 closed := c.expect(`["CLOSED","badsub"`)
582 if !strings.Contains(closed, "invalid") {
583 t.Fatalf("bad filter closed: %s", closed)
584 }
585
586 c.writeText([]byte(`["REQ",""]`))
587 c.expect(`["NOTICE"`)
588
589 longID := strings.Repeat("x", 65)
590 c.writeText([]byte(`["REQ","` + longID + `",{}]`))
591 c.expect(`["NOTICE"`)
592 }
593
594 func TestFilterAndSubscriptionLimits(t *testing.T) {
595 tr := startTestRelayWith(t, false, func(c *config.Config) {
596 c.MaxFilters = 2
597 c.MaxSubsPerConn = 2
598 })
599 c := dialWS(t, tr.addr)
600 defer c.close()
601 c.expect(`["AUTH"`)
602
603 c.writeText([]byte(`["REQ","many",{},{},{}]`))
604 closed := c.expect(`["CLOSED","many"`)
605 if !strings.Contains(closed, "too many filters") {
606 t.Fatalf("filter limit: %s", closed)
607 }
608
609 c.writeText([]byte(`["REQ","one",{}]`))
610 c.expect(`["EOSE","one"`)
611 c.writeText([]byte(`["REQ","two",{}]`))
612 c.expect(`["EOSE","two"`)
613 c.writeText([]byte(`["REQ","three",{}]`))
614 closed = c.expect(`["CLOSED","three"`)
615 if !strings.Contains(closed, "too many subscriptions") {
616 t.Fatalf("subscription limit: %s", closed)
617 }
618 }
619
620 func TestCountMultipleFiltersIsApproximate(t *testing.T) {
621 tr := startTestRelay(t, false)
622 c := dialWS(t, tr.addr)
623 defer c.close()
624 c.expect(`["AUTH"`)
625 ev := testEvent(t, 8, 1, nostr.Tags{{"t", "countmulti"}}, "counted twice")
626 msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
627 msg = append(msg, ']')
628 c.writeText(msg)
629 c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
630
631 c.writeText([]byte(`["COUNT","cm",{"kinds":[1]},{"kinds":[1]}]`))
632 got := c.expect(`["COUNT","cm"`)
633 if !strings.Contains(got, `"count":2`) || !strings.Contains(got, `"approximate":true`) {
634 t.Fatalf("multi-filter count: %s", got)
635 }
636 }
637
638 func TestStaticFilesAndNotFound(t *testing.T) {
639 dir := t.TempDir()
640 if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html>static</html>"), 0o644); err != nil {
641 t.Fatal(err)
642 }
643 tr := startTestRelayWith(t, false, func(c *config.Config) { c.StaticDir = dir })
644
645 resp, err := http.Get("http://" + tr.addr + "/")
646 if err != nil {
647 t.Fatal(err)
648 }
649 body, _ := io.ReadAll(resp.Body)
650 resp.Body.Close()
651 if resp.StatusCode != 200 || !strings.Contains(string(body), "static") {
652 t.Fatalf("static index: %d %q", resp.StatusCode, body)
653 }
654
655 resp, err = http.Get("http://" + tr.addr + "/missing.txt")
656 if err != nil {
657 t.Fatal(err)
658 }
659 resp.Body.Close()
660 if resp.StatusCode != 404 {
661 t.Fatalf("missing static: %d", resp.StatusCode)
662 }
663
664 resp, err = http.Get("http://" + tr.addr + "/../server_test.go")
665 if err != nil {
666 t.Fatal(err)
667 }
668 resp.Body.Close()
669 if resp.StatusCode != 404 {
670 t.Fatalf("traversal: %d", resp.StatusCode)
671 }
672 }
673
674 func TestNIP11Head(t *testing.T) {
675 tr := startTestRelay(t, false)
676 req, _ := http.NewRequest("HEAD", "http://"+tr.addr+"/", nil)
677 req.Header.Set("Accept", "application/nostr+json")
678 resp, err := http.DefaultClient.Do(req)
679 if err != nil {
680 t.Fatal(err)
681 }
682 defer resp.Body.Close()
683 if resp.StatusCode != 200 || resp.Header.Get("Content-Length") == "" {
684 t.Fatalf("NIP-11 HEAD: %d %v", resp.StatusCode, resp.Header)
685 }
686 body, _ := io.ReadAll(resp.Body)
687 if len(body) != 0 {
688 t.Fatalf("HEAD returned a body: %q", body)
689 }
690 }
691