handle-nip43_test.go raw
1 package app
2
3 import (
4 "context"
5 "os"
6 "testing"
7 "time"
8
9 "next.orly.dev/app/config"
10 "next.orly.dev/pkg/acl"
11 "next.orly.dev/pkg/nostr/crypto/keys"
12 "next.orly.dev/pkg/database"
13 "next.orly.dev/pkg/nostr/encoders/event"
14 "next.orly.dev/pkg/nostr/encoders/hex"
15 "next.orly.dev/pkg/nostr/encoders/tag"
16 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
17 "next.orly.dev/pkg/protocol/nip43"
18 "next.orly.dev/pkg/protocol/publish"
19 )
20
21 // setupTestListener creates a test listener with NIP-43 enabled
22 func setupTestListener(t *testing.T) (*Listener, *database.D, func()) {
23 tempDir, err := os.MkdirTemp("", "nip43_handler_test_*")
24 if err != nil {
25 t.Fatalf("failed to create temp dir: %v", err)
26 }
27
28 ctx, cancel := context.WithCancel(context.Background())
29 db, err := database.New(ctx, cancel, tempDir, "info")
30 if err != nil {
31 os.RemoveAll(tempDir)
32 t.Fatalf("failed to open database: %v", err)
33 }
34
35 cfg := &config.C{
36 NIP43Enabled: true,
37 NIP43PublishEvents: true,
38 NIP43PublishMemberList: true,
39 NIP43InviteExpiry: 24 * time.Hour,
40 RelayURL: "wss://test.relay",
41 Listen: "localhost",
42 Port: 3334,
43 ACLMode: "none",
44 }
45
46 server := &Server{
47 Ctx: ctx,
48 Config: cfg,
49 DB: db,
50 publishers: publish.New(NewPublisher(ctx)),
51 InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
52 cfg: cfg,
53 db: db,
54 }
55
56 // Configure ACL registry
57 acl.Registry.SetMode(cfg.ACLMode)
58 if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
59 db.Close()
60 os.RemoveAll(tempDir)
61 t.Fatalf("failed to configure ACL: %v", err)
62 }
63
64 listener := &Listener{
65 Server: server,
66 ctx: ctx,
67 writeChan: make(chan publish.WriteRequest, 100),
68 writeDone: make(chan struct{}),
69 messageQueue: make(chan messageRequest, 100),
70 processingDone: make(chan struct{}),
71 subscriptions: make(map[string]context.CancelFunc),
72 }
73
74 // Start write worker and message processor
75 go listener.writeWorker()
76 go listener.messageProcessor()
77
78 cleanup := func() {
79 // Close listener channels
80 close(listener.writeChan)
81 <-listener.writeDone
82 close(listener.messageQueue)
83 <-listener.processingDone
84 db.Close()
85 os.RemoveAll(tempDir)
86 }
87
88 return listener, db, cleanup
89 }
90
91 // TestHandleNIP43JoinRequest_ValidRequest tests a successful join request
92 func TestHandleNIP43JoinRequest_ValidRequest(t *testing.T) {
93 listener, db, cleanup := setupTestListener(t)
94 defer cleanup()
95
96 // Generate test user
97 userSecret, err := keys.GenerateSecretKey()
98 if err != nil {
99 t.Fatalf("failed to generate user secret: %v", err)
100 }
101 userSigner, err := p8k.New()
102 if err != nil {
103 t.Fatalf("failed to create signer: %v", err)
104 }
105 if err = userSigner.InitSec(userSecret); err != nil {
106 t.Fatalf("failed to initialize signer: %v", err)
107 }
108 userPubkey := userSigner.Pub()
109
110 // Generate invite code
111 code, err := listener.Server.InviteManager.GenerateCode()
112 if err != nil {
113 t.Fatalf("failed to generate invite code: %v", err)
114 }
115
116 // Create join request event
117 ev := event.New()
118 ev.Kind = nip43.KindJoinRequest
119 copy(ev.Pubkey, userPubkey)
120 ev.Tags = tag.NewS()
121 ev.Tags.Append(tag.NewFromAny("-"))
122 ev.Tags.Append(tag.NewFromAny("claim", code))
123 ev.CreatedAt = time.Now().Unix()
124 ev.Content = []byte("")
125
126 // Sign event
127 if err = ev.Sign(userSigner); err != nil {
128 t.Fatalf("failed to sign event: %v", err)
129 }
130
131 // Handle join request
132 err = listener.HandleNIP43JoinRequest(ev)
133 if err != nil {
134 t.Fatalf("failed to handle join request: %v", err)
135 }
136
137 // Verify user was added to database
138 isMember, err := db.IsNIP43Member(userPubkey)
139 if err != nil {
140 t.Fatalf("failed to check membership: %v", err)
141 }
142 if !isMember {
143 t.Error("user was not added as member")
144 }
145
146 // Verify membership details
147 membership, err := db.GetNIP43Membership(userPubkey)
148 if err != nil {
149 t.Fatalf("failed to get membership: %v", err)
150 }
151 if membership.InviteCode != code {
152 t.Errorf("wrong invite code stored: got %s, want %s", membership.InviteCode, code)
153 }
154 }
155
156 // TestHandleNIP43JoinRequest_InvalidCode tests join request with invalid code
157 func TestHandleNIP43JoinRequest_InvalidCode(t *testing.T) {
158 listener, db, cleanup := setupTestListener(t)
159 defer cleanup()
160
161 // Generate test user
162 userSecret, err := keys.GenerateSecretKey()
163 if err != nil {
164 t.Fatalf("failed to generate user secret: %v", err)
165 }
166 userSigner, err := p8k.New()
167 if err != nil {
168 t.Fatalf("failed to create signer: %v", err)
169 }
170 if err = userSigner.InitSec(userSecret); err != nil {
171 t.Fatalf("failed to initialize signer: %v", err)
172 }
173 userPubkey := userSigner.Pub()
174
175 // Create join request with invalid code
176 ev := event.New()
177 ev.Kind = nip43.KindJoinRequest
178 copy(ev.Pubkey, userPubkey)
179 ev.Tags = tag.NewS()
180 ev.Tags.Append(tag.NewFromAny("-"))
181 ev.Tags.Append(tag.NewFromAny("claim", "invalid-code-123"))
182 ev.CreatedAt = time.Now().Unix()
183 ev.Content = []byte("")
184
185 if err = ev.Sign(userSigner); err != nil {
186 t.Fatalf("failed to sign event: %v", err)
187 }
188
189 // Handle join request - should succeed but not add member
190 err = listener.HandleNIP43JoinRequest(ev)
191 if err != nil {
192 t.Fatalf("handler returned error: %v", err)
193 }
194
195 // Verify user was NOT added
196 isMember, err := db.IsNIP43Member(userPubkey)
197 if err != nil {
198 t.Fatalf("failed to check membership: %v", err)
199 }
200 if isMember {
201 t.Error("user was incorrectly added as member with invalid code")
202 }
203 }
204
205 // TestHandleNIP43JoinRequest_DuplicateMember tests join request from existing member
206 func TestHandleNIP43JoinRequest_DuplicateMember(t *testing.T) {
207 listener, db, cleanup := setupTestListener(t)
208 defer cleanup()
209
210 // Generate test user
211 userSecret, err := keys.GenerateSecretKey()
212 if err != nil {
213 t.Fatalf("failed to generate user secret: %v", err)
214 }
215 userSigner, err := p8k.New()
216 if err != nil {
217 t.Fatalf("failed to create signer: %v", err)
218 }
219 if err = userSigner.InitSec(userSecret); err != nil {
220 t.Fatalf("failed to initialize signer: %v", err)
221 }
222 userPubkey := userSigner.Pub()
223
224 // Add user directly to database
225 err = db.AddNIP43Member(userPubkey, "original-code")
226 if err != nil {
227 t.Fatalf("failed to add member: %v", err)
228 }
229
230 // Generate new invite code
231 code, err := listener.Server.InviteManager.GenerateCode()
232 if err != nil {
233 t.Fatalf("failed to generate invite code: %v", err)
234 }
235
236 // Create join request
237 ev := event.New()
238 ev.Kind = nip43.KindJoinRequest
239 copy(ev.Pubkey, userPubkey)
240 ev.Tags = tag.NewS()
241 ev.Tags.Append(tag.NewFromAny("-"))
242 ev.Tags.Append(tag.NewFromAny("claim", code))
243 ev.CreatedAt = time.Now().Unix()
244 ev.Content = []byte("")
245
246 if err = ev.Sign(userSigner); err != nil {
247 t.Fatalf("failed to sign event: %v", err)
248 }
249
250 // Handle join request - should handle gracefully
251 err = listener.HandleNIP43JoinRequest(ev)
252 if err != nil {
253 t.Fatalf("handler returned error: %v", err)
254 }
255
256 // Verify original membership is unchanged
257 membership, err := db.GetNIP43Membership(userPubkey)
258 if err != nil {
259 t.Fatalf("failed to get membership: %v", err)
260 }
261 if membership.InviteCode != "original-code" {
262 t.Errorf("invite code was changed: got %s, want original-code", membership.InviteCode)
263 }
264 }
265
266 // TestHandleNIP43LeaveRequest_ValidRequest tests a successful leave request
267 func TestHandleNIP43LeaveRequest_ValidRequest(t *testing.T) {
268 listener, db, cleanup := setupTestListener(t)
269 defer cleanup()
270
271 // Generate test user
272 userSecret, err := keys.GenerateSecretKey()
273 if err != nil {
274 t.Fatalf("failed to generate user secret: %v", err)
275 }
276 userSigner, err := p8k.New()
277 if err != nil {
278 t.Fatalf("failed to create signer: %v", err)
279 }
280 if err = userSigner.InitSec(userSecret); err != nil {
281 t.Fatalf("failed to initialize signer: %v", err)
282 }
283 userPubkey := userSigner.Pub()
284
285 // Add user as member
286 err = db.AddNIP43Member(userPubkey, "test-code")
287 if err != nil {
288 t.Fatalf("failed to add member: %v", err)
289 }
290
291 // Create leave request
292 ev := event.New()
293 ev.Kind = nip43.KindLeaveRequest
294 copy(ev.Pubkey, userPubkey)
295 ev.Tags = tag.NewS()
296 ev.Tags.Append(tag.NewFromAny("-"))
297 ev.CreatedAt = time.Now().Unix()
298 ev.Content = []byte("")
299
300 if err = ev.Sign(userSigner); err != nil {
301 t.Fatalf("failed to sign event: %v", err)
302 }
303
304 // Handle leave request
305 err = listener.HandleNIP43LeaveRequest(ev)
306 if err != nil {
307 t.Fatalf("failed to handle leave request: %v", err)
308 }
309
310 // Verify user was removed
311 isMember, err := db.IsNIP43Member(userPubkey)
312 if err != nil {
313 t.Fatalf("failed to check membership: %v", err)
314 }
315 if isMember {
316 t.Error("user was not removed")
317 }
318 }
319
320 // TestHandleNIP43LeaveRequest_NonMember tests leave request from non-member
321 func TestHandleNIP43LeaveRequest_NonMember(t *testing.T) {
322 listener, _, cleanup := setupTestListener(t)
323 defer cleanup()
324
325 // Generate test user (not a member)
326 userSecret, err := keys.GenerateSecretKey()
327 if err != nil {
328 t.Fatalf("failed to generate user secret: %v", err)
329 }
330 userSigner, err := p8k.New()
331 if err != nil {
332 t.Fatalf("failed to create signer: %v", err)
333 }
334 if err = userSigner.InitSec(userSecret); err != nil {
335 t.Fatalf("failed to initialize signer: %v", err)
336 }
337 userPubkey := userSigner.Pub()
338
339 // Create leave request
340 ev := event.New()
341 ev.Kind = nip43.KindLeaveRequest
342 copy(ev.Pubkey, userPubkey)
343 ev.Tags = tag.NewS()
344 ev.Tags.Append(tag.NewFromAny("-"))
345 ev.CreatedAt = time.Now().Unix()
346 ev.Content = []byte("")
347
348 if err = ev.Sign(userSigner); err != nil {
349 t.Fatalf("failed to sign event: %v", err)
350 }
351
352 // Handle leave request - should handle gracefully
353 err = listener.HandleNIP43LeaveRequest(ev)
354 if err != nil {
355 t.Fatalf("handler returned error: %v", err)
356 }
357 }
358
359 // TestHandleNIP43InviteRequest_ValidRequest tests invite request from admin
360 func TestHandleNIP43InviteRequest_ValidRequest(t *testing.T) {
361 listener, _, cleanup := setupTestListener(t)
362 defer cleanup()
363
364 // Generate admin user
365 adminSecret, err := keys.GenerateSecretKey()
366 if err != nil {
367 t.Fatalf("failed to generate admin secret: %v", err)
368 }
369 adminSigner, err := p8k.New()
370 if err != nil {
371 t.Fatalf("failed to create signer: %v", err)
372 }
373 if err = adminSigner.InitSec(adminSecret); err != nil {
374 t.Fatalf("failed to initialize signer: %v", err)
375 }
376 adminPubkey := adminSigner.Pub()
377
378 // Add admin to config and reconfigure ACL
379 adminHex := hex.Enc(adminPubkey)
380 listener.Server.Config.Admins = []string{adminHex}
381 acl.Registry.SetMode("none")
382 if err = acl.Registry.Configure(listener.Server.Config, listener.Server.DB, listener.ctx); err != nil {
383 t.Fatalf("failed to reconfigure ACL: %v", err)
384 }
385
386 // Handle invite request
387 inviteEvent, err := listener.Server.HandleNIP43InviteRequest(adminPubkey)
388 if err != nil {
389 t.Fatalf("failed to handle invite request: %v", err)
390 }
391
392 // Verify invite event
393 if inviteEvent == nil {
394 t.Fatal("invite event is nil")
395 }
396 if inviteEvent.Kind != nip43.KindInviteReq {
397 t.Errorf("wrong event kind: got %d, want %d", inviteEvent.Kind, nip43.KindInviteReq)
398 }
399
400 // Verify claim tag
401 claimTag := inviteEvent.Tags.GetFirst([]byte("claim"))
402 if claimTag == nil {
403 t.Fatal("missing claim tag")
404 }
405 if claimTag.Len() < 2 {
406 t.Fatal("claim tag has no value")
407 }
408 }
409
410 // TestHandleNIP43InviteRequest_Unauthorized tests invite request from non-admin
411 func TestHandleNIP43InviteRequest_Unauthorized(t *testing.T) {
412 listener, _, cleanup := setupTestListener(t)
413 defer cleanup()
414
415 // Generate regular user (not admin)
416 userSecret, err := keys.GenerateSecretKey()
417 if err != nil {
418 t.Fatalf("failed to generate user secret: %v", err)
419 }
420 userSigner, err := p8k.New()
421 if err != nil {
422 t.Fatalf("failed to create signer: %v", err)
423 }
424 if err = userSigner.InitSec(userSecret); err != nil {
425 t.Fatalf("failed to initialize signer: %v", err)
426 }
427 userPubkey := userSigner.Pub()
428
429 // Handle invite request - should fail
430 _, err = listener.Server.HandleNIP43InviteRequest(userPubkey)
431 if err == nil {
432 t.Fatal("expected error for unauthorized user")
433 }
434 }
435
436 // TestJoinAndLeaveFlow tests the complete join and leave flow
437 func TestJoinAndLeaveFlow(t *testing.T) {
438 listener, db, cleanup := setupTestListener(t)
439 defer cleanup()
440
441 // Generate test user
442 userSecret, err := keys.GenerateSecretKey()
443 if err != nil {
444 t.Fatalf("failed to generate user secret: %v", err)
445 }
446 userSigner, err := p8k.New()
447 if err != nil {
448 t.Fatalf("failed to create signer: %v", err)
449 }
450 if err = userSigner.InitSec(userSecret); err != nil {
451 t.Fatalf("failed to initialize signer: %v", err)
452 }
453 userPubkey := userSigner.Pub()
454
455 // Step 1: Generate invite code
456 code, err := listener.Server.InviteManager.GenerateCode()
457 if err != nil {
458 t.Fatalf("failed to generate invite code: %v", err)
459 }
460
461 // Step 2: User sends join request
462 joinEv := event.New()
463 joinEv.Kind = nip43.KindJoinRequest
464 copy(joinEv.Pubkey, userPubkey)
465 joinEv.Tags = tag.NewS()
466 joinEv.Tags.Append(tag.NewFromAny("-"))
467 joinEv.Tags.Append(tag.NewFromAny("claim", code))
468 joinEv.CreatedAt = time.Now().Unix()
469 joinEv.Content = []byte("")
470 if err = joinEv.Sign(userSigner); err != nil {
471 t.Fatalf("failed to sign join event: %v", err)
472 }
473
474 err = listener.HandleNIP43JoinRequest(joinEv)
475 if err != nil {
476 t.Fatalf("failed to handle join request: %v", err)
477 }
478
479 // Verify user is member
480 isMember, err := db.IsNIP43Member(userPubkey)
481 if err != nil {
482 t.Fatalf("failed to check membership after join: %v", err)
483 }
484 if !isMember {
485 t.Fatal("user is not a member after join")
486 }
487
488 // Step 3: User sends leave request
489 leaveEv := event.New()
490 leaveEv.Kind = nip43.KindLeaveRequest
491 copy(leaveEv.Pubkey, userPubkey)
492 leaveEv.Tags = tag.NewS()
493 leaveEv.Tags.Append(tag.NewFromAny("-"))
494 leaveEv.CreatedAt = time.Now().Unix()
495 leaveEv.Content = []byte("")
496 if err = leaveEv.Sign(userSigner); err != nil {
497 t.Fatalf("failed to sign leave event: %v", err)
498 }
499
500 err = listener.HandleNIP43LeaveRequest(leaveEv)
501 if err != nil {
502 t.Fatalf("failed to handle leave request: %v", err)
503 }
504
505 // Verify user is no longer member
506 isMember, err = db.IsNIP43Member(userPubkey)
507 if err != nil {
508 t.Fatalf("failed to check membership after leave: %v", err)
509 }
510 if isMember {
511 t.Fatal("user is still a member after leave")
512 }
513 }
514
515 // TestMultipleUsersJoining tests multiple users joining concurrently
516 func TestMultipleUsersJoining(t *testing.T) {
517 listener, db, cleanup := setupTestListener(t)
518 defer cleanup()
519
520 userCount := 10
521 done := make(chan bool, userCount)
522
523 for i := 0; i < userCount; i++ {
524 go func(index int) {
525 // Generate user
526 userSecret, err := keys.GenerateSecretKey()
527 if err != nil {
528 t.Errorf("failed to generate user secret %d: %v", index, err)
529 done <- false
530 return
531 }
532 userSigner, err := p8k.New()
533 if err != nil {
534 t.Errorf("failed to create signer %d: %v", index, err)
535 done <- false
536 return
537 }
538 if err = userSigner.InitSec(userSecret); err != nil {
539 t.Errorf("failed to initialize signer %d: %v", index, err)
540 done <- false
541 return
542 }
543 userPubkey := userSigner.Pub()
544
545 // Generate invite code
546 code, err := listener.Server.InviteManager.GenerateCode()
547 if err != nil {
548 t.Errorf("failed to generate invite code %d: %v", index, err)
549 done <- false
550 return
551 }
552
553 // Create join request
554 joinEv := event.New()
555 joinEv.Kind = nip43.KindJoinRequest
556 copy(joinEv.Pubkey, userPubkey)
557 joinEv.Tags = tag.NewS()
558 joinEv.Tags.Append(tag.NewFromAny("-"))
559 joinEv.Tags.Append(tag.NewFromAny("claim", code))
560 joinEv.CreatedAt = time.Now().Unix()
561 joinEv.Content = []byte("")
562 if err = joinEv.Sign(userSigner); err != nil {
563 t.Errorf("failed to sign event %d: %v", index, err)
564 done <- false
565 return
566 }
567
568 // Handle join request
569 if err = listener.HandleNIP43JoinRequest(joinEv); err != nil {
570 t.Errorf("failed to handle join request %d: %v", index, err)
571 done <- false
572 return
573 }
574
575 done <- true
576 }(i)
577 }
578
579 // Wait for all goroutines
580 successCount := 0
581 for i := 0; i < userCount; i++ {
582 if <-done {
583 successCount++
584 }
585 }
586
587 if successCount != userCount {
588 t.Errorf("not all users joined successfully: %d/%d", successCount, userCount)
589 }
590
591 // Verify member count
592 members, err := db.GetAllNIP43Members()
593 if err != nil {
594 t.Fatalf("failed to get all members: %v", err)
595 }
596
597 if len(members) != successCount {
598 t.Errorf("wrong member count: got %d, want %d", len(members), successCount)
599 }
600 }
601