nip43_e2e_test.go raw
1 package app
2
3 import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
9 "os"
10 "testing"
11 "time"
12
13 "next.orly.dev/app/config"
14 "next.orly.dev/pkg/acl"
15 "next.orly.dev/pkg/nostr/crypto/keys"
16 "next.orly.dev/pkg/database"
17 "next.orly.dev/pkg/nostr/encoders/event"
18 "next.orly.dev/pkg/nostr/encoders/hex"
19 "next.orly.dev/pkg/nostr/encoders/tag"
20 "next.orly.dev/pkg/protocol/nip43"
21 "next.orly.dev/pkg/protocol/publish"
22 "next.orly.dev/pkg/nostr/relayinfo"
23 )
24
25 // newTestListener creates a properly initialized Listener for testing
26 func newTestListener(server *Server, ctx context.Context) *Listener {
27 listener := &Listener{
28 Server: server,
29 ctx: ctx,
30 writeChan: make(chan publish.WriteRequest, 100),
31 writeDone: make(chan struct{}),
32 messageQueue: make(chan messageRequest, 100),
33 processingDone: make(chan struct{}),
34 subscriptions: make(map[string]context.CancelFunc),
35 }
36
37 // Start write worker and message processor
38 go listener.writeWorker()
39 go listener.messageProcessor()
40
41 return listener
42 }
43
44 // closeTestListener properly closes a test listener
45 func closeTestListener(listener *Listener) {
46 close(listener.writeChan)
47 <-listener.writeDone
48 close(listener.messageQueue)
49 <-listener.processingDone
50 }
51
52 // setupE2ETest creates a full test server for end-to-end testing
53 func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) {
54 tempDir, err := os.MkdirTemp("", "nip43_e2e_test_*")
55 if err != nil {
56 t.Fatalf("failed to create temp dir: %v", err)
57 }
58
59 ctx, cancel := context.WithCancel(context.Background())
60 db, err := database.New(ctx, cancel, tempDir, "info")
61 if err != nil {
62 os.RemoveAll(tempDir)
63 t.Fatalf("failed to open database: %v", err)
64 }
65
66 cfg := &config.C{
67 AppName: "TestRelay",
68 NIP43Enabled: true,
69 NIP43PublishEvents: true,
70 NIP43PublishMemberList: true,
71 NIP43InviteExpiry: 24 * time.Hour,
72 RelayURL: "wss://test.relay",
73 Listen: "localhost",
74 Port: 3334,
75 ACLMode: "none",
76 AuthRequired: false,
77 }
78
79 // Generate admin keys
80 adminSecret, err := keys.GenerateSecretKey()
81 if err != nil {
82 t.Fatalf("failed to generate admin secret: %v", err)
83 }
84 adminSigner, err := p8k.New()
85 if err != nil {
86 t.Fatalf("failed to create admin signer: %v", err)
87 }
88 if err = adminSigner.InitSec(adminSecret); err != nil {
89 t.Fatalf("failed to initialize admin signer: %v", err)
90 }
91 adminPubkey := adminSigner.Pub()
92
93 // Add admin to config for ACL
94 cfg.Admins = []string{hex.Enc(adminPubkey)}
95
96 server := &Server{
97 Ctx: ctx,
98 Config: cfg,
99 DB: db,
100 publishers: publish.New(NewPublisher(ctx)),
101 Admins: [][]byte{adminPubkey},
102 InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
103 cfg: cfg,
104 db: db,
105 }
106
107 // Configure ACL registry
108 acl.Registry.SetMode(cfg.ACLMode)
109 if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
110 db.Close()
111 os.RemoveAll(tempDir)
112 t.Fatalf("failed to configure ACL: %v", err)
113 }
114
115 server.mux = http.NewServeMux()
116
117 // Set up HTTP handlers
118 server.mux.HandleFunc(
119 "/", func(w http.ResponseWriter, r *http.Request) {
120 if r.Header.Get("Accept") == "application/nostr+json" {
121 server.HandleRelayInfo(w, r)
122 return
123 }
124 http.NotFound(w, r)
125 },
126 )
127
128 httpServer := httptest.NewServer(server.mux)
129
130 cleanup := func() {
131 httpServer.Close()
132 db.Close()
133 os.RemoveAll(tempDir)
134 }
135
136 return server, httpServer, cleanup
137 }
138
139 // TestE2E_RelayInfoIncludesNIP43 tests that NIP-43 is advertised in relay info
140 func TestE2E_RelayInfoIncludesNIP43(t *testing.T) {
141 server, httpServer, cleanup := setupE2ETest(t)
142 defer cleanup()
143
144 // Make request to relay info endpoint
145 req, err := http.NewRequest("GET", httpServer.URL, nil)
146 if err != nil {
147 t.Fatalf("failed to create request: %v", err)
148 }
149 req.Header.Set("Accept", "application/nostr+json")
150
151 resp, err := http.DefaultClient.Do(req)
152 if err != nil {
153 t.Fatalf("failed to make request: %v", err)
154 }
155 defer resp.Body.Close()
156
157 // Parse relay info
158 var info relayinfo.T
159 if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
160 t.Fatalf("failed to decode relay info: %v", err)
161 }
162
163 // Verify NIP-43 is in supported NIPs
164 hasNIP43 := false
165 for _, nip := range info.Nips {
166 if nip == 43 {
167 hasNIP43 = true
168 break
169 }
170 }
171
172 if !hasNIP43 {
173 t.Error("NIP-43 not advertised in supported_nips")
174 }
175
176 // Verify server name
177 if info.Name != server.Config.AppName {
178 t.Errorf(
179 "wrong relay name: got %s, want %s", info.Name,
180 server.Config.AppName,
181 )
182 }
183 }
184
185 // TestE2E_CompleteJoinFlow tests the complete user join flow
186 func TestE2E_CompleteJoinFlow(t *testing.T) {
187 server, _, cleanup := setupE2ETest(t)
188 defer cleanup()
189
190 // Step 1: Admin requests invite code
191 adminPubkey := server.Admins[0]
192 inviteEvent, err := server.HandleNIP43InviteRequest(adminPubkey)
193 if err != nil {
194 t.Fatalf("failed to generate invite: %v", err)
195 }
196
197 // Extract invite code
198 claimTag := inviteEvent.Tags.GetFirst([]byte("claim"))
199 if claimTag == nil || claimTag.Len() < 2 {
200 t.Fatal("invite event missing claim tag")
201 }
202 inviteCode := string(claimTag.T[1])
203
204 // Step 2: User creates join request
205 userSecret, err := keys.GenerateSecretKey()
206 if err != nil {
207 t.Fatalf("failed to generate user secret: %v", err)
208 }
209 userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
210 if err != nil {
211 t.Fatalf("failed to get user pubkey: %v", err)
212 }
213 signer, err := keys.SecretBytesToSigner(userSecret)
214 if err != nil {
215 t.Fatalf("failed to create signer: %v", err)
216 }
217
218 joinEv := event.New()
219 joinEv.Kind = nip43.KindJoinRequest
220 copy(joinEv.Pubkey, userPubkey)
221 joinEv.Tags = tag.NewS()
222 joinEv.Tags.Append(tag.NewFromAny("-"))
223 joinEv.Tags.Append(tag.NewFromAny("claim", inviteCode))
224 joinEv.CreatedAt = time.Now().Unix()
225 joinEv.Content = []byte("")
226 if err = joinEv.Sign(signer); err != nil {
227 t.Fatalf("failed to sign join event: %v", err)
228 }
229
230 // Step 3: Process join request
231 listener := newTestListener(server, server.Ctx)
232 defer closeTestListener(listener)
233 err = listener.HandleNIP43JoinRequest(joinEv)
234 if err != nil {
235 t.Fatalf("failed to handle join request: %v", err)
236 }
237
238 // Step 4: Verify membership
239 isMember, err := server.DB.IsNIP43Member(userPubkey)
240 if err != nil {
241 t.Fatalf("failed to check membership: %v", err)
242 }
243 if !isMember {
244 t.Error("user was not added as member")
245 }
246
247 membership, err := server.DB.GetNIP43Membership(userPubkey)
248 if err != nil {
249 t.Fatalf("failed to get membership: %v", err)
250 }
251 if membership.InviteCode != inviteCode {
252 t.Errorf(
253 "wrong invite code: got %s, want %s", membership.InviteCode,
254 inviteCode,
255 )
256 }
257 }
258
259 // TestE2E_InviteCodeReuse tests that invite codes can only be used once
260 func TestE2E_InviteCodeReuse(t *testing.T) {
261 server, _, cleanup := setupE2ETest(t)
262 defer cleanup()
263
264 // Generate invite code
265 code, err := server.InviteManager.GenerateCode()
266 if err != nil {
267 t.Fatalf("failed to generate invite code: %v", err)
268 }
269
270 listener := newTestListener(server, server.Ctx)
271 defer closeTestListener(listener)
272
273 // First user uses the code
274 user1Secret, err := keys.GenerateSecretKey()
275 if err != nil {
276 t.Fatalf("failed to generate user1 secret: %v", err)
277 }
278 user1Pubkey, err := keys.SecretBytesToPubKeyBytes(user1Secret)
279 if err != nil {
280 t.Fatalf("failed to get user1 pubkey: %v", err)
281 }
282 signer1, err := keys.SecretBytesToSigner(user1Secret)
283 if err != nil {
284 t.Fatalf("failed to create signer1: %v", err)
285 }
286
287 joinEv1 := event.New()
288 joinEv1.Kind = nip43.KindJoinRequest
289 copy(joinEv1.Pubkey, user1Pubkey)
290 joinEv1.Tags = tag.NewS()
291 joinEv1.Tags.Append(tag.NewFromAny("-"))
292 joinEv1.Tags.Append(tag.NewFromAny("claim", code))
293 joinEv1.CreatedAt = time.Now().Unix()
294 joinEv1.Content = []byte("")
295 if err = joinEv1.Sign(signer1); err != nil {
296 t.Fatalf("failed to sign join event 1: %v", err)
297 }
298
299 err = listener.HandleNIP43JoinRequest(joinEv1)
300 if err != nil {
301 t.Fatalf("failed to handle join request 1: %v", err)
302 }
303
304 // Verify first user is member
305 isMember, err := server.DB.IsNIP43Member(user1Pubkey)
306 if err != nil {
307 t.Fatalf("failed to check user1 membership: %v", err)
308 }
309 if !isMember {
310 t.Error("user1 was not added")
311 }
312
313 // Second user tries to use same code
314 user2Secret, err := keys.GenerateSecretKey()
315 if err != nil {
316 t.Fatalf("failed to generate user2 secret: %v", err)
317 }
318 user2Pubkey, err := keys.SecretBytesToPubKeyBytes(user2Secret)
319 if err != nil {
320 t.Fatalf("failed to get user2 pubkey: %v", err)
321 }
322 signer2, err := keys.SecretBytesToSigner(user2Secret)
323 if err != nil {
324 t.Fatalf("failed to create signer2: %v", err)
325 }
326
327 joinEv2 := event.New()
328 joinEv2.Kind = nip43.KindJoinRequest
329 copy(joinEv2.Pubkey, user2Pubkey)
330 joinEv2.Tags = tag.NewS()
331 joinEv2.Tags.Append(tag.NewFromAny("-"))
332 joinEv2.Tags.Append(tag.NewFromAny("claim", code))
333 joinEv2.CreatedAt = time.Now().Unix()
334 joinEv2.Content = []byte("")
335 if err = joinEv2.Sign(signer2); err != nil {
336 t.Fatalf("failed to sign join event 2: %v", err)
337 }
338
339 // Should handle without error but not add user
340 err = listener.HandleNIP43JoinRequest(joinEv2)
341 if err != nil {
342 t.Fatalf("handler returned error: %v", err)
343 }
344
345 // Verify second user is NOT member
346 isMember, err = server.DB.IsNIP43Member(user2Pubkey)
347 if err != nil {
348 t.Fatalf("failed to check user2 membership: %v", err)
349 }
350 if isMember {
351 t.Error("user2 was incorrectly added with reused code")
352 }
353 }
354
355 // TestE2E_MembershipListGeneration tests membership list event generation
356 func TestE2E_MembershipListGeneration(t *testing.T) {
357 server, _, cleanup := setupE2ETest(t)
358 defer cleanup()
359
360 listener := newTestListener(server, server.Ctx)
361 defer closeTestListener(listener)
362
363 // Add multiple members
364 memberCount := 5
365 members := make([][]byte, memberCount)
366
367 for i := 0; i < memberCount; i++ {
368 userSecret, err := keys.GenerateSecretKey()
369 if err != nil {
370 t.Fatalf("failed to generate user secret %d: %v", i, err)
371 }
372 userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
373 if err != nil {
374 t.Fatalf("failed to get user pubkey %d: %v", i, err)
375 }
376 members[i] = userPubkey
377
378 // Add directly to database for speed
379 err = server.DB.AddNIP43Member(userPubkey, "code")
380 if err != nil {
381 t.Fatalf("failed to add member %d: %v", i, err)
382 }
383 }
384
385 // Generate membership list
386 err := listener.publishMembershipList()
387 if err != nil {
388 t.Fatalf("failed to publish membership list: %v", err)
389 }
390
391 // Note: In a real test, you would verify the event was published
392 // through the publishers system. For now, we just verify no error.
393 }
394
395 // TestE2E_ExpiredInviteCode tests that expired codes are rejected
396 func TestE2E_ExpiredInviteCode(t *testing.T) {
397 tempDir, err := os.MkdirTemp("", "nip43_expired_test_*")
398 if err != nil {
399 t.Fatalf("failed to create temp dir: %v", err)
400 }
401 defer os.RemoveAll(tempDir)
402
403 ctx, cancel := context.WithCancel(context.Background())
404 defer cancel()
405
406 db, err := database.New(ctx, cancel, tempDir, "info")
407 if err != nil {
408 t.Fatalf("failed to open database: %v", err)
409 }
410 defer db.Close()
411
412 cfg := &config.C{
413 NIP43Enabled: true,
414 NIP43InviteExpiry: 1 * time.Millisecond, // Very short expiry
415 }
416
417 server := &Server{
418 Ctx: ctx,
419 Config: cfg,
420 DB: db,
421 publishers: publish.New(NewPublisher(ctx)),
422 InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
423 cfg: cfg,
424 db: db,
425 }
426
427 listener := newTestListener(server, ctx)
428 defer closeTestListener(listener)
429
430 // Generate invite code
431 code, err := server.InviteManager.GenerateCode()
432 if err != nil {
433 t.Fatalf("failed to generate invite code: %v", err)
434 }
435
436 // Wait for expiry
437 time.Sleep(10 * time.Millisecond)
438
439 // Try to use expired code
440 userSecret, err := keys.GenerateSecretKey()
441 if err != nil {
442 t.Fatalf("failed to generate user secret: %v", err)
443 }
444 userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
445 if err != nil {
446 t.Fatalf("failed to get user pubkey: %v", err)
447 }
448 signer, err := keys.SecretBytesToSigner(userSecret)
449 if err != nil {
450 t.Fatalf("failed to create signer: %v", err)
451 }
452
453 joinEv := event.New()
454 joinEv.Kind = nip43.KindJoinRequest
455 copy(joinEv.Pubkey, userPubkey)
456 joinEv.Tags = tag.NewS()
457 joinEv.Tags.Append(tag.NewFromAny("-"))
458 joinEv.Tags.Append(tag.NewFromAny("claim", code))
459 joinEv.CreatedAt = time.Now().Unix()
460 joinEv.Content = []byte("")
461 if err = joinEv.Sign(signer); err != nil {
462 t.Fatalf("failed to sign event: %v", err)
463 }
464
465 err = listener.HandleNIP43JoinRequest(joinEv)
466 if err != nil {
467 t.Fatalf("handler returned error: %v", err)
468 }
469
470 // Verify user was NOT added
471 isMember, err := db.IsNIP43Member(userPubkey)
472 if err != nil {
473 t.Fatalf("failed to check membership: %v", err)
474 }
475 if isMember {
476 t.Error("user was added with expired code")
477 }
478 }
479
480 // TestE2E_InvalidTimestampRejected tests that events with invalid timestamps are rejected
481 func TestE2E_InvalidTimestampRejected(t *testing.T) {
482 server, _, cleanup := setupE2ETest(t)
483 defer cleanup()
484
485 listener := newTestListener(server, server.Ctx)
486 defer closeTestListener(listener)
487
488 // Generate invite code
489 code, err := server.InviteManager.GenerateCode()
490 if err != nil {
491 t.Fatalf("failed to generate invite code: %v", err)
492 }
493
494 // Create user
495 userSecret, err := keys.GenerateSecretKey()
496 if err != nil {
497 t.Fatalf("failed to generate user secret: %v", err)
498 }
499 userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
500 if err != nil {
501 t.Fatalf("failed to get user pubkey: %v", err)
502 }
503 signer, err := keys.SecretBytesToSigner(userSecret)
504 if err != nil {
505 t.Fatalf("failed to create signer: %v", err)
506 }
507
508 // Create join request with timestamp far in the past
509 joinEv := event.New()
510 joinEv.Kind = nip43.KindJoinRequest
511 copy(joinEv.Pubkey, userPubkey)
512 joinEv.Tags = tag.NewS()
513 joinEv.Tags.Append(tag.NewFromAny("-"))
514 joinEv.Tags.Append(tag.NewFromAny("claim", code))
515 joinEv.CreatedAt = time.Now().Unix() - 700 // More than 10 minutes ago
516 joinEv.Content = []byte("")
517 if err = joinEv.Sign(signer); err != nil {
518 t.Fatalf("failed to sign event: %v", err)
519 }
520
521 // Should handle without error but not add user
522 err = listener.HandleNIP43JoinRequest(joinEv)
523 if err != nil {
524 t.Fatalf("handler returned error: %v", err)
525 }
526
527 // Verify user was NOT added
528 isMember, err := server.DB.IsNIP43Member(userPubkey)
529 if err != nil {
530 t.Fatalf("failed to check membership: %v", err)
531 }
532 if isMember {
533 t.Error("user was added with invalid timestamp")
534 }
535 }
536
537 // BenchmarkJoinRequestProcessing benchmarks join request processing
538 func BenchmarkJoinRequestProcessing(b *testing.B) {
539 tempDir, err := os.MkdirTemp("", "nip43_bench_*")
540 if err != nil {
541 b.Fatalf("failed to create temp dir: %v", err)
542 }
543 defer os.RemoveAll(tempDir)
544
545 ctx, cancel := context.WithCancel(context.Background())
546 defer cancel()
547
548 db, err := database.New(ctx, cancel, tempDir, "error")
549 if err != nil {
550 b.Fatalf("failed to open database: %v", err)
551 }
552 defer db.Close()
553
554 cfg := &config.C{
555 NIP43Enabled: true,
556 NIP43InviteExpiry: 24 * time.Hour,
557 }
558
559 server := &Server{
560 Ctx: ctx,
561 Config: cfg,
562 DB: db,
563 publishers: publish.New(NewPublisher(ctx)),
564 InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
565 cfg: cfg,
566 db: db,
567 }
568
569 listener := newTestListener(server, ctx)
570 defer closeTestListener(listener)
571
572 b.ResetTimer()
573
574 for i := 0; i < b.N; i++ {
575 // Generate unique user and code for each iteration
576 userSecret, _ := keys.GenerateSecretKey()
577 userPubkey, _ := keys.SecretBytesToPubKeyBytes(userSecret)
578 signer, _ := keys.SecretBytesToSigner(userSecret)
579 code, _ := server.InviteManager.GenerateCode()
580
581 joinEv := event.New()
582 joinEv.Kind = nip43.KindJoinRequest
583 copy(joinEv.Pubkey, userPubkey)
584 joinEv.Tags = tag.NewS()
585 joinEv.Tags.Append(tag.NewFromAny("-"))
586 joinEv.Tags.Append(tag.NewFromAny("claim", code))
587 joinEv.CreatedAt = time.Now().Unix()
588 joinEv.Content = []byte("")
589 joinEv.Sign(signer)
590
591 listener.HandleNIP43JoinRequest(joinEv)
592 }
593 }
594