package server import ( "net" "net/http" "testing" "time" "smesh.lol/pkg/acl" "smesh.lol/pkg/nostr/event" "smesh.lol/pkg/nostr/filter" "smesh.lol/pkg/nostr/kind" "smesh.lol/pkg/nostr/signer/p8k" "smesh.lol/pkg/nostr/tag" "smesh.lol/pkg/nostr/ws" "smesh.lol/pkg/relay/pipeline" "smesh.lol/pkg/store" ) func startRelay(t *testing.T) (addr string) { t.Helper() eng, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { eng.Close() }) srv := New(eng, acl.Open{}, nil, pipeline.DefaultConfig()) ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { ln.Close() }) go http.Serve(ln, srv) return ln.Addr().String() } func TestUpgradeAndClose(t *testing.T) { addr := startRelay(t) c, err := ws.Dial("ws://" + addr) if err != nil { t.Fatalf("dial: %v", err) } c.Close() } func TestPublishAndSubscribe(t *testing.T) { addr := startRelay(t) url := "ws://" + addr // Client 1: publisher. pub, err := ws.Connect(url) if err != nil { t.Fatalf("connect publisher: %v", err) } defer pub.Close() signer := p8k.MustNew() if err := signer.Generate(); err != nil { t.Fatal(err) } ev := &event.E{ CreatedAt: time.Now().Unix(), Kind: 1, Content: []byte("hello relay"), } if err := ev.Sign(signer); err != nil { t.Fatal(err) } if err := pub.Publish(ev); err != nil { t.Fatalf("publish: %v", err) } // Allow processing. time.Sleep(100 * time.Millisecond) // Client 2: subscriber queries stored events. sub, err := ws.Connect(url) if err != nil { t.Fatalf("connect subscriber: %v", err) } defer sub.Close() f := &filter.F{ Kinds: kind.NewS(kind.New(uint16(1))), Authors: tag.NewFromBytesSlice(signer.Pub()), } limit := uint(10) f.Limit = &limit subscription, err := sub.Subscribe(f) if err != nil { t.Fatalf("subscribe: %v", err) } var received []*event.E timeout := time.After(3 * time.Second) loop: for { select { case got, ok := <-subscription.Events: if !ok { break loop } if got != nil { received = append(received, got) } case <-subscription.EOSE: break loop case <-timeout: t.Fatal("timeout waiting for EOSE") } } if len(received) != 1 { t.Fatalf("expected 1 event, got %d", len(received)) } if string(received[0].Content) != "hello relay" { t.Fatalf("wrong content: %s", received[0].Content) } } func TestRealTimeBroadcast(t *testing.T) { addr := startRelay(t) url := "ws://" + addr signer := p8k.MustNew() if err := signer.Generate(); err != nil { t.Fatal(err) } // Client 1: subscribe first. c1, err := ws.Connect(url) if err != nil { t.Fatal(err) } defer c1.Close() f := &filter.F{ Kinds: kind.NewS(kind.New(uint16(1))), Authors: tag.NewFromBytesSlice(signer.Pub()), } limit := uint(10) f.Limit = &limit subscription, err := c1.Subscribe(f) if err != nil { t.Fatal(err) } // Wait for EOSE (subscription active, no stored events yet). select { case <-subscription.EOSE: case <-time.After(3 * time.Second): t.Fatal("timeout waiting for EOSE") } // Client 2: publish after subscription is active. c2, err := ws.Connect(url) if err != nil { t.Fatal(err) } defer c2.Close() ev := &event.E{ CreatedAt: time.Now().Unix(), Kind: 1, Content: []byte("real-time!"), } if err := ev.Sign(signer); err != nil { t.Fatal(err) } if err := c2.Publish(ev); err != nil { t.Fatal(err) } // Client 1 should receive the event via broadcast. select { case got := <-subscription.Events: if got == nil { t.Fatal("got nil event") } if string(got.Content) != "real-time!" { t.Fatalf("wrong content: %s", got.Content) } case <-time.After(3 * time.Second): t.Fatal("timeout waiting for real-time event") } } func TestNIP11Info(t *testing.T) { addr := startRelay(t) req, _ := http.NewRequest("GET", "http://"+addr, nil) req.Header.Set("Accept", "application/nostr+json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("NIP-11 request: %v", err) } defer resp.Body.Close() if resp.Header.Get("Content-Type") != "application/nostr+json" { t.Fatalf("wrong content-type: %s", resp.Header.Get("Content-Type")) } buf := []byte{:1024} n, _ := resp.Body.Read(buf) body := string(buf[:n]) if len(body) == 0 || body[0] != '{' { t.Fatalf("bad NIP-11 body: %s", body) } }