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