nrc_test.go raw
1 package nrc
2
3 import (
4 "testing"
5 "time"
6
7 "next.orly.dev/pkg/nostr/crypto/keys"
8 "next.orly.dev/pkg/nostr/encoders/hex"
9 )
10
11 // Test keys - generated from known secrets for reproducibility
12 var (
13 // From secret: 0000000000000000000000000000000000000000000000000000000000000001
14 testRelaySecret = "0000000000000000000000000000000000000000000000000000000000000001"
15 // From secret: 0000000000000000000000000000000000000000000000000000000000000002
16 testClientSecret = "0000000000000000000000000000000000000000000000000000000000000002"
17 )
18
19 // getTestRelayPubkey returns the pubkey derived from testRelaySecret
20 func getTestRelayPubkey(t *testing.T) []byte {
21 secretBytes, err := hex.Dec(testRelaySecret)
22 if err != nil {
23 t.Fatalf("failed to decode test secret: %v", err)
24 }
25 pubkey, err := keys.SecretBytesToPubKeyBytes(secretBytes)
26 if err != nil {
27 t.Fatalf("failed to derive pubkey: %v", err)
28 }
29 return pubkey
30 }
31
32 // getTestRelayPubkeyHex returns the hex-encoded pubkey
33 func getTestRelayPubkeyHex(t *testing.T) string {
34 return string(hex.Enc(getTestRelayPubkey(t)))
35 }
36
37 func TestParseConnectionURI(t *testing.T) {
38 // Get valid pubkey for tests
39 relayPubkeyHex := getTestRelayPubkeyHex(t)
40
41 tests := []struct {
42 name string
43 uri string
44 wantErr bool
45 check func(*testing.T, *ConnectionURI)
46 }{
47 {
48 name: "invalid scheme",
49 uri: "nostr+wallet://abc123",
50 wantErr: true,
51 },
52 {
53 name: "missing relay parameter",
54 uri: "nostr+relayconnect://" + relayPubkeyHex,
55 wantErr: true,
56 },
57 {
58 name: "missing secret parameter",
59 uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com",
60 wantErr: true,
61 },
62 {
63 name: "invalid relay pubkey",
64 uri: "nostr+relayconnect://invalid?relay=wss://relay.example.com&secret=" + testClientSecret,
65 wantErr: true,
66 },
67 {
68 name: "valid secret-based URI",
69 uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret,
70 check: func(t *testing.T, conn *ConnectionURI) {
71 if conn.AuthMode != AuthModeSecret {
72 t.Errorf("expected AuthModeSecret, got %d", conn.AuthMode)
73 }
74 if conn.RendezvousRelay != "wss://relay.example.com" {
75 t.Errorf("expected wss://relay.example.com, got %s", conn.RendezvousRelay)
76 }
77 if conn.GetClientSigner() == nil {
78 t.Error("expected client signer to be set")
79 }
80 if len(conn.GetConversationKey()) != 32 {
81 t.Errorf("expected 32-byte conversation key, got %d", len(conn.GetConversationKey()))
82 }
83 },
84 },
85 {
86 name: "valid URI with device name",
87 uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret + "&name=phone",
88 check: func(t *testing.T, conn *ConnectionURI) {
89 if conn.DeviceName != "phone" {
90 t.Errorf("expected device name 'phone', got '%s'", conn.DeviceName)
91 }
92 },
93 },
94 }
95
96 for _, tt := range tests {
97 t.Run(tt.name, func(t *testing.T) {
98 conn, err := ParseConnectionURI(tt.uri)
99 if (err != nil) != tt.wantErr {
100 t.Errorf("ParseConnectionURI() error = %v, wantErr %v", err, tt.wantErr)
101 return
102 }
103 if tt.check != nil && err == nil {
104 tt.check(t, conn)
105 }
106 })
107 }
108 }
109
110 func TestGenerateConnectionURI(t *testing.T) {
111 relayPubkey := getTestRelayPubkey(t)
112 rendezvousRelay := "wss://relay.example.com"
113
114 uri, secret, err := GenerateConnectionURI(relayPubkey, rendezvousRelay, "test-device")
115 if err != nil {
116 t.Fatalf("GenerateConnectionURI() error = %v", err)
117 }
118
119 if len(secret) != 32 {
120 t.Errorf("expected 32-byte secret, got %d", len(secret))
121 }
122
123 // Parse the generated URI to verify it's valid
124 conn, err := ParseConnectionURI(uri)
125 if err != nil {
126 t.Fatalf("failed to parse generated URI: %v", err)
127 }
128
129 if conn.DeviceName != "test-device" {
130 t.Errorf("expected device name 'test-device', got '%s'", conn.DeviceName)
131 }
132
133 if conn.RendezvousRelay != rendezvousRelay {
134 t.Errorf("expected rendezvous relay %s, got %s", rendezvousRelay, conn.RendezvousRelay)
135 }
136 }
137
138 func TestSession(t *testing.T) {
139 clientPubkey := make([]byte, 32)
140 conversationKey := make([]byte, 32)
141
142 session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
143 if session == nil {
144 t.Fatal("NewSession returned nil")
145 }
146
147 // Test basic properties
148 if session.ID != "test-session" {
149 t.Errorf("expected ID 'test-session', got '%s'", session.ID)
150 }
151 if session.DeviceName != "test-device" {
152 t.Errorf("expected device name 'test-device', got '%s'", session.DeviceName)
153 }
154 if session.AuthMode != AuthModeSecret {
155 t.Errorf("expected AuthModeSecret, got %d", session.AuthMode)
156 }
157
158 // Test subscription management
159 if err := session.AddSubscription("sub1"); err != nil {
160 t.Errorf("AddSubscription() error = %v", err)
161 }
162 if !session.HasSubscription("sub1") {
163 t.Error("expected subscription 'sub1' to exist")
164 }
165 if session.SubscriptionCount() != 1 {
166 t.Errorf("expected 1 subscription, got %d", session.SubscriptionCount())
167 }
168
169 session.RemoveSubscription("sub1")
170 if session.HasSubscription("sub1") {
171 t.Error("expected subscription 'sub1' to be removed")
172 }
173
174 // Test expiry
175 if session.IsExpired(time.Hour) {
176 t.Error("session should not be expired")
177 }
178
179 // Test close
180 session.Close()
181 select {
182 case <-session.Context().Done():
183 // Expected
184 default:
185 t.Error("expected session context to be cancelled after Close()")
186 }
187 }
188
189 func TestSessionManager(t *testing.T) {
190 manager := NewSessionManager(time.Minute)
191 if manager == nil {
192 t.Fatal("NewSessionManager returned nil")
193 }
194
195 clientPubkey := make([]byte, 32)
196 conversationKey := make([]byte, 32)
197
198 // Test GetOrCreate
199 session := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
200 if session == nil {
201 t.Fatal("GetOrCreate returned nil")
202 }
203
204 // Get same session again
205 session2 := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
206 if session2 != session {
207 t.Error("expected GetOrCreate to return same session")
208 }
209
210 // Test Get
211 retrieved := manager.Get("session1")
212 if retrieved != session {
213 t.Error("expected Get to return the session")
214 }
215
216 // Test Count
217 if manager.Count() != 1 {
218 t.Errorf("expected count 1, got %d", manager.Count())
219 }
220
221 // Test Remove
222 manager.Remove("session1")
223 if manager.Get("session1") != nil {
224 t.Error("expected session to be removed")
225 }
226 if manager.Count() != 0 {
227 t.Errorf("expected count 0 after removal, got %d", manager.Count())
228 }
229
230 // Test Close
231 manager.GetOrCreate("session2", clientPubkey, conversationKey, AuthModeSecret, "device2")
232 manager.Close()
233 if manager.Count() != 0 {
234 t.Errorf("expected count 0 after Close, got %d", manager.Count())
235 }
236 }
237
238 func TestParseRequestContent(t *testing.T) {
239 tests := []struct {
240 name string
241 content string
242 wantErr bool
243 check func(*testing.T, *RequestMessage)
244 }{
245 {
246 name: "empty content",
247 content: "",
248 wantErr: true,
249 },
250 {
251 name: "invalid JSON",
252 content: "not json",
253 wantErr: true,
254 },
255 {
256 name: "missing type",
257 content: `{"payload": []}`,
258 wantErr: true,
259 },
260 {
261 name: "valid EVENT request",
262 content: `{"type": "EVENT", "payload": ["EVENT", {}]}`,
263 check: func(t *testing.T, msg *RequestMessage) {
264 if msg.Type != "EVENT" {
265 t.Errorf("expected type EVENT, got %s", msg.Type)
266 }
267 },
268 },
269 {
270 name: "valid REQ request",
271 content: `{"type": "REQ", "payload": ["REQ", "sub1", {}]}`,
272 check: func(t *testing.T, msg *RequestMessage) {
273 if msg.Type != "REQ" {
274 t.Errorf("expected type REQ, got %s", msg.Type)
275 }
276 },
277 },
278 }
279
280 for _, tt := range tests {
281 t.Run(tt.name, func(t *testing.T) {
282 msg, err := ParseRequestContent([]byte(tt.content))
283 if (err != nil) != tt.wantErr {
284 t.Errorf("ParseRequestContent() error = %v, wantErr %v", err, tt.wantErr)
285 return
286 }
287 if tt.check != nil && err == nil {
288 tt.check(t, msg)
289 }
290 })
291 }
292 }
293
294 func TestMarshalResponseContent(t *testing.T) {
295 resp := &ResponseMessage{
296 Type: "OK",
297 Payload: []any{"OK", "eventid123", true, ""},
298 }
299
300 content, err := MarshalResponseContent(resp)
301 if err != nil {
302 t.Fatalf("MarshalResponseContent() error = %v", err)
303 }
304
305 // Verify it's valid JSON that can be parsed back
306 parsed, err := ParseRequestContent(content)
307 if err != nil {
308 t.Fatalf("failed to parse marshaled response: %v", err)
309 }
310
311 if parsed.Type != "OK" {
312 t.Errorf("expected type OK, got %s", parsed.Type)
313 }
314 }
315
316 func TestBridgeConfig(t *testing.T) {
317 config := &BridgeConfig{
318 RendezvousURL: "wss://relay.example.com",
319 LocalRelayURL: "ws://localhost:3334",
320 AuthorizedSecrets: map[string]string{"pubkey1": "device1"},
321 SessionTimeout: time.Minute,
322 }
323
324 bridge := NewBridge(config)
325 if bridge == nil {
326 t.Fatal("NewBridge returned nil")
327 }
328
329 // Bridge shouldn't start without a valid rendezvous relay
330 // but we can verify it was created
331 bridge.Stop()
332 }
333
334 func TestSubscriptionTooMany(t *testing.T) {
335 clientPubkey := make([]byte, 32)
336 conversationKey := make([]byte, 32)
337
338 session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
339
340 // Add max subscriptions
341 for i := 0; i < DefaultMaxSubscriptions; i++ {
342 if err := session.AddSubscription(string(rune(i))); err != nil {
343 t.Fatalf("AddSubscription() error = %v at iteration %d", err, i)
344 }
345 }
346
347 // Next one should fail
348 err := session.AddSubscription("overflow")
349 if err != ErrTooManySubscriptions {
350 t.Errorf("expected ErrTooManySubscriptions, got %v", err)
351 }
352
353 session.Close()
354 }
355