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