master
go 285 lines 9.46 KB
Raw
1 package cli
2
3 import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/google/uuid"
10 "github.com/ipfs/boxo/autoconf"
11 "github.com/ipfs/boxo/ipns"
12 "github.com/ipfs/boxo/routing/http/client"
13 "github.com/ipfs/boxo/routing/http/types"
14 "github.com/ipfs/boxo/routing/http/types/iter"
15 "github.com/ipfs/go-cid"
16 "github.com/ipfs/kubo/config"
17 "github.com/ipfs/kubo/test/cli/harness"
18 "github.com/libp2p/go-libp2p/core/peer"
19 "github.com/stretchr/testify/assert"
20 "github.com/stretchr/testify/require"
21 )
22
23 func TestRoutingV1Server(t *testing.T) {
24 t.Parallel()
25
26 setupNodes := func(t *testing.T) harness.Nodes {
27 nodes := harness.NewT(t).NewNodes(5).Init()
28 nodes.ForEachPar(func(node *harness.Node) {
29 node.UpdateConfig(func(cfg *config.Config) {
30 cfg.Gateway.ExposeRoutingAPI = config.True
31 cfg.Routing.Type = config.NewOptionalString("dht")
32 })
33 })
34 nodes.StartDaemons().Connect()
35 t.Cleanup(func() { nodes.StopDaemons() })
36 return nodes
37 }
38
39 t.Run("Get Providers Responds With Correct Peers", func(t *testing.T) {
40 t.Parallel()
41 nodes := setupNodes(t)
42
43 text := "hello world " + uuid.New().String()
44 cidStr := nodes[2].IPFSAddStr(text)
45 _ = nodes[3].IPFSAddStr(text)
46 waitUntilProvidesComplete(t, nodes[3])
47
48 cid, err := cid.Decode(cidStr)
49 assert.NoError(t, err)
50
51 c, err := client.New(nodes[1].GatewayURL())
52 assert.NoError(t, err)
53
54 resultsIter, err := c.FindProviders(context.Background(), cid)
55 assert.NoError(t, err)
56
57 records, err := iter.ReadAllResults(resultsIter)
58 assert.NoError(t, err)
59
60 var peers []peer.ID
61 for _, record := range records {
62 assert.Equal(t, types.SchemaPeer, record.GetSchema())
63
64 peer, ok := record.(*types.PeerRecord)
65 assert.True(t, ok)
66 peers = append(peers, *peer.ID)
67 }
68
69 assert.Contains(t, peers, nodes[2].PeerID())
70 assert.Contains(t, peers, nodes[3].PeerID())
71 })
72
73 t.Run("Get Peers Responds With Correct Peers", func(t *testing.T) {
74 t.Parallel()
75 nodes := setupNodes(t)
76
77 c, err := client.New(nodes[1].GatewayURL())
78 assert.NoError(t, err)
79
80 resultsIter, err := c.FindPeers(context.Background(), nodes[2].PeerID())
81 assert.NoError(t, err)
82
83 records, err := iter.ReadAllResults(resultsIter)
84 assert.NoError(t, err)
85 assert.Len(t, records, 1)
86 assert.IsType(t, records[0].GetSchema(), records[0].GetSchema())
87 assert.IsType(t, records[0], &types.PeerRecord{})
88
89 peer := records[0]
90 assert.Equal(t, nodes[2].PeerID().String(), peer.ID.String())
91 assert.NotEmpty(t, peer.Addrs)
92 })
93
94 t.Run("Get IPNS Record Responds With Correct Record", func(t *testing.T) {
95 t.Parallel()
96 nodes := setupNodes(t)
97
98 text := "hello ipns test " + uuid.New().String()
99 cidStr := nodes[0].IPFSAddStr(text)
100 nodes[0].IPFS("name", "publish", "--allow-offline", cidStr)
101
102 // Ask for record from a different peer.
103 c, err := client.New(nodes[1].GatewayURL())
104 assert.NoError(t, err)
105
106 record, err := c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID()))
107 assert.NoError(t, err)
108
109 value, err := record.Value()
110 assert.NoError(t, err)
111 assert.Equal(t, "/ipfs/"+cidStr, value.String())
112 })
113
114 t.Run("Put IPNS Record Succeeds", func(t *testing.T) {
115 t.Parallel()
116 nodes := setupNodes(t)
117
118 // Publish a record and confirm the /routing/v1/ipns API exposes the IPNS record
119 text := "hello ipns test " + uuid.New().String()
120 cidStr := nodes[0].IPFSAddStr(text)
121 nodes[0].IPFS("name", "publish", "--allow-offline", cidStr)
122 c, err := client.New(nodes[0].GatewayURL())
123 assert.NoError(t, err)
124 record, err := c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID()))
125 assert.NoError(t, err)
126 value, err := record.Value()
127 assert.NoError(t, err)
128 assert.Equal(t, "/ipfs/"+cidStr, value.String())
129
130 // Start lonely node that is not connected to other nodes.
131 node := harness.NewT(t).NewNode().Init()
132 node.UpdateConfig(func(cfg *config.Config) {
133 cfg.Gateway.ExposeRoutingAPI = config.True
134 cfg.Routing.Type = config.NewOptionalString("dht")
135 })
136 node.StartDaemon()
137 defer node.StopDaemon()
138
139 // Put IPNS record in lonely node. It should be accepted as it is a valid record.
140 c, err = client.New(node.GatewayURL())
141 assert.NoError(t, err)
142 err = c.PutIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID()), record)
143 assert.NoError(t, err)
144
145 // Get the record from lonely node and double check.
146 record, err = c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID()))
147 assert.NoError(t, err)
148 value, err = record.Value()
149 assert.NoError(t, err)
150 assert.Equal(t, "/ipfs/"+cidStr, value.String())
151 })
152
153 t.Run("GetClosestPeers returns error when DHT is disabled", func(t *testing.T) {
154 t.Parallel()
155
156 // Test various routing types that don't support DHT
157 routingTypes := []string{"none", "delegated", "custom"}
158 for _, routingType := range routingTypes {
159 t.Run("routing_type="+routingType, func(t *testing.T) {
160 t.Parallel()
161
162 // Create node with specified routing type (DHT disabled)
163 node := harness.NewT(t).NewNode().Init()
164 node.UpdateConfig(func(cfg *config.Config) {
165 cfg.Gateway.ExposeRoutingAPI = config.True
166 cfg.Routing.Type = config.NewOptionalString(routingType)
167
168 // For custom routing type, we need to provide minimal valid config
169 // otherwise daemon startup will fail
170 if routingType == "custom" {
171 // Configure a minimal HTTP router (no DHT)
172 cfg.Routing.Routers = map[string]config.RouterParser{
173 "http-only": {
174 Router: config.Router{
175 Type: config.RouterTypeHTTP,
176 Parameters: config.HTTPRouterParams{
177 Endpoint: "https://delegated-ipfs.dev",
178 },
179 },
180 },
181 }
182 cfg.Routing.Methods = map[config.MethodName]config.Method{
183 config.MethodNameProvide: {RouterName: "http-only"},
184 config.MethodNameFindProviders: {RouterName: "http-only"},
185 config.MethodNameFindPeers: {RouterName: "http-only"},
186 config.MethodNameGetIPNS: {RouterName: "http-only"},
187 config.MethodNamePutIPNS: {RouterName: "http-only"},
188 }
189 }
190
191 // For delegated routing type, ensure we have at least one HTTP router
192 // to avoid daemon startup failure
193 if routingType == "delegated" {
194 // Use a minimal delegated router configuration
195 cfg.Routing.DelegatedRouters = []string{"https://delegated-ipfs.dev"}
196 // Delegated routing doesn't support providing, must be disabled
197 cfg.Provide.Enabled = config.False
198 }
199 })
200 node.StartDaemon()
201 defer node.StopDaemon()
202
203 c, err := client.New(node.GatewayURL())
204 require.NoError(t, err)
205
206 // Try to get closest peers - should fail gracefully with an error.
207 // Use 60-second timeout (server has 30s routing timeout).
208 testCid, err := cid.Decode("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
209 require.NoError(t, err)
210
211 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
212 defer cancel()
213 _, err = c.GetClosestPeers(ctx, testCid)
214 require.Error(t, err)
215 // All these routing types should indicate DHT is not available
216 // The exact error message may vary based on implementation details
217 errStr := err.Error()
218 assert.True(t,
219 strings.Contains(errStr, "not supported") ||
220 strings.Contains(errStr, "not available") ||
221 strings.Contains(errStr, "500"),
222 "Expected error indicating DHT not available for routing type %s, got: %s", routingType, errStr)
223 })
224 }
225 })
226
227 t.Run("GetClosestPeers returns peers", func(t *testing.T) {
228 t.Parallel()
229
230 routingTypes := []string{"auto", "autoclient", "dht", "dhtclient"}
231 for _, routingType := range routingTypes {
232 t.Run("routing_type="+routingType, func(t *testing.T) {
233 t.Parallel()
234
235 // Single node with DHT and real bootstrap peers
236 node := harness.NewT(t).NewNode().Init()
237 node.UpdateConfig(func(cfg *config.Config) {
238 cfg.Gateway.ExposeRoutingAPI = config.True
239 cfg.Routing.Type = config.NewOptionalString(routingType)
240 // Set real bootstrap peers from boxo/autoconf
241 cfg.Bootstrap = autoconf.FallbackBootstrapPeers
242 })
243 node.StartDaemon()
244 defer node.StopDaemon()
245
246 c, err := client.New(node.GatewayURL())
247 require.NoError(t, err)
248
249 // Query for closest peers to our own peer ID
250 key := peer.ToCid(node.PeerID())
251
252 // Wait for WAN DHT routing table to be populated.
253 // The server has a 30-second routing timeout, so we use 60 seconds
254 // per request to allow for network latency while preventing hangs.
255 // Total wait time is 5 minutes to accommodate slow CI DHT bootstrapping.
256 // Passing runs finish in 8-48s; failures are total bootstrap failures,
257 // not slow convergence, so extra headroom doesn't waste time on success.
258 var records []*types.PeerRecord
259 require.EventuallyWithT(t, func(ct *assert.CollectT) {
260 ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
261 defer cancel()
262 resultsIter, err := c.GetClosestPeers(ctx, key)
263 if !assert.NoError(ct, err) {
264 return
265 }
266 records, err = iter.ReadAllResults(resultsIter)
267 assert.NoError(ct, err)
268 }, 5*time.Minute, 5*time.Second)
269
270 // Verify we got some peers back from WAN DHT
271 require.NotEmpty(t, records, "should return peers close to own peerid")
272
273 // Per IPIP-0476, GetClosestPeers returns at most 20 peers
274 assert.LessOrEqual(t, len(records), 20, "IPIP-0476 limits GetClosestPeers to 20 peers")
275
276 // Verify structure of returned records
277 for _, record := range records {
278 assert.Equal(t, types.SchemaPeer, record.Schema)
279 assert.NotNil(t, record.ID)
280 assert.NotEmpty(t, record.Addrs, "peer record should have addresses")
281 }
282 })
283 }
284 })
285 }