auth_test.go raw
1 package relay
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "sync"
8 "testing"
9 "time"
10
11 "git.smesh.lol/nostrino/pkg/nostr"
12 )
13
14 type fakeTransport struct {
15 in chan []byte
16 out chan []byte
17 closed chan struct{}
18 once sync.Once
19 }
20
21 func newFakeTransport() *fakeTransport {
22 return &fakeTransport{
23 in: make(chan []byte, 8),
24 out: make(chan []byte, 8),
25 closed: make(chan struct{}),
26 }
27 }
28
29 func (f *fakeTransport) WriteText(msg []byte) error {
30 cp := append([]byte(nil), msg...)
31 select {
32 case f.out <- cp:
33 return nil
34 case <-f.closed:
35 return errors.New("fake: closed")
36 }
37 }
38
39 func (f *fakeTransport) ReadMessage() ([]byte, error) {
40 select {
41 case msg := <-f.in:
42 return msg, nil
43 case <-f.closed:
44 return nil, io.EOF
45 }
46 }
47
48 func (f *fakeTransport) Close() error {
49 f.once.Do(func() { close(f.closed) })
50 return nil
51 }
52
53 func recv(t *testing.T, f *fakeTransport) [][]byte {
54 t.Helper()
55 select {
56 case msg := <-f.out:
57 parts, err := nostr.SplitArray(msg)
58 if err != nil {
59 t.Fatalf("out: %v", err)
60 }
61 return parts
62 case <-time.After(2 * time.Second):
63 t.Fatal("timed out waiting for client write")
64 return nil
65 }
66 }
67
68 func TestNIP42Auth(t *testing.T) {
69 f := newFakeTransport()
70 var key [32]byte
71 key[31] = 3
72 c, err := New(f, key, Options{RelayURL: "ws://relay.test"})
73 if err != nil {
74 t.Fatal(err)
75 }
76 defer c.Close()
77
78 f.in <- []byte(`["AUTH","challenge-xyz"]`)
79 parts := recv(t, f)
80 if len(parts) != 2 {
81 t.Fatalf("auth message parts %d", len(parts))
82 }
83 typ, _ := nostr.ParseJSONString(parts[0])
84 if typ != "AUTH" {
85 t.Fatalf("type %q", typ)
86 }
87 ev, err := nostr.ParseEvent(parts[1])
88 if err != nil {
89 t.Fatal(err)
90 }
91 if err := ev.Verify(); err != nil {
92 t.Fatal(err)
93 }
94 if ev.Kind != 22242 {
95 t.Fatalf("kind %d", ev.Kind)
96 }
97 if relay, _ := ev.Tags.First("relay"); relay != "ws://relay.test" {
98 t.Fatalf("relay tag %q", relay)
99 }
100 if challenge, _ := ev.Tags.First("challenge"); challenge != "challenge-xyz" {
101 t.Fatalf("challenge tag %q", challenge)
102 }
103
104 // A publish waits for the auth result, then writes an EVENT and waits for
105 // its own OK.
106 pub := make(chan error, 1)
107 go func() {
108 pub <- c.Publish(context.Background(), &nostr.Event{Kind: 1, Content: "hello"})
109 }()
110 select {
111 case parts := <-f.out:
112 t.Fatalf("published before auth completed: %s", string(parts[0]))
113 case <-time.After(100 * time.Millisecond):
114 }
115 id := nostr.Hex(ev.ID[:])
116 f.in <- []byte(`["OK","` + id + `",true,""]`)
117 parts = recv(t, f)
118 typ, _ = nostr.ParseJSONString(parts[0])
119 if typ != "EVENT" {
120 t.Fatalf("expected EVENT, got %q", typ)
121 }
122 event, err := nostr.ParseEvent(parts[1])
123 if err != nil {
124 t.Fatal(err)
125 }
126 f.in <- []byte(`["OK","` + nostr.Hex(event.ID[:]) + `",true,""]`)
127 select {
128 case err := <-pub:
129 if err != nil {
130 t.Fatalf("publish: %v", err)
131 }
132 case <-time.After(2 * time.Second):
133 t.Fatal("publish did not return")
134 }
135 }
136
137 func TestNIP42AuthRejected(t *testing.T) {
138 f := newFakeTransport()
139 var key [32]byte
140 key[31] = 4
141 c, err := New(f, key, Options{RelayURL: "ws://relay.test"})
142 if err != nil {
143 t.Fatal(err)
144 }
145 defer c.Close()
146 f.in <- []byte(`["AUTH","challenge"]`)
147 parts := recv(t, f)
148 ev, err := nostr.ParseEvent(parts[1])
149 if err != nil {
150 t.Fatal(err)
151 }
152 f.in <- []byte(`["OK","` + nostr.Hex(ev.ID[:]) + `",false,"auth-required: nope"]`)
153 time.Sleep(50 * time.Millisecond)
154 if err := c.Send(&nostr.Event{Kind: 1, Content: "x"}); err == nil {
155 t.Fatal("send succeeded after rejected auth")
156 }
157 }
158
159 func TestAuthEvent(t *testing.T) {
160 var key [32]byte
161 key[31] = 5
162 ev := AuthEvent("wss://relay", "c", key)
163 if ev == nil {
164 t.Fatal("no event")
165 }
166 if err := ev.Verify(); err != nil {
167 t.Fatal(err)
168 }
169 if v, _ := ev.Tags.First("relay"); v != "wss://relay" {
170 t.Fatal("relay tag")
171 }
172 }
173