types_test.go raw
1 package nip43
2
3 import (
4 "testing"
5 "time"
6
7 "next.orly.dev/pkg/nostr/crypto/keys"
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/tag"
10 )
11
12 // TestInviteManager_GenerateCode tests invite code generation
13 func TestInviteManager_GenerateCode(t *testing.T) {
14 im := NewInviteManager(24 * time.Hour)
15
16 code, err := im.GenerateCode()
17 if err != nil {
18 t.Fatalf("failed to generate code: %v", err)
19 }
20
21 if code == "" {
22 t.Fatal("generated code is empty")
23 }
24
25 // Verify the code exists in the manager
26 im.mu.Lock()
27 invite, exists := im.codes[code]
28 im.mu.Unlock()
29
30 if !exists {
31 t.Fatal("generated code not found in manager")
32 }
33
34 if invite.Code != code {
35 t.Errorf("code mismatch: got %s, want %s", invite.Code, code)
36 }
37
38 if invite.UsedBy != nil {
39 t.Error("newly generated code should not be used")
40 }
41
42 if time.Until(invite.ExpiresAt) > 24*time.Hour {
43 t.Error("expiry time is too far in the future")
44 }
45 }
46
47 // TestInviteManager_ValidateAndConsume tests invite code validation
48 func TestInviteManager_ValidateAndConsume(t *testing.T) {
49 im := NewInviteManager(24 * time.Hour)
50
51 // Generate a code
52 code, err := im.GenerateCode()
53 if err != nil {
54 t.Fatalf("failed to generate code: %v", err)
55 }
56
57 testPubkey := make([]byte, 32)
58 for i := range testPubkey {
59 testPubkey[i] = byte(i)
60 }
61
62 // Test valid code
63 valid, reason := im.ValidateAndConsume(code, testPubkey)
64 if !valid {
65 t.Fatalf("valid code rejected: %s", reason)
66 }
67
68 // Test already used code
69 valid, reason = im.ValidateAndConsume(code, testPubkey)
70 if valid {
71 t.Error("already used code was accepted")
72 }
73 if reason != "invite code already used" {
74 t.Errorf("wrong rejection reason: got %s", reason)
75 }
76
77 // Test invalid code
78 valid, reason = im.ValidateAndConsume("invalid-code", testPubkey)
79 if valid {
80 t.Error("invalid code was accepted")
81 }
82 if reason != "invalid invite code" {
83 t.Errorf("wrong rejection reason: got %s", reason)
84 }
85 }
86
87 // TestInviteManager_ExpiredCode tests expired invite code handling
88 func TestInviteManager_ExpiredCode(t *testing.T) {
89 // Create manager with very short expiry
90 im := NewInviteManager(1 * time.Millisecond)
91
92 code, err := im.GenerateCode()
93 if err != nil {
94 t.Fatalf("failed to generate code: %v", err)
95 }
96
97 // Wait for expiry
98 time.Sleep(10 * time.Millisecond)
99
100 testPubkey := make([]byte, 32)
101 valid, reason := im.ValidateAndConsume(code, testPubkey)
102 if valid {
103 t.Error("expired code was accepted")
104 }
105 if reason != "invite code expired" {
106 t.Errorf("wrong rejection reason: got %s, want 'invite code expired'", reason)
107 }
108
109 // Verify code was deleted
110 im.mu.Lock()
111 _, exists := im.codes[code]
112 im.mu.Unlock()
113
114 if exists {
115 t.Error("expired code was not deleted")
116 }
117 }
118
119 // TestInviteManager_CleanupExpired tests cleanup of expired codes
120 func TestInviteManager_CleanupExpired(t *testing.T) {
121 im := NewInviteManager(1 * time.Millisecond)
122
123 // Generate multiple codes
124 codes := make([]string, 5)
125 for i := 0; i < 5; i++ {
126 code, err := im.GenerateCode()
127 if err != nil {
128 t.Fatalf("failed to generate code %d: %v", i, err)
129 }
130 codes[i] = code
131 }
132
133 // Wait for expiry
134 time.Sleep(10 * time.Millisecond)
135
136 // Cleanup
137 im.CleanupExpired()
138
139 // Verify all codes were deleted
140 im.mu.Lock()
141 remaining := len(im.codes)
142 im.mu.Unlock()
143
144 if remaining != 0 {
145 t.Errorf("cleanup failed: %d codes remaining", remaining)
146 }
147 }
148
149 // TestBuildMemberListEvent tests membership list event creation
150 func TestBuildMemberListEvent(t *testing.T) {
151 // Generate a test relay secret
152 relaySecret, err := keys.GenerateSecretKey()
153 if err != nil {
154 t.Fatalf("failed to generate relay secret: %v", err)
155 }
156
157 // Create test member pubkeys
158 members := make([][]byte, 3)
159 for i := 0; i < 3; i++ {
160 members[i] = make([]byte, 32)
161 for j := range members[i] {
162 members[i][j] = byte(i*10 + j)
163 }
164 }
165
166 // Build event
167 ev, err := BuildMemberListEvent(relaySecret, members)
168 if err != nil {
169 t.Fatalf("failed to build member list event: %v", err)
170 }
171
172 // Verify event kind
173 if ev.Kind != KindMemberList {
174 t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindMemberList)
175 }
176
177 // Verify NIP-70 tag
178 minusTag := ev.Tags.GetFirst([]byte("-"))
179 if minusTag == nil {
180 t.Error("missing NIP-70 `-` tag")
181 }
182
183 // Verify member tags
184 memberTags := ev.Tags.GetAll([]byte("member"))
185 if len(memberTags) != 3 {
186 t.Errorf("wrong number of member tags: got %d, want 3", len(memberTags))
187 }
188
189 // Verify signature
190 valid, err := ev.Verify()
191 if err != nil {
192 t.Fatalf("signature verification error: %v", err)
193 }
194 if !valid {
195 t.Error("event signature is invalid")
196 }
197 }
198
199 // TestBuildAddUserEvent tests add user event creation
200 func TestBuildAddUserEvent(t *testing.T) {
201 relaySecret, err := keys.GenerateSecretKey()
202 if err != nil {
203 t.Fatalf("failed to generate relay secret: %v", err)
204 }
205
206 userPubkey := make([]byte, 32)
207 for i := range userPubkey {
208 userPubkey[i] = byte(i)
209 }
210
211 ev, err := BuildAddUserEvent(relaySecret, userPubkey)
212 if err != nil {
213 t.Fatalf("failed to build add user event: %v", err)
214 }
215
216 // Verify event kind
217 if ev.Kind != KindAddUser {
218 t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindAddUser)
219 }
220
221 // Verify NIP-70 tag
222 minusTag := ev.Tags.GetFirst([]byte("-"))
223 if minusTag == nil {
224 t.Error("missing NIP-70 `-` tag")
225 }
226
227 // Verify p tag
228 pTag := ev.Tags.GetFirst([]byte("p"))
229 if pTag == nil {
230 t.Error("missing p tag")
231 }
232
233 // Verify signature
234 valid, err := ev.Verify()
235 if err != nil {
236 t.Fatalf("signature verification error: %v", err)
237 }
238 if !valid {
239 t.Error("event signature is invalid")
240 }
241 }
242
243 // TestBuildRemoveUserEvent tests remove user event creation
244 func TestBuildRemoveUserEvent(t *testing.T) {
245 relaySecret, err := keys.GenerateSecretKey()
246 if err != nil {
247 t.Fatalf("failed to generate relay secret: %v", err)
248 }
249
250 userPubkey := make([]byte, 32)
251 for i := range userPubkey {
252 userPubkey[i] = byte(i)
253 }
254
255 ev, err := BuildRemoveUserEvent(relaySecret, userPubkey)
256 if err != nil {
257 t.Fatalf("failed to build remove user event: %v", err)
258 }
259
260 // Verify event kind
261 if ev.Kind != KindRemoveUser {
262 t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindRemoveUser)
263 }
264
265 // Verify NIP-70 tag
266 minusTag := ev.Tags.GetFirst([]byte("-"))
267 if minusTag == nil {
268 t.Error("missing NIP-70 `-` tag")
269 }
270
271 // Verify p tag
272 pTag := ev.Tags.GetFirst([]byte("p"))
273 if pTag == nil {
274 t.Error("missing p tag")
275 }
276
277 // Verify signature
278 valid, err := ev.Verify()
279 if err != nil {
280 t.Fatalf("signature verification error: %v", err)
281 }
282 if !valid {
283 t.Error("event signature is invalid")
284 }
285 }
286
287 // TestBuildInviteEvent tests invite event creation
288 func TestBuildInviteEvent(t *testing.T) {
289 relaySecret, err := keys.GenerateSecretKey()
290 if err != nil {
291 t.Fatalf("failed to generate relay secret: %v", err)
292 }
293
294 inviteCode := "test-invite-code-12345"
295
296 ev, err := BuildInviteEvent(relaySecret, inviteCode)
297 if err != nil {
298 t.Fatalf("failed to build invite event: %v", err)
299 }
300
301 // Verify event kind
302 if ev.Kind != KindInviteReq {
303 t.Errorf("wrong event kind: got %d, want %d", ev.Kind, KindInviteReq)
304 }
305
306 // Verify NIP-70 tag
307 minusTag := ev.Tags.GetFirst([]byte("-"))
308 if minusTag == nil {
309 t.Error("missing NIP-70 `-` tag")
310 }
311
312 // Verify claim tag
313 claimTag := ev.Tags.GetFirst([]byte("claim"))
314 if claimTag == nil {
315 t.Error("missing claim tag")
316 }
317 if claimTag.Len() < 2 {
318 t.Error("claim tag has no value")
319 }
320 if string(claimTag.T[1]) != inviteCode {
321 t.Errorf("wrong invite code in tag: got %s, want %s", string(claimTag.T[1]), inviteCode)
322 }
323
324 // Verify signature
325 valid, err := ev.Verify()
326 if err != nil {
327 t.Fatalf("signature verification error: %v", err)
328 }
329 if !valid {
330 t.Error("event signature is invalid")
331 }
332 }
333
334 // TestValidateJoinRequest tests join request validation
335 func TestValidateJoinRequest(t *testing.T) {
336 tests := []struct {
337 name string
338 setupEvent func() *event.E
339 expectValid bool
340 expectCode string
341 expectReason string
342 }{
343 {
344 name: "valid join request",
345 setupEvent: func() *event.E {
346 ev := event.New()
347 ev.Kind = KindJoinRequest
348 ev.Tags = tag.NewS()
349 ev.Tags.Append(tag.NewFromAny("-"))
350 ev.Tags.Append(tag.NewFromAny("claim", "test-code-123"))
351 ev.CreatedAt = time.Now().Unix()
352 return ev
353 },
354 expectValid: true,
355 expectCode: "test-code-123",
356 expectReason: "",
357 },
358 {
359 name: "wrong kind",
360 setupEvent: func() *event.E {
361 ev := event.New()
362 ev.Kind = 1000
363 return ev
364 },
365 expectValid: false,
366 expectReason: "invalid event kind",
367 },
368 {
369 name: "missing minus tag",
370 setupEvent: func() *event.E {
371 ev := event.New()
372 ev.Kind = KindJoinRequest
373 ev.Tags = tag.NewS()
374 ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
375 ev.CreatedAt = time.Now().Unix()
376 return ev
377 },
378 expectValid: false,
379 expectReason: "missing NIP-70 `-` tag",
380 },
381 {
382 name: "missing claim tag",
383 setupEvent: func() *event.E {
384 ev := event.New()
385 ev.Kind = KindJoinRequest
386 ev.Tags = tag.NewS()
387 ev.Tags.Append(tag.NewFromAny("-"))
388 ev.CreatedAt = time.Now().Unix()
389 return ev
390 },
391 expectValid: false,
392 expectReason: "missing claim tag",
393 },
394 {
395 name: "timestamp too old",
396 setupEvent: func() *event.E {
397 ev := event.New()
398 ev.Kind = KindJoinRequest
399 ev.Tags = tag.NewS()
400 ev.Tags.Append(tag.NewFromAny("-"))
401 ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
402 ev.CreatedAt = time.Now().Unix() - 700 // More than 10 minutes ago
403 return ev
404 },
405 expectValid: false,
406 expectCode: "test-code",
407 expectReason: "timestamp out of range",
408 },
409 {
410 name: "timestamp too far in future",
411 setupEvent: func() *event.E {
412 ev := event.New()
413 ev.Kind = KindJoinRequest
414 ev.Tags = tag.NewS()
415 ev.Tags.Append(tag.NewFromAny("-"))
416 ev.Tags.Append(tag.NewFromAny("claim", "test-code"))
417 ev.CreatedAt = time.Now().Unix() + 700 // More than 10 minutes ahead
418 return ev
419 },
420 expectValid: false,
421 expectCode: "test-code",
422 expectReason: "timestamp out of range",
423 },
424 }
425
426 for _, tt := range tests {
427 t.Run(tt.name, func(t *testing.T) {
428 ev := tt.setupEvent()
429 code, valid, reason := ValidateJoinRequest(ev)
430
431 if valid != tt.expectValid {
432 t.Errorf("valid mismatch: got %v, want %v", valid, tt.expectValid)
433 }
434 if tt.expectCode != "" && code != tt.expectCode {
435 t.Errorf("code mismatch: got %s, want %s", code, tt.expectCode)
436 }
437 if tt.expectReason != "" && reason != tt.expectReason {
438 t.Errorf("reason mismatch: got %s, want %s", reason, tt.expectReason)
439 }
440 })
441 }
442 }
443
444 // TestValidateLeaveRequest tests leave request validation
445 func TestValidateLeaveRequest(t *testing.T) {
446 tests := []struct {
447 name string
448 setupEvent func() *event.E
449 expectValid bool
450 expectReason string
451 }{
452 {
453 name: "valid leave request",
454 setupEvent: func() *event.E {
455 ev := event.New()
456 ev.Kind = KindLeaveRequest
457 ev.Tags = tag.NewS()
458 ev.Tags.Append(tag.NewFromAny("-"))
459 ev.CreatedAt = time.Now().Unix()
460 return ev
461 },
462 expectValid: true,
463 expectReason: "",
464 },
465 {
466 name: "wrong kind",
467 setupEvent: func() *event.E {
468 ev := event.New()
469 ev.Kind = 1000
470 return ev
471 },
472 expectValid: false,
473 expectReason: "invalid event kind",
474 },
475 {
476 name: "missing minus tag",
477 setupEvent: func() *event.E {
478 ev := event.New()
479 ev.Kind = KindLeaveRequest
480 ev.CreatedAt = time.Now().Unix()
481 return ev
482 },
483 expectValid: false,
484 expectReason: "missing NIP-70 `-` tag",
485 },
486 {
487 name: "timestamp out of range",
488 setupEvent: func() *event.E {
489 ev := event.New()
490 ev.Kind = KindLeaveRequest
491 ev.Tags = tag.NewS()
492 ev.Tags.Append(tag.NewFromAny("-"))
493 ev.CreatedAt = time.Now().Unix() - 700
494 return ev
495 },
496 expectValid: false,
497 expectReason: "timestamp out of range",
498 },
499 }
500
501 for _, tt := range tests {
502 t.Run(tt.name, func(t *testing.T) {
503 ev := tt.setupEvent()
504 valid, reason := ValidateLeaveRequest(ev)
505
506 if valid != tt.expectValid {
507 t.Errorf("valid mismatch: got %v, want %v", valid, tt.expectValid)
508 }
509 if tt.expectReason != "" && reason != tt.expectReason {
510 t.Errorf("reason mismatch: got %s, want %s", reason, tt.expectReason)
511 }
512 })
513 }
514 }
515