package relay import ( "context" "errors" "io" "sync" "testing" "time" "git.smesh.lol/nostrino/pkg/nostr" ) type fakeTransport struct { in chan []byte out chan []byte closed chan struct{} once sync.Once } func newFakeTransport() *fakeTransport { return &fakeTransport{ in: make(chan []byte, 8), out: make(chan []byte, 8), closed: make(chan struct{}), } } func (f *fakeTransport) WriteText(msg []byte) error { cp := append([]byte(nil), msg...) select { case f.out <- cp: return nil case <-f.closed: return errors.New("fake: closed") } } func (f *fakeTransport) ReadMessage() ([]byte, error) { select { case msg := <-f.in: return msg, nil case <-f.closed: return nil, io.EOF } } func (f *fakeTransport) Close() error { f.once.Do(func() { close(f.closed) }) return nil } func recv(t *testing.T, f *fakeTransport) [][]byte { t.Helper() select { case msg := <-f.out: parts, err := nostr.SplitArray(msg) if err != nil { t.Fatalf("out: %v", err) } return parts case <-time.After(2 * time.Second): t.Fatal("timed out waiting for client write") return nil } } func TestNIP42Auth(t *testing.T) { f := newFakeTransport() var key [32]byte key[31] = 3 c, err := New(f, key, Options{RelayURL: "ws://relay.test"}) if err != nil { t.Fatal(err) } defer c.Close() f.in <- []byte(`["AUTH","challenge-xyz"]`) parts := recv(t, f) if len(parts) != 2 { t.Fatalf("auth message parts %d", len(parts)) } typ, _ := nostr.ParseJSONString(parts[0]) if typ != "AUTH" { t.Fatalf("type %q", typ) } ev, err := nostr.ParseEvent(parts[1]) if err != nil { t.Fatal(err) } if err := ev.Verify(); err != nil { t.Fatal(err) } if ev.Kind != 22242 { t.Fatalf("kind %d", ev.Kind) } if relay, _ := ev.Tags.First("relay"); relay != "ws://relay.test" { t.Fatalf("relay tag %q", relay) } if challenge, _ := ev.Tags.First("challenge"); challenge != "challenge-xyz" { t.Fatalf("challenge tag %q", challenge) } // A publish waits for the auth result, then writes an EVENT and waits for // its own OK. pub := make(chan error, 1) go func() { pub <- c.Publish(context.Background(), &nostr.Event{Kind: 1, Content: "hello"}) }() select { case parts := <-f.out: t.Fatalf("published before auth completed: %s", string(parts[0])) case <-time.After(100 * time.Millisecond): } id := nostr.Hex(ev.ID[:]) f.in <- []byte(`["OK","` + id + `",true,""]`) parts = recv(t, f) typ, _ = nostr.ParseJSONString(parts[0]) if typ != "EVENT" { t.Fatalf("expected EVENT, got %q", typ) } event, err := nostr.ParseEvent(parts[1]) if err != nil { t.Fatal(err) } f.in <- []byte(`["OK","` + nostr.Hex(event.ID[:]) + `",true,""]`) select { case err := <-pub: if err != nil { t.Fatalf("publish: %v", err) } case <-time.After(2 * time.Second): t.Fatal("publish did not return") } } func TestNIP42AuthRejected(t *testing.T) { f := newFakeTransport() var key [32]byte key[31] = 4 c, err := New(f, key, Options{RelayURL: "ws://relay.test"}) if err != nil { t.Fatal(err) } defer c.Close() f.in <- []byte(`["AUTH","challenge"]`) parts := recv(t, f) ev, err := nostr.ParseEvent(parts[1]) if err != nil { t.Fatal(err) } f.in <- []byte(`["OK","` + nostr.Hex(ev.ID[:]) + `",false,"auth-required: nope"]`) time.Sleep(50 * time.Millisecond) if err := c.Send(&nostr.Event{Kind: 1, Content: "x"}); err == nil { t.Fatal("send succeeded after rejected auth") } } func TestAuthEvent(t *testing.T) { var key [32]byte key[31] = 5 ev := AuthEvent("wss://relay", "c", key) if ev == nil { t.Fatal("no event") } if err := ev.Verify(); err != nil { t.Fatal(err) } if v, _ := ev.Tags.First("relay"); v != "wss://relay" { t.Fatal("relay tag") } }