nip43_e2e_test.go raw

   1  package app
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"net/http"
   7  	"net/http/httptest"
   8  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
   9  	"os"
  10  	"testing"
  11  	"time"
  12  
  13  	"next.orly.dev/app/config"
  14  	"next.orly.dev/pkg/acl"
  15  	"next.orly.dev/pkg/nostr/crypto/keys"
  16  	"next.orly.dev/pkg/database"
  17  	"next.orly.dev/pkg/nostr/encoders/event"
  18  	"next.orly.dev/pkg/nostr/encoders/hex"
  19  	"next.orly.dev/pkg/nostr/encoders/tag"
  20  	"next.orly.dev/pkg/protocol/nip43"
  21  	"next.orly.dev/pkg/protocol/publish"
  22  	"next.orly.dev/pkg/nostr/relayinfo"
  23  )
  24  
  25  // newTestListener creates a properly initialized Listener for testing
  26  func newTestListener(server *Server, ctx context.Context) *Listener {
  27  	listener := &Listener{
  28  		Server:         server,
  29  		ctx:            ctx,
  30  		writeChan:      make(chan publish.WriteRequest, 100),
  31  		writeDone:      make(chan struct{}),
  32  		messageQueue:   make(chan messageRequest, 100),
  33  		processingDone: make(chan struct{}),
  34  		subscriptions:  make(map[string]context.CancelFunc),
  35  	}
  36  
  37  	// Start write worker and message processor
  38  	go listener.writeWorker()
  39  	go listener.messageProcessor()
  40  
  41  	return listener
  42  }
  43  
  44  // closeTestListener properly closes a test listener
  45  func closeTestListener(listener *Listener) {
  46  	close(listener.writeChan)
  47  	<-listener.writeDone
  48  	close(listener.messageQueue)
  49  	<-listener.processingDone
  50  }
  51  
  52  // setupE2ETest creates a full test server for end-to-end testing
  53  func setupE2ETest(t *testing.T) (*Server, *httptest.Server, func()) {
  54  	tempDir, err := os.MkdirTemp("", "nip43_e2e_test_*")
  55  	if err != nil {
  56  		t.Fatalf("failed to create temp dir: %v", err)
  57  	}
  58  
  59  	ctx, cancel := context.WithCancel(context.Background())
  60  	db, err := database.New(ctx, cancel, tempDir, "info")
  61  	if err != nil {
  62  		os.RemoveAll(tempDir)
  63  		t.Fatalf("failed to open database: %v", err)
  64  	}
  65  
  66  	cfg := &config.C{
  67  		AppName:                "TestRelay",
  68  		NIP43Enabled:           true,
  69  		NIP43PublishEvents:     true,
  70  		NIP43PublishMemberList: true,
  71  		NIP43InviteExpiry:      24 * time.Hour,
  72  		RelayURL:               "wss://test.relay",
  73  		Listen:                 "localhost",
  74  		Port:                   3334,
  75  		ACLMode:                "none",
  76  		AuthRequired:           false,
  77  	}
  78  
  79  	// Generate admin keys
  80  	adminSecret, err := keys.GenerateSecretKey()
  81  	if err != nil {
  82  		t.Fatalf("failed to generate admin secret: %v", err)
  83  	}
  84  	adminSigner, err := p8k.New()
  85  	if err != nil {
  86  		t.Fatalf("failed to create admin signer: %v", err)
  87  	}
  88  	if err = adminSigner.InitSec(adminSecret); err != nil {
  89  		t.Fatalf("failed to initialize admin signer: %v", err)
  90  	}
  91  	adminPubkey := adminSigner.Pub()
  92  
  93  	// Add admin to config for ACL
  94  	cfg.Admins = []string{hex.Enc(adminPubkey)}
  95  
  96  	server := &Server{
  97  		Ctx:           ctx,
  98  		Config:        cfg,
  99  		DB:            db,
 100  		publishers:    publish.New(NewPublisher(ctx)),
 101  		Admins:        [][]byte{adminPubkey},
 102  		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
 103  		cfg:           cfg,
 104  		db:            db,
 105  	}
 106  
 107  	// Configure ACL registry
 108  	acl.Registry.SetMode(cfg.ACLMode)
 109  	if err = acl.Registry.Configure(cfg, db, ctx); err != nil {
 110  		db.Close()
 111  		os.RemoveAll(tempDir)
 112  		t.Fatalf("failed to configure ACL: %v", err)
 113  	}
 114  
 115  	server.mux = http.NewServeMux()
 116  
 117  	// Set up HTTP handlers
 118  	server.mux.HandleFunc(
 119  		"/", func(w http.ResponseWriter, r *http.Request) {
 120  			if r.Header.Get("Accept") == "application/nostr+json" {
 121  				server.HandleRelayInfo(w, r)
 122  				return
 123  			}
 124  			http.NotFound(w, r)
 125  		},
 126  	)
 127  
 128  	httpServer := httptest.NewServer(server.mux)
 129  
 130  	cleanup := func() {
 131  		httpServer.Close()
 132  		db.Close()
 133  		os.RemoveAll(tempDir)
 134  	}
 135  
 136  	return server, httpServer, cleanup
 137  }
 138  
 139  // TestE2E_RelayInfoIncludesNIP43 tests that NIP-43 is advertised in relay info
 140  func TestE2E_RelayInfoIncludesNIP43(t *testing.T) {
 141  	server, httpServer, cleanup := setupE2ETest(t)
 142  	defer cleanup()
 143  
 144  	// Make request to relay info endpoint
 145  	req, err := http.NewRequest("GET", httpServer.URL, nil)
 146  	if err != nil {
 147  		t.Fatalf("failed to create request: %v", err)
 148  	}
 149  	req.Header.Set("Accept", "application/nostr+json")
 150  
 151  	resp, err := http.DefaultClient.Do(req)
 152  	if err != nil {
 153  		t.Fatalf("failed to make request: %v", err)
 154  	}
 155  	defer resp.Body.Close()
 156  
 157  	// Parse relay info
 158  	var info relayinfo.T
 159  	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
 160  		t.Fatalf("failed to decode relay info: %v", err)
 161  	}
 162  
 163  	// Verify NIP-43 is in supported NIPs
 164  	hasNIP43 := false
 165  	for _, nip := range info.Nips {
 166  		if nip == 43 {
 167  			hasNIP43 = true
 168  			break
 169  		}
 170  	}
 171  
 172  	if !hasNIP43 {
 173  		t.Error("NIP-43 not advertised in supported_nips")
 174  	}
 175  
 176  	// Verify server name
 177  	if info.Name != server.Config.AppName {
 178  		t.Errorf(
 179  			"wrong relay name: got %s, want %s", info.Name,
 180  			server.Config.AppName,
 181  		)
 182  	}
 183  }
 184  
 185  // TestE2E_CompleteJoinFlow tests the complete user join flow
 186  func TestE2E_CompleteJoinFlow(t *testing.T) {
 187  	server, _, cleanup := setupE2ETest(t)
 188  	defer cleanup()
 189  
 190  	// Step 1: Admin requests invite code
 191  	adminPubkey := server.Admins[0]
 192  	inviteEvent, err := server.HandleNIP43InviteRequest(adminPubkey)
 193  	if err != nil {
 194  		t.Fatalf("failed to generate invite: %v", err)
 195  	}
 196  
 197  	// Extract invite code
 198  	claimTag := inviteEvent.Tags.GetFirst([]byte("claim"))
 199  	if claimTag == nil || claimTag.Len() < 2 {
 200  		t.Fatal("invite event missing claim tag")
 201  	}
 202  	inviteCode := string(claimTag.T[1])
 203  
 204  	// Step 2: User creates join request
 205  	userSecret, err := keys.GenerateSecretKey()
 206  	if err != nil {
 207  		t.Fatalf("failed to generate user secret: %v", err)
 208  	}
 209  	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
 210  	if err != nil {
 211  		t.Fatalf("failed to get user pubkey: %v", err)
 212  	}
 213  	signer, err := keys.SecretBytesToSigner(userSecret)
 214  	if err != nil {
 215  		t.Fatalf("failed to create signer: %v", err)
 216  	}
 217  
 218  	joinEv := event.New()
 219  	joinEv.Kind = nip43.KindJoinRequest
 220  	copy(joinEv.Pubkey, userPubkey)
 221  	joinEv.Tags = tag.NewS()
 222  	joinEv.Tags.Append(tag.NewFromAny("-"))
 223  	joinEv.Tags.Append(tag.NewFromAny("claim", inviteCode))
 224  	joinEv.CreatedAt = time.Now().Unix()
 225  	joinEv.Content = []byte("")
 226  	if err = joinEv.Sign(signer); err != nil {
 227  		t.Fatalf("failed to sign join event: %v", err)
 228  	}
 229  
 230  	// Step 3: Process join request
 231  	listener := newTestListener(server, server.Ctx)
 232  	defer closeTestListener(listener)
 233  	err = listener.HandleNIP43JoinRequest(joinEv)
 234  	if err != nil {
 235  		t.Fatalf("failed to handle join request: %v", err)
 236  	}
 237  
 238  	// Step 4: Verify membership
 239  	isMember, err := server.DB.IsNIP43Member(userPubkey)
 240  	if err != nil {
 241  		t.Fatalf("failed to check membership: %v", err)
 242  	}
 243  	if !isMember {
 244  		t.Error("user was not added as member")
 245  	}
 246  
 247  	membership, err := server.DB.GetNIP43Membership(userPubkey)
 248  	if err != nil {
 249  		t.Fatalf("failed to get membership: %v", err)
 250  	}
 251  	if membership.InviteCode != inviteCode {
 252  		t.Errorf(
 253  			"wrong invite code: got %s, want %s", membership.InviteCode,
 254  			inviteCode,
 255  		)
 256  	}
 257  }
 258  
 259  // TestE2E_InviteCodeReuse tests that invite codes can only be used once
 260  func TestE2E_InviteCodeReuse(t *testing.T) {
 261  	server, _, cleanup := setupE2ETest(t)
 262  	defer cleanup()
 263  
 264  	// Generate invite code
 265  	code, err := server.InviteManager.GenerateCode()
 266  	if err != nil {
 267  		t.Fatalf("failed to generate invite code: %v", err)
 268  	}
 269  
 270  	listener := newTestListener(server, server.Ctx)
 271  	defer closeTestListener(listener)
 272  
 273  	// First user uses the code
 274  	user1Secret, err := keys.GenerateSecretKey()
 275  	if err != nil {
 276  		t.Fatalf("failed to generate user1 secret: %v", err)
 277  	}
 278  	user1Pubkey, err := keys.SecretBytesToPubKeyBytes(user1Secret)
 279  	if err != nil {
 280  		t.Fatalf("failed to get user1 pubkey: %v", err)
 281  	}
 282  	signer1, err := keys.SecretBytesToSigner(user1Secret)
 283  	if err != nil {
 284  		t.Fatalf("failed to create signer1: %v", err)
 285  	}
 286  
 287  	joinEv1 := event.New()
 288  	joinEv1.Kind = nip43.KindJoinRequest
 289  	copy(joinEv1.Pubkey, user1Pubkey)
 290  	joinEv1.Tags = tag.NewS()
 291  	joinEv1.Tags.Append(tag.NewFromAny("-"))
 292  	joinEv1.Tags.Append(tag.NewFromAny("claim", code))
 293  	joinEv1.CreatedAt = time.Now().Unix()
 294  	joinEv1.Content = []byte("")
 295  	if err = joinEv1.Sign(signer1); err != nil {
 296  		t.Fatalf("failed to sign join event 1: %v", err)
 297  	}
 298  
 299  	err = listener.HandleNIP43JoinRequest(joinEv1)
 300  	if err != nil {
 301  		t.Fatalf("failed to handle join request 1: %v", err)
 302  	}
 303  
 304  	// Verify first user is member
 305  	isMember, err := server.DB.IsNIP43Member(user1Pubkey)
 306  	if err != nil {
 307  		t.Fatalf("failed to check user1 membership: %v", err)
 308  	}
 309  	if !isMember {
 310  		t.Error("user1 was not added")
 311  	}
 312  
 313  	// Second user tries to use same code
 314  	user2Secret, err := keys.GenerateSecretKey()
 315  	if err != nil {
 316  		t.Fatalf("failed to generate user2 secret: %v", err)
 317  	}
 318  	user2Pubkey, err := keys.SecretBytesToPubKeyBytes(user2Secret)
 319  	if err != nil {
 320  		t.Fatalf("failed to get user2 pubkey: %v", err)
 321  	}
 322  	signer2, err := keys.SecretBytesToSigner(user2Secret)
 323  	if err != nil {
 324  		t.Fatalf("failed to create signer2: %v", err)
 325  	}
 326  
 327  	joinEv2 := event.New()
 328  	joinEv2.Kind = nip43.KindJoinRequest
 329  	copy(joinEv2.Pubkey, user2Pubkey)
 330  	joinEv2.Tags = tag.NewS()
 331  	joinEv2.Tags.Append(tag.NewFromAny("-"))
 332  	joinEv2.Tags.Append(tag.NewFromAny("claim", code))
 333  	joinEv2.CreatedAt = time.Now().Unix()
 334  	joinEv2.Content = []byte("")
 335  	if err = joinEv2.Sign(signer2); err != nil {
 336  		t.Fatalf("failed to sign join event 2: %v", err)
 337  	}
 338  
 339  	// Should handle without error but not add user
 340  	err = listener.HandleNIP43JoinRequest(joinEv2)
 341  	if err != nil {
 342  		t.Fatalf("handler returned error: %v", err)
 343  	}
 344  
 345  	// Verify second user is NOT member
 346  	isMember, err = server.DB.IsNIP43Member(user2Pubkey)
 347  	if err != nil {
 348  		t.Fatalf("failed to check user2 membership: %v", err)
 349  	}
 350  	if isMember {
 351  		t.Error("user2 was incorrectly added with reused code")
 352  	}
 353  }
 354  
 355  // TestE2E_MembershipListGeneration tests membership list event generation
 356  func TestE2E_MembershipListGeneration(t *testing.T) {
 357  	server, _, cleanup := setupE2ETest(t)
 358  	defer cleanup()
 359  
 360  	listener := newTestListener(server, server.Ctx)
 361  	defer closeTestListener(listener)
 362  
 363  	// Add multiple members
 364  	memberCount := 5
 365  	members := make([][]byte, memberCount)
 366  
 367  	for i := 0; i < memberCount; i++ {
 368  		userSecret, err := keys.GenerateSecretKey()
 369  		if err != nil {
 370  			t.Fatalf("failed to generate user secret %d: %v", i, err)
 371  		}
 372  		userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
 373  		if err != nil {
 374  			t.Fatalf("failed to get user pubkey %d: %v", i, err)
 375  		}
 376  		members[i] = userPubkey
 377  
 378  		// Add directly to database for speed
 379  		err = server.DB.AddNIP43Member(userPubkey, "code")
 380  		if err != nil {
 381  			t.Fatalf("failed to add member %d: %v", i, err)
 382  		}
 383  	}
 384  
 385  	// Generate membership list
 386  	err := listener.publishMembershipList()
 387  	if err != nil {
 388  		t.Fatalf("failed to publish membership list: %v", err)
 389  	}
 390  
 391  	// Note: In a real test, you would verify the event was published
 392  	// through the publishers system. For now, we just verify no error.
 393  }
 394  
 395  // TestE2E_ExpiredInviteCode tests that expired codes are rejected
 396  func TestE2E_ExpiredInviteCode(t *testing.T) {
 397  	tempDir, err := os.MkdirTemp("", "nip43_expired_test_*")
 398  	if err != nil {
 399  		t.Fatalf("failed to create temp dir: %v", err)
 400  	}
 401  	defer os.RemoveAll(tempDir)
 402  
 403  	ctx, cancel := context.WithCancel(context.Background())
 404  	defer cancel()
 405  
 406  	db, err := database.New(ctx, cancel, tempDir, "info")
 407  	if err != nil {
 408  		t.Fatalf("failed to open database: %v", err)
 409  	}
 410  	defer db.Close()
 411  
 412  	cfg := &config.C{
 413  		NIP43Enabled:      true,
 414  		NIP43InviteExpiry: 1 * time.Millisecond, // Very short expiry
 415  	}
 416  
 417  	server := &Server{
 418  		Ctx:           ctx,
 419  		Config:        cfg,
 420  		DB:            db,
 421  		publishers:    publish.New(NewPublisher(ctx)),
 422  		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
 423  		cfg:           cfg,
 424  		db:            db,
 425  	}
 426  
 427  	listener := newTestListener(server, ctx)
 428  	defer closeTestListener(listener)
 429  
 430  	// Generate invite code
 431  	code, err := server.InviteManager.GenerateCode()
 432  	if err != nil {
 433  		t.Fatalf("failed to generate invite code: %v", err)
 434  	}
 435  
 436  	// Wait for expiry
 437  	time.Sleep(10 * time.Millisecond)
 438  
 439  	// Try to use expired code
 440  	userSecret, err := keys.GenerateSecretKey()
 441  	if err != nil {
 442  		t.Fatalf("failed to generate user secret: %v", err)
 443  	}
 444  	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
 445  	if err != nil {
 446  		t.Fatalf("failed to get user pubkey: %v", err)
 447  	}
 448  	signer, err := keys.SecretBytesToSigner(userSecret)
 449  	if err != nil {
 450  		t.Fatalf("failed to create signer: %v", err)
 451  	}
 452  
 453  	joinEv := event.New()
 454  	joinEv.Kind = nip43.KindJoinRequest
 455  	copy(joinEv.Pubkey, userPubkey)
 456  	joinEv.Tags = tag.NewS()
 457  	joinEv.Tags.Append(tag.NewFromAny("-"))
 458  	joinEv.Tags.Append(tag.NewFromAny("claim", code))
 459  	joinEv.CreatedAt = time.Now().Unix()
 460  	joinEv.Content = []byte("")
 461  	if err = joinEv.Sign(signer); err != nil {
 462  		t.Fatalf("failed to sign event: %v", err)
 463  	}
 464  
 465  	err = listener.HandleNIP43JoinRequest(joinEv)
 466  	if err != nil {
 467  		t.Fatalf("handler returned error: %v", err)
 468  	}
 469  
 470  	// Verify user was NOT added
 471  	isMember, err := db.IsNIP43Member(userPubkey)
 472  	if err != nil {
 473  		t.Fatalf("failed to check membership: %v", err)
 474  	}
 475  	if isMember {
 476  		t.Error("user was added with expired code")
 477  	}
 478  }
 479  
 480  // TestE2E_InvalidTimestampRejected tests that events with invalid timestamps are rejected
 481  func TestE2E_InvalidTimestampRejected(t *testing.T) {
 482  	server, _, cleanup := setupE2ETest(t)
 483  	defer cleanup()
 484  
 485  	listener := newTestListener(server, server.Ctx)
 486  	defer closeTestListener(listener)
 487  
 488  	// Generate invite code
 489  	code, err := server.InviteManager.GenerateCode()
 490  	if err != nil {
 491  		t.Fatalf("failed to generate invite code: %v", err)
 492  	}
 493  
 494  	// Create user
 495  	userSecret, err := keys.GenerateSecretKey()
 496  	if err != nil {
 497  		t.Fatalf("failed to generate user secret: %v", err)
 498  	}
 499  	userPubkey, err := keys.SecretBytesToPubKeyBytes(userSecret)
 500  	if err != nil {
 501  		t.Fatalf("failed to get user pubkey: %v", err)
 502  	}
 503  	signer, err := keys.SecretBytesToSigner(userSecret)
 504  	if err != nil {
 505  		t.Fatalf("failed to create signer: %v", err)
 506  	}
 507  
 508  	// Create join request with timestamp far in the past
 509  	joinEv := event.New()
 510  	joinEv.Kind = nip43.KindJoinRequest
 511  	copy(joinEv.Pubkey, userPubkey)
 512  	joinEv.Tags = tag.NewS()
 513  	joinEv.Tags.Append(tag.NewFromAny("-"))
 514  	joinEv.Tags.Append(tag.NewFromAny("claim", code))
 515  	joinEv.CreatedAt = time.Now().Unix() - 700 // More than 10 minutes ago
 516  	joinEv.Content = []byte("")
 517  	if err = joinEv.Sign(signer); err != nil {
 518  		t.Fatalf("failed to sign event: %v", err)
 519  	}
 520  
 521  	// Should handle without error but not add user
 522  	err = listener.HandleNIP43JoinRequest(joinEv)
 523  	if err != nil {
 524  		t.Fatalf("handler returned error: %v", err)
 525  	}
 526  
 527  	// Verify user was NOT added
 528  	isMember, err := server.DB.IsNIP43Member(userPubkey)
 529  	if err != nil {
 530  		t.Fatalf("failed to check membership: %v", err)
 531  	}
 532  	if isMember {
 533  		t.Error("user was added with invalid timestamp")
 534  	}
 535  }
 536  
 537  // BenchmarkJoinRequestProcessing benchmarks join request processing
 538  func BenchmarkJoinRequestProcessing(b *testing.B) {
 539  	tempDir, err := os.MkdirTemp("", "nip43_bench_*")
 540  	if err != nil {
 541  		b.Fatalf("failed to create temp dir: %v", err)
 542  	}
 543  	defer os.RemoveAll(tempDir)
 544  
 545  	ctx, cancel := context.WithCancel(context.Background())
 546  	defer cancel()
 547  
 548  	db, err := database.New(ctx, cancel, tempDir, "error")
 549  	if err != nil {
 550  		b.Fatalf("failed to open database: %v", err)
 551  	}
 552  	defer db.Close()
 553  
 554  	cfg := &config.C{
 555  		NIP43Enabled:      true,
 556  		NIP43InviteExpiry: 24 * time.Hour,
 557  	}
 558  
 559  	server := &Server{
 560  		Ctx:           ctx,
 561  		Config:        cfg,
 562  		DB:            db,
 563  		publishers:    publish.New(NewPublisher(ctx)),
 564  		InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
 565  		cfg:           cfg,
 566  		db:            db,
 567  	}
 568  
 569  	listener := newTestListener(server, ctx)
 570  	defer closeTestListener(listener)
 571  
 572  	b.ResetTimer()
 573  
 574  	for i := 0; i < b.N; i++ {
 575  		// Generate unique user and code for each iteration
 576  		userSecret, _ := keys.GenerateSecretKey()
 577  		userPubkey, _ := keys.SecretBytesToPubKeyBytes(userSecret)
 578  		signer, _ := keys.SecretBytesToSigner(userSecret)
 579  		code, _ := server.InviteManager.GenerateCode()
 580  
 581  		joinEv := event.New()
 582  		joinEv.Kind = nip43.KindJoinRequest
 583  		copy(joinEv.Pubkey, userPubkey)
 584  		joinEv.Tags = tag.NewS()
 585  		joinEv.Tags.Append(tag.NewFromAny("-"))
 586  		joinEv.Tags.Append(tag.NewFromAny("claim", code))
 587  		joinEv.CreatedAt = time.Now().Unix()
 588  		joinEv.Content = []byte("")
 589  		joinEv.Sign(signer)
 590  
 591  		listener.HandleNIP43JoinRequest(joinEv)
 592  	}
 593  }
 594