p2pforge_resolver_test.go
1 package node 2 3 import ( 4 "context" 5 "errors" 6 "net" 7 "testing" 8 9 "github.com/ipfs/kubo/config" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 // Test constants matching p2p-forge production format 15 const ( 16 // testPeerID is a valid peerID in CIDv1 base36 format as used by p2p-forge. 17 // Base36 is lowercase-only, making it safe for case-insensitive DNS. 18 // Corresponds to 12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN in base58btc. 19 testPeerID = "k51qzi5uqu5dhnwe629wdlncpql6frppdpwnz4wtlcw816aysd5wwlk63g4wmh" 20 21 // domainSuffix is the default p2p-forge domain used in tests. 22 domainSuffix = config.DefaultDomainSuffix 23 ) 24 25 // mockResolver implements madns.BasicResolver for testing 26 type mockResolver struct { 27 txtRecords map[string][]string 28 ipRecords map[string][]net.IPAddr 29 ipErr error 30 } 31 32 func (m *mockResolver) LookupIPAddr(_ context.Context, hostname string) ([]net.IPAddr, error) { 33 if m.ipErr != nil { 34 return nil, m.ipErr 35 } 36 if m.ipRecords != nil { 37 return m.ipRecords[hostname], nil 38 } 39 return nil, nil 40 } 41 42 func (m *mockResolver) LookupTXT(_ context.Context, name string) ([]string, error) { 43 if m.txtRecords != nil { 44 return m.txtRecords[name], nil 45 } 46 return nil, nil 47 } 48 49 // newTestResolver creates a p2pForgeResolver with default suffix. 50 func newTestResolver(t *testing.T) *p2pForgeResolver { 51 t.Helper() 52 return NewP2PForgeResolver([]string{domainSuffix}, &mockResolver{}) 53 } 54 55 // assertLookupIP verifies that hostname resolves to wantIP. 56 func assertLookupIP(t *testing.T, r *p2pForgeResolver, hostname, wantIP string) { 57 t.Helper() 58 addrs, err := r.LookupIPAddr(t.Context(), hostname) 59 require.NoError(t, err) 60 require.Len(t, addrs, 1) 61 assert.Equal(t, wantIP, addrs[0].IP.String()) 62 } 63 64 func TestP2PForgeResolver_LookupIPAddr(t *testing.T) { 65 r := newTestResolver(t) 66 67 tests := []struct { 68 name string 69 hostname string 70 wantIP string 71 }{ 72 // IPv4 73 {"ipv4/basic", "192-168-1-1." + testPeerID + "." + domainSuffix, "192.168.1.1"}, 74 {"ipv4/zeros", "0-0-0-0." + testPeerID + "." + domainSuffix, "0.0.0.0"}, 75 {"ipv4/max", "255-255-255-255." + testPeerID + "." + domainSuffix, "255.255.255.255"}, 76 {"ipv4/trailing dot", "10-0-0-1." + testPeerID + "." + domainSuffix + ".", "10.0.0.1"}, 77 {"ipv4/uppercase suffix", "192-168-1-1." + testPeerID + ".LIBP2P.DIRECT", "192.168.1.1"}, 78 // IPv6 79 {"ipv6/full", "2001-db8-0-0-0-0-0-1." + testPeerID + "." + domainSuffix, "2001:db8::1"}, 80 {"ipv6/compressed", "2001-db8--1." + testPeerID + "." + domainSuffix, "2001:db8::1"}, 81 {"ipv6/loopback", "0--1." + testPeerID + "." + domainSuffix, "::1"}, 82 {"ipv6/all zeros", "0--0." + testPeerID + "." + domainSuffix, "::"}, 83 } 84 85 for _, tt := range tests { 86 t.Run(tt.name, func(t *testing.T) { 87 assertLookupIP(t, r, tt.hostname, tt.wantIP) 88 }) 89 } 90 } 91 92 func TestP2PForgeResolver_LookupIPAddr_MultipleSuffixes(t *testing.T) { 93 r := NewP2PForgeResolver([]string{domainSuffix, "custom.example.com"}, &mockResolver{}) 94 95 tests := []struct { 96 hostname string 97 wantIP string 98 }{ 99 {"192-168-1-1." + testPeerID + "." + domainSuffix, "192.168.1.1"}, 100 {"10-0-0-1." + testPeerID + ".custom.example.com", "10.0.0.1"}, 101 } 102 103 for _, tt := range tests { 104 t.Run(tt.hostname, func(t *testing.T) { 105 assertLookupIP(t, r, tt.hostname, tt.wantIP) 106 }) 107 } 108 } 109 110 func TestP2PForgeResolver_LookupIPAddr_FallbackToNetwork(t *testing.T) { 111 fallbackIP := []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}} 112 113 tests := []struct { 114 name string 115 hostname string 116 }{ 117 {"peerID only", testPeerID + "." + domainSuffix}, 118 {"invalid peerID", "192-168-1-1.invalid-peer-id." + domainSuffix}, 119 {"invalid IP encoding", "not-an-ip." + testPeerID + "." + domainSuffix}, 120 {"leading hyphen", "-192-168-1-1." + testPeerID + "." + domainSuffix}, 121 {"too many parts", "extra.192-168-1-1." + testPeerID + "." + domainSuffix}, 122 {"wrong suffix", "192-168-1-1." + testPeerID + ".example.com"}, 123 } 124 125 // Build fallback records from test cases 126 ipRecords := make(map[string][]net.IPAddr, len(tests)) 127 for _, tt := range tests { 128 ipRecords[tt.hostname] = fallbackIP 129 } 130 fallback := &mockResolver{ipRecords: ipRecords} 131 r := NewP2PForgeResolver([]string{domainSuffix}, fallback) 132 133 for _, tt := range tests { 134 t.Run(tt.name, func(t *testing.T) { 135 addrs, err := r.LookupIPAddr(t.Context(), tt.hostname) 136 require.NoError(t, err) 137 require.Len(t, addrs, 1, "should fallback to network") 138 assert.Equal(t, "93.184.216.34", addrs[0].IP.String()) 139 }) 140 } 141 } 142 143 func TestP2PForgeResolver_LookupIPAddr_FallbackError(t *testing.T) { 144 expectedErr := errors.New("network error") 145 r := NewP2PForgeResolver([]string{domainSuffix}, &mockResolver{ipErr: expectedErr}) 146 147 // peerID-only triggers fallback, which returns error 148 _, err := r.LookupIPAddr(t.Context(), testPeerID+"."+domainSuffix) 149 require.ErrorIs(t, err, expectedErr) 150 } 151 152 func TestP2PForgeResolver_LookupTXT(t *testing.T) { 153 t.Run("delegates to fallback for ACME DNS-01", func(t *testing.T) { 154 acmeHost := "_acme-challenge." + testPeerID + "." + domainSuffix 155 fallback := &mockResolver{ 156 txtRecords: map[string][]string{acmeHost: {"acme-token-value"}}, 157 } 158 r := NewP2PForgeResolver([]string{domainSuffix}, fallback) 159 160 records, err := r.LookupTXT(t.Context(), acmeHost) 161 require.NoError(t, err) 162 assert.Equal(t, []string{"acme-token-value"}, records) 163 }) 164 165 t.Run("returns empty when fallback has no records", func(t *testing.T) { 166 r := NewP2PForgeResolver([]string{domainSuffix}, &mockResolver{}) 167 168 records, err := r.LookupTXT(t.Context(), "anything."+domainSuffix) 169 require.NoError(t, err) 170 assert.Empty(t, records) 171 }) 172 }