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
+}