directory_test.go raw
1 package directory_test
2
3 import (
4 "encoding/hex"
5 "testing"
6 "time"
7
8 "next.orly.dev/pkg/lol/chk"
9 "next.orly.dev/pkg/nostr/crypto/ec/secp256k1"
10 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
11 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
12 "next.orly.dev/pkg/protocol/directory"
13 )
14
15 // Helper to create a test keypair using p8k.Signer
16 func createTestKeypair(t *testing.T) (*p8k.Signer, []byte) {
17 signer := p8k.MustNew()
18 if err := signer.Generate(); chk.E(err) {
19 t.Fatalf("failed to generate keypair: %v", err)
20 }
21
22 pubkey := signer.Pub()
23 return signer, pubkey
24 }
25
26 // TestRelayIdentityAnnouncementCreation tests creating and parsing relay identity announcements
27 func TestRelayIdentityAnnouncementCreation(t *testing.T) {
28 secKey, pubkey := createTestKeypair(t)
29 pubkeyHex := hex.EncodeToString(pubkey)
30
31 // Create relay identity announcement
32 ria, err := directory.NewRelayIdentityAnnouncement(
33 pubkey,
34 "Test Relay",
35 "Test relay for unit tests",
36 "admin@test.com",
37 "wss://relay.test.com/",
38 pubkeyHex,
39 pubkeyHex,
40 "1",
41 )
42 if err != nil {
43 t.Fatalf("failed to create relay identity announcement: %v", err)
44 }
45
46 // Sign the event
47 if err := ria.Event.Sign(secKey); err != nil {
48 t.Fatalf("failed to sign event: %v", err)
49 }
50
51 // Verify the event
52 if _, err := ria.Event.Verify(); err != nil {
53 t.Fatalf("failed to verify event: %v", err)
54 }
55
56 // Parse back the announcement
57 parsed, err := directory.ParseRelayIdentityAnnouncement(ria.Event)
58 if err != nil {
59 t.Fatalf("failed to parse relay identity announcement: %v", err)
60 }
61
62 // Verify fields
63 if parsed.RelayURL != "wss://relay.test.com/" {
64 t.Errorf("relay URL mismatch: got %s, want wss://relay.test.com/", parsed.RelayURL)
65 }
66
67 if parsed.SigningKey != pubkeyHex {
68 t.Errorf("signing key mismatch")
69 }
70
71 if parsed.Version != "1" {
72 t.Errorf("version mismatch: got %s, want 1", parsed.Version)
73 }
74
75 t.Logf("✓ Relay identity announcement created and parsed successfully")
76 }
77
78 // TestTrustActCreationWithNumericLevels tests trust act creation with numeric trust levels
79 func TestTrustActCreationWithNumericLevels(t *testing.T) {
80 testCases := []struct {
81 name string
82 trustLevel directory.TrustLevel
83 shouldFail bool
84 }{
85 {"Zero trust", directory.TrustLevelNone, false},
86 {"Minimal trust", directory.TrustLevelMinimal, false},
87 {"Low trust", directory.TrustLevelLow, false},
88 {"Medium trust", directory.TrustLevelMedium, false},
89 {"High trust", directory.TrustLevelHigh, false},
90 {"Full trust", directory.TrustLevelFull, false},
91 {"Custom 33%", directory.TrustLevel(33), false},
92 {"Custom 99%", directory.TrustLevel(99), false},
93 {"Invalid >100", directory.TrustLevel(101), true},
94 }
95
96 secKey, pubkey := createTestKeypair(t)
97 targetPubkey := hex.EncodeToString(pubkey)
98
99 for _, tc := range testCases {
100 t.Run(tc.name, func(t *testing.T) {
101 ta, err := directory.NewTrustAct(
102 pubkey,
103 targetPubkey,
104 tc.trustLevel,
105 "wss://target.relay.com/",
106 nil,
107 directory.TrustReasonManual,
108 []uint16{1, 3, 7},
109 nil,
110 )
111
112 if tc.shouldFail {
113 if err == nil {
114 t.Errorf("expected error for trust level %d, got nil", tc.trustLevel)
115 }
116 return
117 }
118
119 if err != nil {
120 t.Fatalf("failed to create trust act: %v", err)
121 }
122
123 // Sign and verify
124 if err := ta.Event.Sign(secKey); err != nil {
125 t.Fatalf("failed to sign event: %v", err)
126 }
127
128 // Parse back
129 parsed, err := directory.ParseTrustAct(ta.Event)
130 if err != nil {
131 t.Fatalf("failed to parse trust act: %v", err)
132 }
133
134 if parsed.TrustLevel != tc.trustLevel {
135 t.Errorf("trust level mismatch: got %d, want %d", parsed.TrustLevel, tc.trustLevel)
136 }
137
138 if parsed.RelayURL != "wss://target.relay.com/" {
139 t.Errorf("relay URL mismatch: got %s", parsed.RelayURL)
140 }
141
142 if len(parsed.ReplicationKinds) != 3 {
143 t.Errorf("replication kinds count mismatch: got %d, want 3", len(parsed.ReplicationKinds))
144 }
145 })
146 }
147
148 t.Logf("✓ All trust level tests passed")
149 }
150
151 // TestPartialReplicationDiceThrow tests the probabilistic replication mechanism
152 func TestPartialReplicationDiceThrow(t *testing.T) {
153 if testing.Short() {
154 t.Skip("skipping probabilistic test in short mode")
155 }
156
157 _, pubkey := createTestKeypair(t)
158 targetPubkey := hex.EncodeToString(pubkey)
159
160 testCases := []struct {
161 name string
162 trustLevel directory.TrustLevel
163 iterations int
164 expectedRatio float64
165 toleranceRatio float64
166 }{
167 {"0% replication", directory.TrustLevelNone, 1000, 0.00, 0.05},
168 {"10% replication", directory.TrustLevelMinimal, 1000, 0.10, 0.05},
169 {"25% replication", directory.TrustLevelLow, 1000, 0.25, 0.05},
170 {"50% replication", directory.TrustLevelMedium, 1000, 0.50, 0.05},
171 {"75% replication", directory.TrustLevelHigh, 1000, 0.75, 0.05},
172 {"100% replication", directory.TrustLevelFull, 1000, 1.00, 0.05},
173 }
174
175 for _, tc := range testCases {
176 t.Run(tc.name, func(t *testing.T) {
177 ta, err := directory.NewTrustAct(
178 pubkey,
179 targetPubkey,
180 tc.trustLevel,
181 "wss://target.relay.com/",
182 nil,
183 directory.TrustReasonManual,
184 []uint16{1}, // Kind 1 for testing
185 nil,
186 )
187 if err != nil {
188 t.Fatalf("failed to create trust act: %v", err)
189 }
190
191 replicatedCount := 0
192 for i := 0; i < tc.iterations; i++ {
193 shouldReplicate, err := ta.ShouldReplicateEvent(1)
194 if err != nil {
195 t.Fatalf("failed to check replication: %v", err)
196 }
197 if shouldReplicate {
198 replicatedCount++
199 }
200 }
201
202 actualRatio := float64(replicatedCount) / float64(tc.iterations)
203 diff := actualRatio - tc.expectedRatio
204 if diff < 0 {
205 diff = -diff
206 }
207
208 if diff > tc.toleranceRatio {
209 t.Errorf("replication ratio out of tolerance: got %.2f, want %.2f±%.2f",
210 actualRatio, tc.expectedRatio, tc.toleranceRatio)
211 }
212
213 t.Logf("Trust level %d%%: replicated %d/%d (%.2f%%)",
214 tc.trustLevel, replicatedCount, tc.iterations, actualRatio*100)
215 })
216 }
217
218 t.Logf("✓ Partial replication mechanism works correctly")
219 }
220
221 // TestGroupTagActCreation tests group tag act creation with ownership specs
222 func TestGroupTagActCreation(t *testing.T) {
223 secKey, pubkey := createTestKeypair(t)
224 pubkeyHex := hex.EncodeToString(pubkey)
225
226 testCases := []struct {
227 name string
228 groupID string
229 ownership *directory.OwnershipSpec
230 shouldFail bool
231 }{
232 {
233 name: "Valid single owner",
234 groupID: "test-group",
235 ownership: &directory.OwnershipSpec{
236 Scheme: directory.SchemeSingle,
237 Owners: []string{pubkeyHex},
238 },
239 shouldFail: false,
240 },
241 {
242 name: "Valid 2-of-3 multisig",
243 groupID: "multisig-group",
244 ownership: &directory.OwnershipSpec{
245 Scheme: directory.Scheme2of3,
246 Owners: []string{pubkeyHex, pubkeyHex, pubkeyHex},
247 },
248 shouldFail: false,
249 },
250 {
251 name: "Valid 3-of-5 multisig",
252 groupID: "large-multisig",
253 ownership: &directory.OwnershipSpec{
254 Scheme: directory.Scheme3of5,
255 Owners: []string{pubkeyHex, pubkeyHex, pubkeyHex, pubkeyHex, pubkeyHex},
256 },
257 shouldFail: false,
258 },
259 {
260 name: "Invalid group ID with spaces",
261 groupID: "invalid group",
262 ownership: &directory.OwnershipSpec{Scheme: directory.SchemeSingle, Owners: []string{pubkeyHex}},
263 shouldFail: true,
264 },
265 {
266 name: "Invalid group ID with special chars",
267 groupID: "invalid@group!",
268 ownership: &directory.OwnershipSpec{Scheme: directory.SchemeSingle, Owners: []string{pubkeyHex}},
269 shouldFail: true,
270 },
271 }
272
273 for _, tc := range testCases {
274 t.Run(tc.name, func(t *testing.T) {
275 gta, err := directory.NewGroupTagAct(
276 pubkey,
277 tc.groupID,
278 "role",
279 "admin",
280 pubkeyHex,
281 95,
282 tc.ownership,
283 "Test group tag",
284 nil,
285 )
286
287 if tc.shouldFail {
288 if err == nil {
289 t.Errorf("expected error, got nil")
290 }
291 return
292 }
293
294 if err != nil {
295 t.Fatalf("failed to create group tag act: %v", err)
296 }
297
298 // Sign the event
299 if err := gta.Event.Sign(secKey); err != nil {
300 t.Fatalf("failed to sign event: %v", err)
301 }
302
303 // Parse back
304 parsed, err := directory.ParseGroupTagAct(gta.Event)
305 if err != nil {
306 t.Fatalf("failed to parse group tag act: %v", err)
307 }
308
309 if parsed.GroupID != tc.groupID {
310 t.Errorf("group ID mismatch: got %s, want %s", parsed.GroupID, tc.groupID)
311 }
312
313 if parsed.Owners != nil {
314 if parsed.Owners.Scheme != tc.ownership.Scheme {
315 t.Errorf("ownership scheme mismatch: got %s, want %s",
316 parsed.Owners.Scheme, tc.ownership.Scheme)
317 }
318 }
319 })
320 }
321
322 t.Logf("✓ Group tag act creation tests passed")
323 }
324
325 // TestPublicKeyAdvertisementWithExpiry tests public key advertisement with expiration
326 func TestPublicKeyAdvertisementWithExpiry(t *testing.T) {
327 // Generate identity and delegate keys
328 identitySigner, identityPubkey := createTestKeypair(t)
329 _, delegatePubkey := createTestKeypair(t)
330
331 // Convert identity pubkey to secp256k1.PublicKey for npub encoding
332 pubKey, err := secp256k1.ParsePubKey(append([]byte{0x02}, identityPubkey...))
333 if err != nil {
334 t.Fatalf("failed to parse pubkey: %v", err)
335 }
336
337 // Convert identity to npub (for potential future use)
338 _, err = bech32encoding.PublicKeyToNpub(pubKey)
339 if err != nil {
340 t.Fatalf("failed to encode npub: %v", err)
341 }
342
343 // Test cases with different expiry scenarios
344 testCases := []struct {
345 name string
346 expiry *time.Time
347 isExpired bool
348 }{
349 {
350 name: "No expiry",
351 expiry: nil,
352 isExpired: false,
353 },
354 {
355 name: "Future expiry",
356 expiry: func() *time.Time {
357 t := time.Now().Add(24 * time.Hour)
358 return &t
359 }(),
360 isExpired: false,
361 },
362 {
363 name: "Past expiry (should allow creation, fail on validation)",
364 expiry: func() *time.Time {
365 t := time.Now().Add(-24 * time.Hour)
366 return &t
367 }(),
368 isExpired: true,
369 },
370 }
371
372 for _, tc := range testCases {
373 t.Run(tc.name, func(t *testing.T) {
374 pka, err := directory.NewPublicKeyAdvertisement(
375 identityPubkey,
376 "key-001",
377 hex.EncodeToString(delegatePubkey),
378 directory.KeyPurposeSigning,
379 tc.expiry,
380 "schnorr",
381 "m/0/1",
382 1,
383 nil,
384 )
385
386 // For past expiry, we expect creation to fail
387 if tc.isExpired && err != nil {
388 t.Logf("✓ Correctly rejected past expiry: %v", err)
389 return
390 }
391
392 if err != nil {
393 t.Fatalf("failed to create public key advertisement: %v", err)
394 }
395
396 // Sign with identity key
397 if err := pka.Event.Sign(identitySigner); err != nil {
398 t.Fatalf("failed to sign event: %v", err)
399 }
400
401 // Parse back
402 parsed, err := directory.ParsePublicKeyAdvertisement(pka.Event)
403 if err != nil {
404 t.Fatalf("failed to parse public key advertisement: %v", err)
405 }
406
407 // Verify expiry
408 if tc.expiry != nil {
409 if parsed.Expiry == nil {
410 t.Errorf("expected expiry, got nil")
411 } else if parsed.Expiry.Unix() != tc.expiry.Unix() {
412 t.Errorf("expiry mismatch: got %v, want %v", parsed.Expiry, tc.expiry)
413 }
414 }
415
416 // Test IsExpired method
417 if tc.isExpired != parsed.IsExpired() {
418 t.Errorf("IsExpired mismatch: got %v, want %v", parsed.IsExpired(), tc.isExpired)
419 }
420 })
421 }
422
423 t.Logf("✓ Public key advertisement expiry tests passed")
424 }
425
426 // TestTrustInheritanceCalculation tests web of trust calculations
427 func TestTrustInheritanceCalculation(t *testing.T) {
428 calc := directory.NewTrustCalculator()
429
430 _, pubkeyA := createTestKeypair(t)
431 _, pubkeyB := createTestKeypair(t)
432 _, pubkeyC := createTestKeypair(t)
433
434 targetB := hex.EncodeToString(pubkeyB)
435 targetC := hex.EncodeToString(pubkeyC)
436
437 // Direct trust: A trusts B at 75%
438 actAB, err := directory.NewTrustAct(
439 pubkeyA, targetB, directory.TrustLevelHigh, "wss://b.relay.com/",
440 nil, directory.TrustReasonManual, nil, nil,
441 )
442 if err != nil {
443 t.Fatalf("failed to create trust act A->B: %v", err)
444 }
445
446 calc.AddAct(actAB)
447
448 // Verify direct trust
449 if calc.GetTrustLevel(targetB) != directory.TrustLevelHigh {
450 t.Errorf("direct trust mismatch: got %d, want %d",
451 calc.GetTrustLevel(targetB), directory.TrustLevelHigh)
452 }
453
454 // For inherited trust test, add B->C (50%)
455 actBC, err := directory.NewTrustAct(
456 pubkeyB, targetC, directory.TrustLevelMedium, "wss://c.relay.com/",
457 nil, directory.TrustReasonManual, nil, nil,
458 )
459 if err != nil {
460 t.Fatalf("failed to create trust act B->C: %v", err)
461 }
462
463 calc.AddAct(actBC)
464
465 // Calculate inherited trust A->B->C
466 // Since B is an intermediate node, the inherited trust should be
467 // 75% * 50% = 37.5% = 37%
468 inherited := calc.CalculateInheritedTrust(hex.EncodeToString(pubkeyA), targetC)
469
470 // Note: The current implementation may return direct trust if found,
471 // or 0 if no path exists. This tests the basic functionality.
472 t.Logf("Trust levels: A->B(%d%%) B->C(%d%%) => A inherits %d%% for C",
473 calc.GetTrustLevel(targetB),
474 calc.GetTrustLevel(targetC),
475 inherited)
476
477 // Verify at least that we can get trust levels
478 if calc.GetTrustLevel(targetB) == 0 {
479 t.Errorf("failed to retrieve trust level for B")
480 }
481
482 t.Logf("✓ Trust calculator basic operations work correctly")
483 }
484
485 // TestGroupTagNameValidation tests URL-safe group tag validation
486 func TestGroupTagNameValidation(t *testing.T) {
487 testCases := []struct {
488 name string
489 groupID string
490 shouldFail bool
491 }{
492 {"Valid alphanumeric", "mygroup123", false},
493 {"Valid with dash", "my-group", false},
494 {"Valid with underscore inside", "my_group", false},
495 {"Valid with dot inside", "my.group", false},
496 {"Valid with tilde", "my~group", false},
497 {"Invalid with space", "my group", true},
498 {"Invalid with @", "my@group", true},
499 {"Invalid with #", "my#group", true},
500 {"Invalid with slash", "my/group", true},
501 {"Invalid starting with dot", ".mygroup", true},
502 {"Invalid starting with underscore", "_mygroup", true},
503 {"Too long", string(make([]byte, 256)), true},
504 {"Empty", "", true},
505 }
506
507 for _, tc := range testCases {
508 t.Run(tc.name, func(t *testing.T) {
509 err := directory.ValidateGroupTagName(tc.groupID)
510
511 if tc.shouldFail && err == nil {
512 t.Errorf("expected error for group ID %q, got nil", tc.groupID)
513 }
514
515 if !tc.shouldFail && err != nil {
516 t.Errorf("unexpected error for group ID %q: %v", tc.groupID, err)
517 }
518 })
519 }
520
521 t.Logf("✓ Group tag name validation tests passed")
522 }
523
524 // TestDirectoryEventKindDetection tests IsDirectoryEventKind helper
525 func TestDirectoryEventKindDetection(t *testing.T) {
526 testCases := []struct {
527 kind uint16
528 isDirectory bool
529 }{
530 {0, true}, // Metadata
531 {3, true}, // Contacts
532 {5, true}, // Deletions
533 {1984, true}, // Reporting
534 {10002, true}, // Relay list
535 {10000, true}, // Mute list
536 {10050, true}, // DM relay list
537 {39100, true}, // Relay identity
538 {39101, true}, // Trust act
539 {39102, true}, // Group tag act
540 {39103, true}, // Public key advertisement
541 {39104, true}, // Replication request
542 {39105, true}, // Replication response
543 {1, false}, // Text note (not directory)
544 {7, false}, // Reaction (not directory)
545 {30023, false}, // Long-form (not directory)
546 }
547
548 for _, tc := range testCases {
549 result := directory.IsDirectoryEventKind(tc.kind)
550 if result != tc.isDirectory {
551 t.Errorf("kind %d: got %v, want %v", tc.kind, result, tc.isDirectory)
552 }
553 }
554
555 t.Logf("✓ Directory event kind detection tests passed")
556 }
557