nrc_test.go raw

   1  package nrc
   2  
   3  import (
   4  	"testing"
   5  	"time"
   6  
   7  	"next.orly.dev/pkg/nostr/crypto/keys"
   8  	"next.orly.dev/pkg/nostr/encoders/hex"
   9  )
  10  
  11  // Test keys - generated from known secrets for reproducibility
  12  var (
  13  	// From secret: 0000000000000000000000000000000000000000000000000000000000000001
  14  	testRelaySecret = "0000000000000000000000000000000000000000000000000000000000000001"
  15  	// From secret: 0000000000000000000000000000000000000000000000000000000000000002
  16  	testClientSecret = "0000000000000000000000000000000000000000000000000000000000000002"
  17  )
  18  
  19  // getTestRelayPubkey returns the pubkey derived from testRelaySecret
  20  func getTestRelayPubkey(t *testing.T) []byte {
  21  	secretBytes, err := hex.Dec(testRelaySecret)
  22  	if err != nil {
  23  		t.Fatalf("failed to decode test secret: %v", err)
  24  	}
  25  	pubkey, err := keys.SecretBytesToPubKeyBytes(secretBytes)
  26  	if err != nil {
  27  		t.Fatalf("failed to derive pubkey: %v", err)
  28  	}
  29  	return pubkey
  30  }
  31  
  32  // getTestRelayPubkeyHex returns the hex-encoded pubkey
  33  func getTestRelayPubkeyHex(t *testing.T) string {
  34  	return string(hex.Enc(getTestRelayPubkey(t)))
  35  }
  36  
  37  func TestParseConnectionURI(t *testing.T) {
  38  	// Get valid pubkey for tests
  39  	relayPubkeyHex := getTestRelayPubkeyHex(t)
  40  
  41  	tests := []struct {
  42  		name    string
  43  		uri     string
  44  		wantErr bool
  45  		check   func(*testing.T, *ConnectionURI)
  46  	}{
  47  		{
  48  			name:    "invalid scheme",
  49  			uri:     "nostr+wallet://abc123",
  50  			wantErr: true,
  51  		},
  52  		{
  53  			name:    "missing relay parameter",
  54  			uri:     "nostr+relayconnect://" + relayPubkeyHex,
  55  			wantErr: true,
  56  		},
  57  		{
  58  			name:    "missing secret parameter",
  59  			uri:     "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com",
  60  			wantErr: true,
  61  		},
  62  		{
  63  			name:    "invalid relay pubkey",
  64  			uri:     "nostr+relayconnect://invalid?relay=wss://relay.example.com&secret=" + testClientSecret,
  65  			wantErr: true,
  66  		},
  67  		{
  68  			name: "valid secret-based URI",
  69  			uri:  "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret,
  70  			check: func(t *testing.T, conn *ConnectionURI) {
  71  				if conn.AuthMode != AuthModeSecret {
  72  					t.Errorf("expected AuthModeSecret, got %d", conn.AuthMode)
  73  				}
  74  				if conn.RendezvousRelay != "wss://relay.example.com" {
  75  					t.Errorf("expected wss://relay.example.com, got %s", conn.RendezvousRelay)
  76  				}
  77  				if conn.GetClientSigner() == nil {
  78  					t.Error("expected client signer to be set")
  79  				}
  80  				if len(conn.GetConversationKey()) != 32 {
  81  					t.Errorf("expected 32-byte conversation key, got %d", len(conn.GetConversationKey()))
  82  				}
  83  			},
  84  		},
  85  		{
  86  			name: "valid URI with device name",
  87  			uri:  "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret + "&name=phone",
  88  			check: func(t *testing.T, conn *ConnectionURI) {
  89  				if conn.DeviceName != "phone" {
  90  					t.Errorf("expected device name 'phone', got '%s'", conn.DeviceName)
  91  				}
  92  			},
  93  		},
  94  	}
  95  
  96  	for _, tt := range tests {
  97  		t.Run(tt.name, func(t *testing.T) {
  98  			conn, err := ParseConnectionURI(tt.uri)
  99  			if (err != nil) != tt.wantErr {
 100  				t.Errorf("ParseConnectionURI() error = %v, wantErr %v", err, tt.wantErr)
 101  				return
 102  			}
 103  			if tt.check != nil && err == nil {
 104  				tt.check(t, conn)
 105  			}
 106  		})
 107  	}
 108  }
 109  
 110  func TestGenerateConnectionURI(t *testing.T) {
 111  	relayPubkey := getTestRelayPubkey(t)
 112  	rendezvousRelay := "wss://relay.example.com"
 113  
 114  	uri, secret, err := GenerateConnectionURI(relayPubkey, rendezvousRelay, "test-device")
 115  	if err != nil {
 116  		t.Fatalf("GenerateConnectionURI() error = %v", err)
 117  	}
 118  
 119  	if len(secret) != 32 {
 120  		t.Errorf("expected 32-byte secret, got %d", len(secret))
 121  	}
 122  
 123  	// Parse the generated URI to verify it's valid
 124  	conn, err := ParseConnectionURI(uri)
 125  	if err != nil {
 126  		t.Fatalf("failed to parse generated URI: %v", err)
 127  	}
 128  
 129  	if conn.DeviceName != "test-device" {
 130  		t.Errorf("expected device name 'test-device', got '%s'", conn.DeviceName)
 131  	}
 132  
 133  	if conn.RendezvousRelay != rendezvousRelay {
 134  		t.Errorf("expected rendezvous relay %s, got %s", rendezvousRelay, conn.RendezvousRelay)
 135  	}
 136  }
 137  
 138  func TestSession(t *testing.T) {
 139  	clientPubkey := make([]byte, 32)
 140  	conversationKey := make([]byte, 32)
 141  
 142  	session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
 143  	if session == nil {
 144  		t.Fatal("NewSession returned nil")
 145  	}
 146  
 147  	// Test basic properties
 148  	if session.ID != "test-session" {
 149  		t.Errorf("expected ID 'test-session', got '%s'", session.ID)
 150  	}
 151  	if session.DeviceName != "test-device" {
 152  		t.Errorf("expected device name 'test-device', got '%s'", session.DeviceName)
 153  	}
 154  	if session.AuthMode != AuthModeSecret {
 155  		t.Errorf("expected AuthModeSecret, got %d", session.AuthMode)
 156  	}
 157  
 158  	// Test subscription management
 159  	if err := session.AddSubscription("sub1"); err != nil {
 160  		t.Errorf("AddSubscription() error = %v", err)
 161  	}
 162  	if !session.HasSubscription("sub1") {
 163  		t.Error("expected subscription 'sub1' to exist")
 164  	}
 165  	if session.SubscriptionCount() != 1 {
 166  		t.Errorf("expected 1 subscription, got %d", session.SubscriptionCount())
 167  	}
 168  
 169  	session.RemoveSubscription("sub1")
 170  	if session.HasSubscription("sub1") {
 171  		t.Error("expected subscription 'sub1' to be removed")
 172  	}
 173  
 174  	// Test expiry
 175  	if session.IsExpired(time.Hour) {
 176  		t.Error("session should not be expired")
 177  	}
 178  
 179  	// Test close
 180  	session.Close()
 181  	select {
 182  	case <-session.Context().Done():
 183  		// Expected
 184  	default:
 185  		t.Error("expected session context to be cancelled after Close()")
 186  	}
 187  }
 188  
 189  func TestSessionManager(t *testing.T) {
 190  	manager := NewSessionManager(time.Minute)
 191  	if manager == nil {
 192  		t.Fatal("NewSessionManager returned nil")
 193  	}
 194  
 195  	clientPubkey := make([]byte, 32)
 196  	conversationKey := make([]byte, 32)
 197  
 198  	// Test GetOrCreate
 199  	session := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
 200  	if session == nil {
 201  		t.Fatal("GetOrCreate returned nil")
 202  	}
 203  
 204  	// Get same session again
 205  	session2 := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
 206  	if session2 != session {
 207  		t.Error("expected GetOrCreate to return same session")
 208  	}
 209  
 210  	// Test Get
 211  	retrieved := manager.Get("session1")
 212  	if retrieved != session {
 213  		t.Error("expected Get to return the session")
 214  	}
 215  
 216  	// Test Count
 217  	if manager.Count() != 1 {
 218  		t.Errorf("expected count 1, got %d", manager.Count())
 219  	}
 220  
 221  	// Test Remove
 222  	manager.Remove("session1")
 223  	if manager.Get("session1") != nil {
 224  		t.Error("expected session to be removed")
 225  	}
 226  	if manager.Count() != 0 {
 227  		t.Errorf("expected count 0 after removal, got %d", manager.Count())
 228  	}
 229  
 230  	// Test Close
 231  	manager.GetOrCreate("session2", clientPubkey, conversationKey, AuthModeSecret, "device2")
 232  	manager.Close()
 233  	if manager.Count() != 0 {
 234  		t.Errorf("expected count 0 after Close, got %d", manager.Count())
 235  	}
 236  }
 237  
 238  func TestParseRequestContent(t *testing.T) {
 239  	tests := []struct {
 240  		name    string
 241  		content string
 242  		wantErr bool
 243  		check   func(*testing.T, *RequestMessage)
 244  	}{
 245  		{
 246  			name:    "empty content",
 247  			content: "",
 248  			wantErr: true,
 249  		},
 250  		{
 251  			name:    "invalid JSON",
 252  			content: "not json",
 253  			wantErr: true,
 254  		},
 255  		{
 256  			name:    "missing type",
 257  			content: `{"payload": []}`,
 258  			wantErr: true,
 259  		},
 260  		{
 261  			name:    "valid EVENT request",
 262  			content: `{"type": "EVENT", "payload": ["EVENT", {}]}`,
 263  			check: func(t *testing.T, msg *RequestMessage) {
 264  				if msg.Type != "EVENT" {
 265  					t.Errorf("expected type EVENT, got %s", msg.Type)
 266  				}
 267  			},
 268  		},
 269  		{
 270  			name:    "valid REQ request",
 271  			content: `{"type": "REQ", "payload": ["REQ", "sub1", {}]}`,
 272  			check: func(t *testing.T, msg *RequestMessage) {
 273  				if msg.Type != "REQ" {
 274  					t.Errorf("expected type REQ, got %s", msg.Type)
 275  				}
 276  			},
 277  		},
 278  	}
 279  
 280  	for _, tt := range tests {
 281  		t.Run(tt.name, func(t *testing.T) {
 282  			msg, err := ParseRequestContent([]byte(tt.content))
 283  			if (err != nil) != tt.wantErr {
 284  				t.Errorf("ParseRequestContent() error = %v, wantErr %v", err, tt.wantErr)
 285  				return
 286  			}
 287  			if tt.check != nil && err == nil {
 288  				tt.check(t, msg)
 289  			}
 290  		})
 291  	}
 292  }
 293  
 294  func TestMarshalResponseContent(t *testing.T) {
 295  	resp := &ResponseMessage{
 296  		Type:    "OK",
 297  		Payload: []any{"OK", "eventid123", true, ""},
 298  	}
 299  
 300  	content, err := MarshalResponseContent(resp)
 301  	if err != nil {
 302  		t.Fatalf("MarshalResponseContent() error = %v", err)
 303  	}
 304  
 305  	// Verify it's valid JSON that can be parsed back
 306  	parsed, err := ParseRequestContent(content)
 307  	if err != nil {
 308  		t.Fatalf("failed to parse marshaled response: %v", err)
 309  	}
 310  
 311  	if parsed.Type != "OK" {
 312  		t.Errorf("expected type OK, got %s", parsed.Type)
 313  	}
 314  }
 315  
 316  func TestBridgeConfig(t *testing.T) {
 317  	config := &BridgeConfig{
 318  		RendezvousURL:     "wss://relay.example.com",
 319  		LocalRelayURL:     "ws://localhost:3334",
 320  		AuthorizedSecrets: map[string]string{"pubkey1": "device1"},
 321  		SessionTimeout:    time.Minute,
 322  	}
 323  
 324  	bridge := NewBridge(config)
 325  	if bridge == nil {
 326  		t.Fatal("NewBridge returned nil")
 327  	}
 328  
 329  	// Bridge shouldn't start without a valid rendezvous relay
 330  	// but we can verify it was created
 331  	bridge.Stop()
 332  }
 333  
 334  func TestSubscriptionTooMany(t *testing.T) {
 335  	clientPubkey := make([]byte, 32)
 336  	conversationKey := make([]byte, 32)
 337  
 338  	session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
 339  
 340  	// Add max subscriptions
 341  	for i := 0; i < DefaultMaxSubscriptions; i++ {
 342  		if err := session.AddSubscription(string(rune(i))); err != nil {
 343  			t.Fatalf("AddSubscription() error = %v at iteration %d", err, i)
 344  		}
 345  	}
 346  
 347  	// Next one should fail
 348  	err := session.AddSubscription("overflow")
 349  	if err != ErrTooManySubscriptions {
 350  		t.Errorf("expected ErrTooManySubscriptions, got %v", err)
 351  	}
 352  
 353  	session.Close()
 354  }
 355