summaryrefslogtreecommitdiffstats
path: root/internal/nostr/relay_test.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-13 17:35:32 -0800
committerbndw <ben@bdw.to>2026-02-13 17:35:32 -0800
commit581ceecbf046f99b39885c74e2780a5320e5b15e (patch)
treec82dcaddb4f555d5051684221881e36f7fe3f718 /internal/nostr/relay_test.go
parent06b9b13274825f797523935494a1b5225f0e0862 (diff)
feat: add Nostr protocol implementation (internal/nostr, internal/websocket)
Diffstat (limited to 'internal/nostr/relay_test.go')
-rw-r--r--internal/nostr/relay_test.go326
1 files changed, 326 insertions, 0 deletions
diff --git a/internal/nostr/relay_test.go b/internal/nostr/relay_test.go
new file mode 100644
index 0000000..02bd8e5
--- /dev/null
+++ b/internal/nostr/relay_test.go
@@ -0,0 +1,326 @@
1package nostr
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 "time"
11
12 "northwest.io/nostr-grpc/internal/websocket"
13)
14
15// mockRelay creates a test WebSocket server that echoes messages
16func mockRelay(t *testing.T, handler func(conn *websocket.Conn)) *httptest.Server {
17 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 conn, err := websocket.Accept(w, r)
19 if err != nil {
20 t.Logf("Failed to accept WebSocket: %v", err)
21 return
22 }
23 defer conn.Close(websocket.StatusNormalClosure, "")
24
25 handler(conn)
26 }))
27}
28
29func TestConnect(t *testing.T) {
30 server := mockRelay(t, func(conn *websocket.Conn) {
31 // Just accept and wait
32 time.Sleep(100 * time.Millisecond)
33 })
34 defer server.Close()
35
36 url := "ws" + strings.TrimPrefix(server.URL, "http")
37 ctx := context.Background()
38
39 relay, err := Connect(ctx, url)
40 if err != nil {
41 t.Fatalf("Connect() error = %v", err)
42 }
43 defer relay.Close()
44
45 if relay.URL != url {
46 t.Errorf("Relay.URL = %s, want %s", relay.URL, url)
47 }
48}
49
50func TestConnectError(t *testing.T) {
51 ctx := context.Background()
52 _, err := Connect(ctx, "ws://localhost:99999")
53 if err == nil {
54 t.Error("Connect() expected error for invalid URL")
55 }
56}
57
58func TestRelaySendReceive(t *testing.T) {
59 server := mockRelay(t, func(conn *websocket.Conn) {
60 // Read message
61 _, data, err := conn.Read(context.Background())
62 if err != nil {
63 t.Logf("Read error: %v", err)
64 return
65 }
66
67 // Echo it back as NOTICE
68 var arr []interface{}
69 json.Unmarshal(data, &arr)
70
71 response, _ := json.Marshal([]interface{}{"NOTICE", "received: " + arr[0].(string)})
72 conn.Write(context.Background(), websocket.MessageText, response)
73 })
74 defer server.Close()
75
76 url := "ws" + strings.TrimPrefix(server.URL, "http")
77 ctx := context.Background()
78
79 // Create relay without auto-Listen to test Send/Receive directly
80 conn, err := websocket.Dial(ctx, url)
81 if err != nil {
82 t.Fatalf("Dial() error = %v", err)
83 }
84 relay := &Relay{
85 URL: url,
86 conn: conn,
87 subscriptions: make(map[string]*Subscription),
88 okChannels: make(map[string]chan *OKEnvelope),
89 }
90 defer relay.Close()
91
92 // Send a CLOSE envelope
93 closeEnv := CloseEnvelope{SubscriptionID: "test"}
94 if err := relay.Send(ctx, closeEnv); err != nil {
95 t.Fatalf("Send() error = %v", err)
96 }
97
98 // Receive response
99 env, err := relay.Receive(ctx)
100 if err != nil {
101 t.Fatalf("Receive() error = %v", err)
102 }
103
104 noticeEnv, ok := env.(*NoticeEnvelope)
105 if !ok {
106 t.Fatalf("Expected *NoticeEnvelope, got %T", env)
107 }
108
109 if !strings.Contains(noticeEnv.Message, "CLOSE") {
110 t.Errorf("Message = %s, want to contain 'CLOSE'", noticeEnv.Message)
111 }
112}
113
114func TestRelayPublish(t *testing.T) {
115 server := mockRelay(t, func(conn *websocket.Conn) {
116 // Read the EVENT message
117 _, data, err := conn.Read(context.Background())
118 if err != nil {
119 t.Logf("Read error: %v", err)
120 return
121 }
122
123 // Parse to get event ID
124 var arr []json.RawMessage
125 json.Unmarshal(data, &arr)
126
127 var event Event
128 json.Unmarshal(arr[1], &event)
129
130 // Send OK response
131 response, _ := json.Marshal([]interface{}{"OK", event.ID, true, ""})
132 conn.Write(context.Background(), websocket.MessageText, response)
133 })
134 defer server.Close()
135
136 url := "ws" + strings.TrimPrefix(server.URL, "http")
137 ctx := context.Background()
138
139 relay, err := Connect(ctx, url)
140 if err != nil {
141 t.Fatalf("Connect() error = %v", err)
142 }
143 defer relay.Close()
144
145 // Create and sign event
146 key, _ := GenerateKey()
147 event := &Event{
148 CreatedAt: time.Now().Unix(),
149 Kind: KindTextNote,
150 Tags: Tags{},
151 Content: "Test publish",
152 }
153 key.Sign(event)
154
155 // Publish
156 if err := relay.Publish(ctx, event); err != nil {
157 t.Fatalf("Publish() error = %v", err)
158 }
159}
160
161func TestRelayPublishRejected(t *testing.T) {
162 server := mockRelay(t, func(conn *websocket.Conn) {
163 // Read the EVENT message
164 _, data, err := conn.Read(context.Background())
165 if err != nil {
166 return
167 }
168
169 var arr []json.RawMessage
170 json.Unmarshal(data, &arr)
171
172 var event Event
173 json.Unmarshal(arr[1], &event)
174
175 // Send rejection
176 response, _ := json.Marshal([]interface{}{"OK", event.ID, false, "blocked: spam"})
177 conn.Write(context.Background(), websocket.MessageText, response)
178 })
179 defer server.Close()
180
181 url := "ws" + strings.TrimPrefix(server.URL, "http")
182 ctx := context.Background()
183
184 relay, err := Connect(ctx, url)
185 if err != nil {
186 t.Fatalf("Connect() error = %v", err)
187 }
188 defer relay.Close()
189
190 key, _ := GenerateKey()
191 event := &Event{
192 CreatedAt: time.Now().Unix(),
193 Kind: KindTextNote,
194 Tags: Tags{},
195 Content: "Test",
196 }
197 key.Sign(event)
198
199 err = relay.Publish(ctx, event)
200 if err == nil {
201 t.Error("Publish() expected error for rejected event")
202 }
203 if !strings.Contains(err.Error(), "rejected") {
204 t.Errorf("Error = %v, want to contain 'rejected'", err)
205 }
206}
207
208func TestRelaySubscribe(t *testing.T) {
209 server := mockRelay(t, func(conn *websocket.Conn) {
210 // Read REQ
211 _, data, err := conn.Read(context.Background())
212 if err != nil {
213 return
214 }
215
216 var arr []json.RawMessage
217 json.Unmarshal(data, &arr)
218
219 var subID string
220 json.Unmarshal(arr[1], &subID)
221
222 // Send some events
223 for i := 0; i < 3; i++ {
224 event := Event{
225 ID: "event" + string(rune('0'+i)),
226 PubKey: "pubkey",
227 CreatedAt: time.Now().Unix(),
228 Kind: 1,
229 Tags: Tags{},
230 Content: "Test event",
231 Sig: "sig",
232 }
233 response, _ := json.Marshal([]interface{}{"EVENT", subID, event})
234 conn.Write(context.Background(), websocket.MessageText, response)
235 }
236
237 // Send EOSE
238 eose, _ := json.Marshal([]interface{}{"EOSE", subID})
239 conn.Write(context.Background(), websocket.MessageText, eose)
240 })
241 defer server.Close()
242
243 url := "ws" + strings.TrimPrefix(server.URL, "http")
244 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
245 defer cancel()
246
247 relay, err := Connect(ctx, url)
248 if err != nil {
249 t.Fatalf("Connect() error = %v", err)
250 }
251 defer relay.Close()
252
253 sub := relay.Fetch(ctx, Filter{Kinds: []int{1}})
254
255 eventCount := 0
256 for range sub.Events {
257 eventCount++
258 }
259
260 if eventCount != 3 {
261 t.Errorf("Received %d events, want 3", eventCount)
262 }
263 if sub.Err != nil {
264 t.Errorf("Subscription.Err = %v, want nil", sub.Err)
265 }
266}
267
268func TestRelayClose(t *testing.T) {
269 server := mockRelay(t, func(conn *websocket.Conn) {
270 time.Sleep(100 * time.Millisecond)
271 })
272 defer server.Close()
273
274 url := "ws" + strings.TrimPrefix(server.URL, "http")
275 ctx := context.Background()
276
277 relay, err := Connect(ctx, url)
278 if err != nil {
279 t.Fatalf("Connect() error = %v", err)
280 }
281
282 if err := relay.Close(); err != nil {
283 t.Errorf("Close() error = %v", err)
284 }
285
286 // Second close should be safe
287 if err := relay.Close(); err != nil {
288 t.Errorf("Second Close() error = %v", err)
289 }
290}
291
292func TestSubscriptionClose(t *testing.T) {
293 server := mockRelay(t, func(conn *websocket.Conn) {
294 // Read REQ
295 conn.Read(context.Background())
296
297 // Wait for CLOSE
298 _, data, err := conn.Read(context.Background())
299 if err != nil {
300 return
301 }
302
303 var arr []interface{}
304 json.Unmarshal(data, &arr)
305
306 if arr[0] != "CLOSE" {
307 t.Errorf("Expected CLOSE, got %v", arr[0])
308 }
309 })
310 defer server.Close()
311
312 url := "ws" + strings.TrimPrefix(server.URL, "http")
313 ctx := context.Background()
314
315 relay, err := Connect(ctx, url)
316 if err != nil {
317 t.Fatalf("Connect() error = %v", err)
318 }
319 defer relay.Close()
320
321 sub := relay.Subscribe(ctx, Filter{Kinds: []int{1}})
322
323 if err := sub.Close(ctx); err != nil {
324 t.Errorf("Subscription.Close() error = %v", err)
325 }
326}