master
go 351 lines 11.5 KB
Raw
1 package autoconf
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "maps"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14
15 "github.com/ipfs/boxo/autoconf"
16 "github.com/ipfs/kubo/test/cli/harness"
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19 )
20
21 // TestAutoConfIPNS tests IPNS publishing with autoconf-resolved delegated publishers
22 func TestAutoConfIPNS(t *testing.T) {
23 t.Parallel()
24
25 t.Run("PublishingWithWorkingEndpoint", func(t *testing.T) {
26 t.Parallel()
27 testIPNSPublishingWithWorkingEndpoint(t)
28 })
29
30 t.Run("PublishingResilience", func(t *testing.T) {
31 t.Parallel()
32 testIPNSPublishingResilience(t)
33 })
34 }
35
36 // testIPNSPublishingWithWorkingEndpoint verifies that IPNS delegated publishing works
37 // correctly when the HTTP endpoint is functioning normally and accepts requests.
38 // It also verifies that the PUT payload matches what can be retrieved via routing get.
39 func testIPNSPublishingWithWorkingEndpoint(t *testing.T) {
40 // Create mock IPNS publisher that accepts requests
41 publisher := newMockIPNSPublisher(t)
42 defer publisher.close()
43
44 // Create node with delegated publisher
45 node := setupNodeWithAutoconf(t, publisher.server.URL, "auto")
46 defer node.StopDaemon()
47
48 // Wait for daemon to be ready
49 time.Sleep(5 * time.Second)
50
51 // Get node's peer ID
52 idResult := node.RunIPFS("id", "-f", "<id>")
53 require.Equal(t, 0, idResult.ExitCode())
54 peerID := strings.TrimSpace(idResult.Stdout.String())
55
56 // Get peer ID in base36 format (used for IPNS keys)
57 idBase36Result := node.RunIPFS("id", "--peerid-base", "base36", "-f", "<id>")
58 require.Equal(t, 0, idBase36Result.ExitCode())
59 peerIDBase36 := strings.TrimSpace(idBase36Result.Stdout.String())
60
61 // Verify autoconf resolved "auto" correctly
62 result := node.RunIPFS("config", "Ipns.DelegatedPublishers", "--expand-auto")
63 var resolvedPublishers []string
64 err := json.Unmarshal([]byte(result.Stdout.String()), &resolvedPublishers)
65 require.NoError(t, err)
66 expectedURL := publisher.server.URL + "/routing/v1/ipns"
67 assert.Contains(t, resolvedPublishers, expectedURL, "AutoConf should resolve 'auto' to mock publisher")
68
69 // Test publishing with --allow-delegated
70 testCID := "bafkqablimvwgy3y"
71 result = node.RunIPFS("name", "publish", "--allow-delegated", "/ipfs/"+testCID)
72 require.Equal(t, 0, result.ExitCode(), "Publishing should succeed")
73 assert.Contains(t, result.Stdout.String(), "Published to")
74
75 // Wait for async HTTP request to delegated publisher
76 time.Sleep(2 * time.Second)
77
78 // Verify HTTP PUT was made to delegated publisher
79 publishedKeys := publisher.getPublishedKeys()
80 assert.NotEmpty(t, publishedKeys, "HTTP PUT request should have been made to delegated publisher")
81
82 // Get the PUT payload that was sent to the delegated publisher
83 putPayload := publisher.getRecordPayload(peerIDBase36)
84 require.NotNil(t, putPayload, "Should have captured PUT payload")
85 require.Greater(t, len(putPayload), 0, "PUT payload should not be empty")
86
87 // Retrieve the IPNS record using routing get
88 getResult := node.RunIPFS("routing", "get", "/ipns/"+peerID)
89 require.Equal(t, 0, getResult.ExitCode(), "Should be able to retrieve IPNS record")
90 getPayload := getResult.Stdout.Bytes()
91
92 // Compare the payloads
93 assert.Equal(t, putPayload, getPayload,
94 "PUT payload sent to delegated publisher should match what routing get returns")
95
96 // Also verify the record points to the expected content
97 assert.Contains(t, getResult.Stdout.String(), testCID,
98 "Retrieved IPNS record should reference the published CID")
99
100 // Use ipfs name inspect to verify the IPNS record's value matches the published CID
101 // First write the routing get result to a file for inspection
102 node.WriteBytes("ipns-record", getPayload)
103 inspectResult := node.RunIPFS("name", "inspect", "ipns-record")
104 require.Equal(t, 0, inspectResult.ExitCode(), "Should be able to inspect IPNS record")
105
106 // The inspect output should show the path we published
107 inspectOutput := inspectResult.Stdout.String()
108 assert.Contains(t, inspectOutput, "/ipfs/"+testCID,
109 "IPNS record value should match the published path")
110
111 // Also verify it's a valid record with proper fields
112 assert.Contains(t, inspectOutput, "Value:", "Should have Value field")
113 assert.Contains(t, inspectOutput, "Validity:", "Should have Validity field")
114 assert.Contains(t, inspectOutput, "Sequence:", "Should have Sequence field")
115
116 t.Log("Verified: PUT payload to delegated publisher matches routing get result and name inspect confirms correct path")
117 }
118
119 // testIPNSPublishingResilience verifies that IPNS publishing is resilient by design.
120 // Publishing succeeds as long as local storage works, even when all delegated endpoints fail.
121 // This test documents the intentional resilient behavior, not bugs.
122 func testIPNSPublishingResilience(t *testing.T) {
123 testCases := []struct {
124 name string
125 routingType string // "auto" or "delegated"
126 description string
127 }{
128 {
129 name: "AutoRouting",
130 routingType: "auto",
131 description: "auto mode uses DHT + HTTP, tolerates HTTP failures",
132 },
133 {
134 name: "DelegatedRouting",
135 routingType: "delegated",
136 description: "delegated mode uses HTTP only, tolerates HTTP failures",
137 },
138 }
139
140 for _, tc := range testCases {
141 t.Run(tc.name, func(t *testing.T) {
142 // Create publisher that always fails
143 publisher := newMockIPNSPublisher(t)
144 defer publisher.close()
145 publisher.responseFunc = func(peerID string, record []byte) int {
146 return http.StatusInternalServerError
147 }
148
149 // Create node with failing endpoint
150 node := setupNodeWithAutoconf(t, publisher.server.URL, tc.routingType)
151 defer node.StopDaemon()
152
153 // Test different publishing modes - all should succeed due to resilient design
154 testCID := "/ipfs/bafkqablimvwgy3y"
155
156 // Normal publishing (should succeed despite endpoint failures)
157 result := node.RunIPFS("name", "publish", testCID)
158 assert.Equal(t, 0, result.ExitCode(),
159 "%s: Normal publishing should succeed (local storage works)", tc.description)
160
161 // Publishing with --allow-offline (local only, no network)
162 result = node.RunIPFS("name", "publish", "--allow-offline", testCID)
163 assert.Equal(t, 0, result.ExitCode(),
164 "--allow-offline should succeed (local only)")
165
166 // Publishing with --allow-delegated (if using auto routing)
167 if tc.routingType == "auto" {
168 result = node.RunIPFS("name", "publish", "--allow-delegated", testCID)
169 assert.Equal(t, 0, result.ExitCode(),
170 "--allow-delegated should succeed (no DHT required)")
171 }
172
173 t.Logf("%s: All publishing modes succeeded despite endpoint failures (resilient design)", tc.name)
174 })
175 }
176 }
177
178 // ============================================================================
179 // Helper Functions
180 // ============================================================================
181
182 // setupNodeWithAutoconf creates an IPFS node with autoconf-configured delegated publishers
183 func setupNodeWithAutoconf(t *testing.T, publisherURL string, routingType string) *harness.Node {
184 // Create autoconf server with the publisher endpoint
185 autoconfData := createAutoconfJSON(publisherURL)
186 autoconfServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
187 w.Header().Set("Content-Type", "application/json")
188 fmt.Fprint(w, autoconfData)
189 }))
190 t.Cleanup(func() { autoconfServer.Close() })
191
192 // Create and configure node
193 h := harness.NewT(t)
194 node := h.NewNode().Init("--profile=test")
195
196 // Configure autoconf
197 node.SetIPFSConfig("AutoConf.URL", autoconfServer.URL)
198 node.SetIPFSConfig("AutoConf.Enabled", true)
199 node.SetIPFSConfig("Ipns.DelegatedPublishers", []string{"auto"})
200 node.SetIPFSConfig("Routing.Type", routingType)
201
202 // Additional config for delegated routing mode
203 if routingType == "delegated" {
204 node.SetIPFSConfig("Provide.Enabled", false)
205 node.SetIPFSConfig("Provide.DHT.Interval", "0s")
206 }
207
208 // Add bootstrap peers for connectivity
209 node.SetIPFSConfig("Bootstrap", autoconf.FallbackBootstrapPeers)
210
211 // Start daemon
212 node.StartDaemon()
213
214 return node
215 }
216
217 // createAutoconfJSON generates autoconf configuration with a delegated IPNS publisher
218 func createAutoconfJSON(publisherURL string) string {
219 // Use bootstrap peers from autoconf fallbacks for consistency
220 bootstrapPeers, _ := json.Marshal(autoconf.FallbackBootstrapPeers)
221
222 return fmt.Sprintf(`{
223 "AutoConfVersion": 2025072302,
224 "AutoConfSchema": 1,
225 "AutoConfTTL": 86400,
226 "SystemRegistry": {
227 "TestSystem": {
228 "Description": "Test system for IPNS publishing",
229 "NativeConfig": {
230 "Bootstrap": %s
231 }
232 }
233 },
234 "DNSResolvers": {},
235 "DelegatedEndpoints": {
236 "%s": {
237 "Systems": ["TestSystem"],
238 "Read": ["/routing/v1/ipns"],
239 "Write": ["/routing/v1/ipns"]
240 }
241 }
242 }`, string(bootstrapPeers), publisherURL)
243 }
244
245 // ============================================================================
246 // Mock IPNS Publisher
247 // ============================================================================
248
249 // mockIPNSPublisher implements a simple IPNS publishing HTTP API server
250 type mockIPNSPublisher struct {
251 t *testing.T
252 server *httptest.Server
253 mu sync.Mutex
254 publishedKeys map[string]string // peerID -> published CID
255 recordPayloads map[string][]byte // peerID -> actual HTTP PUT record payload
256 responseFunc func(peerID string, record []byte) int // returns HTTP status code
257 }
258
259 func newMockIPNSPublisher(t *testing.T) *mockIPNSPublisher {
260 m := &mockIPNSPublisher{
261 t: t,
262 publishedKeys: make(map[string]string),
263 recordPayloads: make(map[string][]byte),
264 }
265
266 // Default response function accepts all publishes
267 m.responseFunc = func(peerID string, record []byte) int {
268 return http.StatusOK
269 }
270
271 mux := http.NewServeMux()
272 mux.HandleFunc("/routing/v1/ipns/", m.handleIPNS)
273
274 m.server = httptest.NewServer(mux)
275 return m
276 }
277
278 func (m *mockIPNSPublisher) handleIPNS(w http.ResponseWriter, r *http.Request) {
279 m.mu.Lock()
280 defer m.mu.Unlock()
281
282 // Extract peer ID from path
283 parts := strings.Split(r.URL.Path, "/")
284 if len(parts) < 5 {
285 http.Error(w, "invalid path", http.StatusBadRequest)
286 return
287 }
288
289 peerID := parts[4]
290
291 if r.Method == "PUT" {
292 // Handle IPNS record publication
293 body, err := io.ReadAll(r.Body)
294 if err != nil {
295 http.Error(w, "failed to read body", http.StatusBadRequest)
296 return
297 }
298
299 // Get response status from response function
300 status := m.responseFunc(peerID, body)
301
302 if status == http.StatusOK {
303 if len(body) > 0 {
304 // Store the actual record payload
305 m.recordPayloads[peerID] = make([]byte, len(body))
306 copy(m.recordPayloads[peerID], body)
307 }
308
309 // Mark as published
310 m.publishedKeys[peerID] = fmt.Sprintf("published-%d", time.Now().Unix())
311 }
312
313 w.WriteHeader(status)
314 if status != http.StatusOK {
315 fmt.Fprint(w, `{"error": "publish failed"}`)
316 }
317 } else if r.Method == "GET" {
318 // Handle IPNS record retrieval
319 if record, exists := m.publishedKeys[peerID]; exists {
320 w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record")
321 fmt.Fprint(w, record)
322 } else {
323 http.Error(w, "record not found", http.StatusNotFound)
324 }
325 } else {
326 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
327 }
328 }
329
330 func (m *mockIPNSPublisher) getPublishedKeys() map[string]string {
331 m.mu.Lock()
332 defer m.mu.Unlock()
333 result := make(map[string]string)
334 maps.Copy(result, m.publishedKeys)
335 return result
336 }
337
338 func (m *mockIPNSPublisher) getRecordPayload(peerID string) []byte {
339 m.mu.Lock()
340 defer m.mu.Unlock()
341 if payload, exists := m.recordPayloads[peerID]; exists {
342 result := make([]byte, len(payload))
343 copy(result, payload)
344 return result
345 }
346 return nil
347 }
348
349 func (m *mockIPNSPublisher) close() {
350 m.server.Close()
351 }