You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

371 lines
9.9 KiB

package nrc
import (
"testing"
"time"
"git.mleku.dev/mleku/nostr/crypto/keys"
"git.mleku.dev/mleku/nostr/encoders/hex"
)
// Test keys - generated from known secrets for reproducibility
var (
// From secret: 0000000000000000000000000000000000000000000000000000000000000001
testRelaySecret = "0000000000000000000000000000000000000000000000000000000000000001"
// From secret: 0000000000000000000000000000000000000000000000000000000000000002
testClientSecret = "0000000000000000000000000000000000000000000000000000000000000002"
)
// getTestRelayPubkey returns the pubkey derived from testRelaySecret
func getTestRelayPubkey(t *testing.T) []byte {
secretBytes, err := hex.Dec(testRelaySecret)
if err != nil {
t.Fatalf("failed to decode test secret: %v", err)
}
pubkey, err := keys.SecretBytesToPubKeyBytes(secretBytes)
if err != nil {
t.Fatalf("failed to derive pubkey: %v", err)
}
return pubkey
}
// getTestRelayPubkeyHex returns the hex-encoded pubkey
func getTestRelayPubkeyHex(t *testing.T) string {
return string(hex.Enc(getTestRelayPubkey(t)))
}
func TestParseConnectionURI(t *testing.T) {
// Get valid pubkey for tests
relayPubkeyHex := getTestRelayPubkeyHex(t)
tests := []struct {
name string
uri string
wantErr bool
check func(*testing.T, *ConnectionURI)
}{
{
name: "invalid scheme",
uri: "nostr+wallet://abc123",
wantErr: true,
},
{
name: "missing relay parameter",
uri: "nostr+relayconnect://" + relayPubkeyHex,
wantErr: true,
},
{
name: "missing secret parameter",
uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com",
wantErr: true,
},
{
name: "invalid relay pubkey",
uri: "nostr+relayconnect://invalid?relay=wss://relay.example.com&secret=" + testClientSecret,
wantErr: true,
},
{
name: "valid secret-based URI",
uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret,
check: func(t *testing.T, conn *ConnectionURI) {
if conn.AuthMode != AuthModeSecret {
t.Errorf("expected AuthModeSecret, got %d", conn.AuthMode)
}
if conn.RendezvousRelay != "wss://relay.example.com" {
t.Errorf("expected wss://relay.example.com, got %s", conn.RendezvousRelay)
}
if conn.GetClientSigner() == nil {
t.Error("expected client signer to be set")
}
if len(conn.GetConversationKey()) != 32 {
t.Errorf("expected 32-byte conversation key, got %d", len(conn.GetConversationKey()))
}
},
},
{
name: "valid URI with device name",
uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&secret=" + testClientSecret + "&name=phone",
check: func(t *testing.T, conn *ConnectionURI) {
if conn.DeviceName != "phone" {
t.Errorf("expected device name 'phone', got '%s'", conn.DeviceName)
}
},
},
{
name: "valid CAT-based URI",
uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&auth=cat&mint=https://mint.example.com",
check: func(t *testing.T, conn *ConnectionURI) {
if conn.AuthMode != AuthModeCAT {
t.Errorf("expected AuthModeCAT, got %d", conn.AuthMode)
}
if conn.MintURL != "https://mint.example.com" {
t.Errorf("expected mint URL, got %s", conn.MintURL)
}
},
},
{
name: "CAT URI missing mint",
uri: "nostr+relayconnect://" + relayPubkeyHex + "?relay=wss://relay.example.com&auth=cat",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conn, err := ParseConnectionURI(tt.uri)
if (err != nil) != tt.wantErr {
t.Errorf("ParseConnectionURI() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.check != nil && err == nil {
tt.check(t, conn)
}
})
}
}
func TestGenerateConnectionURI(t *testing.T) {
relayPubkey := getTestRelayPubkey(t)
rendezvousRelay := "wss://relay.example.com"
uri, secret, err := GenerateConnectionURI(relayPubkey, rendezvousRelay, "test-device")
if err != nil {
t.Fatalf("GenerateConnectionURI() error = %v", err)
}
if len(secret) != 32 {
t.Errorf("expected 32-byte secret, got %d", len(secret))
}
// Parse the generated URI to verify it's valid
conn, err := ParseConnectionURI(uri)
if err != nil {
t.Fatalf("failed to parse generated URI: %v", err)
}
if conn.DeviceName != "test-device" {
t.Errorf("expected device name 'test-device', got '%s'", conn.DeviceName)
}
if conn.RendezvousRelay != rendezvousRelay {
t.Errorf("expected rendezvous relay %s, got %s", rendezvousRelay, conn.RendezvousRelay)
}
}
func TestSession(t *testing.T) {
clientPubkey := make([]byte, 32)
conversationKey := make([]byte, 32)
session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
if session == nil {
t.Fatal("NewSession returned nil")
}
// Test basic properties
if session.ID != "test-session" {
t.Errorf("expected ID 'test-session', got '%s'", session.ID)
}
if session.DeviceName != "test-device" {
t.Errorf("expected device name 'test-device', got '%s'", session.DeviceName)
}
if session.AuthMode != AuthModeSecret {
t.Errorf("expected AuthModeSecret, got %d", session.AuthMode)
}
// Test subscription management
if err := session.AddSubscription("sub1"); err != nil {
t.Errorf("AddSubscription() error = %v", err)
}
if !session.HasSubscription("sub1") {
t.Error("expected subscription 'sub1' to exist")
}
if session.SubscriptionCount() != 1 {
t.Errorf("expected 1 subscription, got %d", session.SubscriptionCount())
}
session.RemoveSubscription("sub1")
if session.HasSubscription("sub1") {
t.Error("expected subscription 'sub1' to be removed")
}
// Test expiry
if session.IsExpired(time.Hour) {
t.Error("session should not be expired")
}
// Test close
session.Close()
select {
case <-session.Context().Done():
// Expected
default:
t.Error("expected session context to be cancelled after Close()")
}
}
func TestSessionManager(t *testing.T) {
manager := NewSessionManager(time.Minute)
if manager == nil {
t.Fatal("NewSessionManager returned nil")
}
clientPubkey := make([]byte, 32)
conversationKey := make([]byte, 32)
// Test GetOrCreate
session := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
if session == nil {
t.Fatal("GetOrCreate returned nil")
}
// Get same session again
session2 := manager.GetOrCreate("session1", clientPubkey, conversationKey, AuthModeSecret, "device1")
if session2 != session {
t.Error("expected GetOrCreate to return same session")
}
// Test Get
retrieved := manager.Get("session1")
if retrieved != session {
t.Error("expected Get to return the session")
}
// Test Count
if manager.Count() != 1 {
t.Errorf("expected count 1, got %d", manager.Count())
}
// Test Remove
manager.Remove("session1")
if manager.Get("session1") != nil {
t.Error("expected session to be removed")
}
if manager.Count() != 0 {
t.Errorf("expected count 0 after removal, got %d", manager.Count())
}
// Test Close
manager.GetOrCreate("session2", clientPubkey, conversationKey, AuthModeSecret, "device2")
manager.Close()
if manager.Count() != 0 {
t.Errorf("expected count 0 after Close, got %d", manager.Count())
}
}
func TestParseRequestContent(t *testing.T) {
tests := []struct {
name string
content string
wantErr bool
check func(*testing.T, *RequestMessage)
}{
{
name: "empty content",
content: "",
wantErr: true,
},
{
name: "invalid JSON",
content: "not json",
wantErr: true,
},
{
name: "missing type",
content: `{"payload": []}`,
wantErr: true,
},
{
name: "valid EVENT request",
content: `{"type": "EVENT", "payload": ["EVENT", {}]}`,
check: func(t *testing.T, msg *RequestMessage) {
if msg.Type != "EVENT" {
t.Errorf("expected type EVENT, got %s", msg.Type)
}
},
},
{
name: "valid REQ request",
content: `{"type": "REQ", "payload": ["REQ", "sub1", {}]}`,
check: func(t *testing.T, msg *RequestMessage) {
if msg.Type != "REQ" {
t.Errorf("expected type REQ, got %s", msg.Type)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg, err := ParseRequestContent([]byte(tt.content))
if (err != nil) != tt.wantErr {
t.Errorf("ParseRequestContent() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.check != nil && err == nil {
tt.check(t, msg)
}
})
}
}
func TestMarshalResponseContent(t *testing.T) {
resp := &ResponseMessage{
Type: "OK",
Payload: []any{"OK", "eventid123", true, ""},
}
content, err := MarshalResponseContent(resp)
if err != nil {
t.Fatalf("MarshalResponseContent() error = %v", err)
}
// Verify it's valid JSON that can be parsed back
parsed, err := ParseRequestContent(content)
if err != nil {
t.Fatalf("failed to parse marshaled response: %v", err)
}
if parsed.Type != "OK" {
t.Errorf("expected type OK, got %s", parsed.Type)
}
}
func TestBridgeConfig(t *testing.T) {
config := &BridgeConfig{
RendezvousURL: "wss://relay.example.com",
LocalRelayURL: "ws://localhost:3334",
AuthorizedSecrets: map[string]string{"pubkey1": "device1"},
SessionTimeout: time.Minute,
}
bridge := NewBridge(config)
if bridge == nil {
t.Fatal("NewBridge returned nil")
}
// Bridge shouldn't start without a valid rendezvous relay
// but we can verify it was created
bridge.Stop()
}
func TestSubscriptionTooMany(t *testing.T) {
clientPubkey := make([]byte, 32)
conversationKey := make([]byte, 32)
session := NewSession("test-session", clientPubkey, conversationKey, AuthModeSecret, "test-device")
// Add max subscriptions
for i := 0; i < DefaultMaxSubscriptions; i++ {
if err := session.AddSubscription(string(rune(i))); err != nil {
t.Fatalf("AddSubscription() error = %v at iteration %d", err, i)
}
}
// Next one should fail
err := session.AddSubscription("overflow")
if err != ErrTooManySubscriptions {
t.Errorf("expected ErrTooManySubscriptions, got %v", err)
}
session.Close()
}