master
go 172 lines 5.46 KB
Raw
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 }