diff --git a/dns/nip05.go b/dns/nip05.go index 500be68..3c47c17 100644 --- a/dns/nip05.go +++ b/dns/nip05.go @@ -5,6 +5,7 @@ package dns import ( "encoding/json" "fmt" + "io" "net/http" "regexp" "strings" @@ -118,12 +119,15 @@ func Fetch(c context.T, account string) (resp *WellKnownResponse, } defer func() { _ = res.Body.Close() }() resp = NewWellKnownResponse() - b := make([]byte, 65535) - var n int - if n, err = res.Body.Read(b); chk.E(err) { + + // Read the entire response body + var b []byte + if b, err = io.ReadAll(res.Body); chk.E(err) { + err = errorf.E("failed to read response body: %w", err) return } - b = b[:n] + + // Unmarshal the JSON response if err = json.Unmarshal(b, resp); chk.E(err) { err = errorf.E("failed to decode json response: %w", err) } diff --git a/dns/nip05_additional_test.go b/dns/nip05_additional_test.go new file mode 100644 index 0000000..285d291 --- /dev/null +++ b/dns/nip05_additional_test.go @@ -0,0 +1,207 @@ +package dns + +import ( + "bytes" + "testing" +) + +func TestIsValidIdentifier(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + {"Valid with name", "user@example.com", true}, + {"Valid domain only", "example.com", true}, + {"Valid with subdomain", "user@sub.example.com", true}, + {"Valid with plus", "user+tag@example.com", true}, + {"Valid with underscore", "user_name@example.com", true}, + {"Invalid no domain", "user@", false}, + {"Invalid special chars", "user!@example.com", false}, + {"Invalid format", "not-an-email", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidIdentifier(tt.input) + if result != tt.expected { + t.Errorf("IsValidIdentifier(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestNormalizeIdentifier(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"Already normalized", "example.com", "example.com"}, + {"With underscore prefix", "_@example.com", "example.com"}, + {"With name", "user@example.com", "user@example.com"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizeIdentifier(tt.input) + if result != tt.expected { + t.Errorf("NormalizeIdentifier(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestStringSliceToByteSlice(t *testing.T) { + tests := []struct { + name string + input []string + expected [][]byte + }{ + { + "Empty slice", + []string{}, + [][]byte{}, + }, + { + "Single item", + []string{"test"}, + [][]byte{[]byte("test")}, + }, + { + "Multiple items", + []string{"test1", "test2", "test3"}, + [][]byte{[]byte("test1"), []byte("test2"), []byte("test3")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := StringSliceToByteSlice(tt.input) + + if len(result) != len(tt.expected) { + t.Fatalf("StringSliceToByteSlice(%v) returned slice of length %d, want %d", + tt.input, len(result), len(tt.expected)) + } + + for i, v := range result { + if !bytes.Equal(v, tt.expected[i]) { + t.Errorf("StringSliceToByteSlice(%v)[%d] = %v, want %v", + tt.input, i, v, tt.expected[i]) + } + } + }) + } +} + +func TestParseIdentifierEdgeCases(t *testing.T) { + tests := []struct { + name string + input string + expectedName string + expectedDomain string + expectError bool + }{ + {"Empty string", "", "", "", true}, + {"Just @", "@", "", "", true}, + {"Multiple @", "user@domain@example.com", "", "", true}, + {"Invalid domain", "user@invalid", "", "", true}, + {"Valid with hyphen", "user-name@example.com", "user-name", "example.com", false}, + {"Valid with numbers", "user123@example123.com", "user123", "example123.com", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, domain, err := ParseIdentifier(tt.input) + + if tt.expectError && err == nil { + t.Errorf("ParseIdentifier(%q) expected error, got nil", tt.input) + return + } + + if !tt.expectError && err != nil { + t.Errorf("ParseIdentifier(%q) unexpected error: %v", tt.input, err) + return + } + + if !tt.expectError { + if name != tt.expectedName { + t.Errorf("ParseIdentifier(%q) name = %q, want %q", tt.input, name, tt.expectedName) + } + + if domain != tt.expectedDomain { + t.Errorf("ParseIdentifier(%q) domain = %q, want %q", tt.input, domain, tt.expectedDomain) + } + } + }) + } +} + +func TestNewWellKnownResponse(t *testing.T) { + resp := NewWellKnownResponse() + + if resp == nil { + t.Fatal("NewWellKnownResponse() returned nil") + } + + if resp.Names == nil { + t.Error("NewWellKnownResponse() Names map is nil") + } + + if resp.Relays == nil { + t.Error("NewWellKnownResponse() Relays map is nil") + } + + if resp.NIP46 == nil { + t.Error("NewWellKnownResponse() NIP46 map is nil") + } + + // Test that we can add entries to the maps + resp.Names["test"] = "pubkey" + resp.Relays["pubkey"] = []string{"relay1", "relay2"} + resp.NIP46["pubkey"] = []string{"nip46url"} + + if resp.Names["test"] != "pubkey" { + t.Error("Failed to add entry to Names map") + } + + if len(resp.Relays["pubkey"]) != 2 { + t.Error("Failed to add entry to Relays map") + } + + if len(resp.NIP46["pubkey"]) != 1 { + t.Error("Failed to add entry to NIP46 map") + } +} + +func TestRegexPattern(t *testing.T) { + // Test the Nip05Regex pattern directly + tests := []struct { + name string + input string + expected bool + }{ + {"Valid email format", "user@example.com", true}, + {"Valid domain only", "example.com", true}, + {"Valid subdomain", "user@sub.example.com", true}, + {"Valid with underscore", "user_name@example.com", true}, + {"Valid with hyphen", "user-name@example.com", true}, + {"Valid with plus", "user+tag@example.com", true}, + {"Valid with numbers", "user123@example123.com", true}, + {"Invalid no TLD", "user@localhost", false}, + {"Valid IP address", "user@127.0.0.1", true}, + {"Invalid with port", "user@example.com:8080", false}, + {"Invalid special chars", "user!@example.com", false}, + {"Invalid multiple @", "user@domain@example.com", false}, + {"Empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Nip05Regex.MatchString(tt.input) + if result != tt.expected { + t.Errorf("Nip05Regex.MatchString(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/keys/keys.go b/keys/keys.go index 2924d4f..422bab1 100644 --- a/keys/keys.go +++ b/keys/keys.go @@ -59,20 +59,34 @@ func SecretBytesToPubKeyHex(skb []byte) (pk string, err error) { // IsValid32ByteHex checks that a hex string is a valid 32 bytes lower case hex encoded value as // per nostr NIP-01 spec. func IsValid32ByteHex[V []byte | string](pk V) bool { - if bytes.Equal(bytes.ToLower([]byte(pk)), []byte(pk)) { + pkBytes := []byte(pk) + + // Check if the input is lowercase + if !bytes.Equal(bytes.ToLower(pkBytes), pkBytes) { return false } - var err error - dec := make([]byte, 32) - if _, err = hex.DecBytes(dec, []byte(pk)); chk.E(err) { + + // Check if the input length is exactly 64 characters (32 bytes * 2) + if len(pkBytes) != 64 { + return false } - return len(dec) == 32 + + // Try to decode the hex string + var err error + if _, err = hex.Dec(string(pkBytes)); err != nil { + return false + } + + return true } // IsValidPublicKey checks that a hex encoded public key is a valid BIP-340 public key. func IsValidPublicKey[V []byte | string](pk V) bool { - v, _ := hex.Dec(string(pk)) - _, err := schnorr.ParsePubKey(v) + v, err := hex.Dec(string(pk)) + if err != nil { + return false + } + _, err = schnorr.ParsePubKey(v) return err == nil } diff --git a/keys/keys_test.go b/keys/keys_test.go new file mode 100644 index 0000000..bad6148 --- /dev/null +++ b/keys/keys_test.go @@ -0,0 +1,197 @@ +package keys + +import ( + "testing" +) + +func TestIsValid32ByteHex(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + "Valid lowercase hex 32 bytes", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + true, + }, + { + "Valid lowercase hex 32 bytes with different value", + "f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7", + true, + }, + { + "Invalid uppercase hex", + "3BF0C63FCB93463407AF97A5E5EE64FA883D107EF9E558472C4EB9AAAEFA459D", + false, + }, + { + "Invalid mixed case hex", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459D", + false, + }, + { + "Invalid too short", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa45", + false, + }, + { + "Invalid too long", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d00", + false, + }, + { + "Invalid non-hex characters", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459g", + false, + }, + { + "Empty string", + "", + false, + }, + { + "Invalid special characters", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa45!@", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValid32ByteHex(tt.input) + if result != tt.expected { + t.Errorf("IsValid32ByteHex(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestIsValid32ByteHexWithBytes(t *testing.T) { + tests := []struct { + name string + input []byte + expected bool + }{ + { + "Valid lowercase hex bytes", + []byte("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"), + true, + }, + { + "Invalid uppercase hex bytes", + []byte("3BF0C63FCB93463407AF97A5E5EE64FA883D107EF9E558472C4EB9AAAEFA459D"), + false, + }, + { + "Empty bytes", + []byte(""), + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValid32ByteHex(tt.input) + if result != tt.expected { + t.Errorf("IsValid32ByteHex(%q) = %v, want %v", string(tt.input), result, tt.expected) + } + }) + } +} + +func TestIsValidPublicKey(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + "Valid public key", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + true, + }, + { + "Another valid public key", + "f9dd6a762506260b38a2d3e5b464213c2e47fa3877429fe9ee60e071a31a07d7", + true, + }, + { + "Invalid too short", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa45", + false, + }, + { + "Invalid too long", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d00", + false, + }, + { + "Invalid non-hex", + "not-a-hex-string", + false, + }, + { + "Empty string", + "", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidPublicKey(tt.input) + if result != tt.expected { + t.Errorf("IsValidPublicKey(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestHexPubkeyToBytes(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + expectedLen int + }{ + { + "Valid hex string", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + false, + 32, + }, + { + "Invalid hex string", + "not-hex", + true, + 0, + }, + { + "Empty string", + "", + true, + 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := HexPubkeyToBytes(tt.input) + + if tt.expectError && err == nil { + t.Errorf("HexPubkeyToBytes(%q) expected error, got nil", tt.input) + return + } + + if !tt.expectError && err != nil { + t.Errorf("HexPubkeyToBytes(%q) unexpected error: %v", tt.input, err) + return + } + + if !tt.expectError && len(result) != tt.expectedLen { + t.Errorf("HexPubkeyToBytes(%q) returned %d bytes, want %d", tt.input, len(result), tt.expectedLen) + } + }) + } +}