master
go 698 lines 30.1 KB
Raw
1 // Package autoconf provides comprehensive tests for --expand-auto functionality.
2 //
3 // Test Scenarios:
4 // 1. Tests WITH daemon: Most tests start a daemon to fetch and cache autoconf data,
5 // then test CLI commands that read from that cache using MustGetConfigCached.
6 // 2. Tests WITHOUT daemon: Error condition tests that don't need cached autoconf.
7 //
8 // The daemon setup uses startDaemonAndWaitForAutoConf() helper which:
9 // - Starts the daemon
10 // - Waits for HTTP request to mock server (not arbitrary timeout)
11 // - Returns when autoconf is cached and ready for CLI commands
12 package autoconf
13
14 import (
15 "encoding/json"
16 "fmt"
17 "net/http"
18 "net/http/httptest"
19 "os"
20 "strings"
21 "sync/atomic"
22 "testing"
23 "time"
24
25 "github.com/ipfs/kubo/test/cli/harness"
26 "github.com/stretchr/testify/assert"
27 "github.com/stretchr/testify/require"
28 )
29
30 func TestExpandAutoComprehensive(t *testing.T) {
31 t.Parallel()
32
33 t.Run("all autoconf fields resolve correctly", func(t *testing.T) {
34 t.Parallel()
35 testAllAutoConfFieldsResolve(t)
36 })
37
38 t.Run("bootstrap list --expand-auto matches config Bootstrap --expand-auto", func(t *testing.T) {
39 t.Parallel()
40 testBootstrapCommandConsistency(t)
41 })
42
43 t.Run("write operations fail with --expand-auto", func(t *testing.T) {
44 t.Parallel()
45 testWriteOperationsFailWithExpandAuto(t)
46 })
47
48 t.Run("config show --expand-auto provides complete expanded view", func(t *testing.T) {
49 t.Parallel()
50 testConfigShowExpandAutoComplete(t)
51 })
52
53 t.Run("multiple expand-auto calls use cache (single HTTP request)", func(t *testing.T) {
54 t.Parallel()
55 testMultipleExpandAutoUsesCache(t)
56 })
57
58 t.Run("CLI uses cache only while daemon handles background updates", func(t *testing.T) {
59 t.Parallel()
60 testCLIUsesCacheOnlyDaemonUpdatesBackground(t)
61 })
62 }
63
64 // testAllAutoConfFieldsResolve verifies that all autoconf fields (Bootstrap, DNS.Resolvers,
65 // Routing.DelegatedRouters, and Ipns.DelegatedPublishers) can be resolved from "auto" values
66 // to their actual configuration using --expand-auto flag with daemon-cached autoconf data.
67 //
68 // This test is critical because:
69 // 1. It validates the core autoconf resolution functionality across all supported fields
70 // 2. It ensures that "auto" placeholders are properly replaced with real configuration values
71 // 3. It verifies that the autoconf JSON structure is correctly parsed and applied
72 // 4. It tests the end-to-end flow from HTTP fetch to config field expansion
73 func testAllAutoConfFieldsResolve(t *testing.T) {
74 // Test scenario: CLI with daemon started and autoconf cached
75 // This validates core autoconf resolution functionality across all supported fields
76
77 // Track HTTP requests to verify mock server is being used
78 var requestCount atomic.Int32
79 var autoConfData []byte
80
81 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82 count := requestCount.Add(1)
83 t.Logf("Mock autoconf server request #%d: %s %s", count, r.Method, r.URL.Path)
84
85 // Create comprehensive autoconf response matching Schema 4 format
86 // Use server URLs to ensure they're reachable and valid
87 serverURL := fmt.Sprintf("http://%s", r.Host) // Get the server URL from the request
88 autoConf := map[string]any{
89 "AutoConfVersion": 2025072301,
90 "AutoConfSchema": 1,
91 "AutoConfTTL": 86400,
92 "SystemRegistry": map[string]any{
93 "AminoDHT": map[string]any{
94 "URL": "https://github.com/ipfs/specs/pull/497",
95 "Description": "Test AminoDHT system",
96 "NativeConfig": map[string]any{
97 "Bootstrap": []string{
98 "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
99 "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
100 },
101 },
102 "DelegatedConfig": map[string]any{
103 "Read": []string{"/routing/v1/providers", "/routing/v1/peers", "/routing/v1/ipns"},
104 "Write": []string{"/routing/v1/ipns"},
105 },
106 },
107 "IPNI": map[string]any{
108 "URL": serverURL + "/ipni-system",
109 "Description": "Test IPNI system",
110 "DelegatedConfig": map[string]any{
111 "Read": []string{"/routing/v1/providers"},
112 "Write": []string{},
113 },
114 },
115 "CustomIPNS": map[string]any{
116 "URL": serverURL + "/ipns-system",
117 "Description": "Test IPNS system",
118 "DelegatedConfig": map[string]any{
119 "Read": []string{"/routing/v1/ipns"},
120 "Write": []string{"/routing/v1/ipns"},
121 },
122 },
123 },
124 "DNSResolvers": map[string][]string{
125 ".": {"https://cloudflare-dns.com/dns-query"},
126 "eth.": {"https://dns.google/dns-query"},
127 },
128 "DelegatedEndpoints": map[string]any{
129 serverURL: map[string]any{
130 "Systems": []string{"IPNI", "CustomIPNS"}, // Use non-AminoDHT systems to avoid filtering
131 "Read": []string{"/routing/v1/providers", "/routing/v1/ipns"},
132 "Write": []string{"/routing/v1/ipns"},
133 },
134 },
135 }
136
137 var err error
138 autoConfData, err = json.Marshal(autoConf)
139 if err != nil {
140 t.Fatalf("Failed to marshal autoConf: %v", err)
141 }
142
143 t.Logf("Serving mock autoconf data: %s", string(autoConfData))
144
145 w.Header().Set("Content-Type", "application/json")
146 w.Header().Set("ETag", `"test-mock-config"`)
147 w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
148 _, _ = w.Write(autoConfData)
149 }))
150 defer server.Close()
151
152 // Create IPFS node with all auto values
153 node := harness.NewT(t).NewNode().Init("--profile=test")
154
155 // Clear any existing autoconf cache to prevent interference
156 result := node.RunIPFS("config", "show")
157 if result.ExitCode() == 0 {
158 var cfg map[string]any
159 if json.Unmarshal([]byte(result.Stdout.String()), &cfg) == nil {
160 if repoPath, exists := cfg["path"]; exists {
161 if pathStr, ok := repoPath.(string); ok {
162 t.Logf("Clearing autoconf cache from %s/autoconf", pathStr)
163 // Note: We can't directly remove files, but clearing cache via config change should help
164 }
165 }
166 }
167 }
168 node.SetIPFSConfig("AutoConf.URL", server.URL)
169 node.SetIPFSConfig("AutoConf.Enabled", true)
170 node.SetIPFSConfig("AutoConf.RefreshInterval", "1s") // Force fresh fetches for testing
171 node.SetIPFSConfig("Bootstrap", []string{"auto"})
172 node.SetIPFSConfig("DNS.Resolvers", map[string]string{
173 ".": "auto",
174 "eth.": "auto",
175 })
176 node.SetIPFSConfig("Routing.DelegatedRouters", []string{"auto"})
177 node.SetIPFSConfig("Ipns.DelegatedPublishers", []string{"auto"})
178
179 // Start daemon and wait for autoconf fetch
180 daemon := startDaemonAndWaitForAutoConf(t, node, &requestCount)
181 defer daemon.StopDaemon()
182
183 // Test 1: Bootstrap resolution
184 result = node.RunIPFS("config", "Bootstrap", "--expand-auto")
185 require.Equal(t, 0, result.ExitCode(), "Bootstrap expansion should succeed")
186
187 var expandedBootstrap []string
188 var err error
189 err = json.Unmarshal([]byte(result.Stdout.String()), &expandedBootstrap)
190 require.NoError(t, err)
191
192 assert.NotContains(t, expandedBootstrap, "auto", "Bootstrap should not contain 'auto'")
193 assert.Contains(t, expandedBootstrap, "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN")
194 assert.Contains(t, expandedBootstrap, "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa")
195 t.Logf("Bootstrap expanded to: %v", expandedBootstrap)
196
197 // Test 2: DNS.Resolvers resolution
198 result = node.RunIPFS("config", "DNS.Resolvers", "--expand-auto")
199 require.Equal(t, 0, result.ExitCode(), "DNS.Resolvers expansion should succeed")
200
201 var expandedResolvers map[string]string
202 err = json.Unmarshal([]byte(result.Stdout.String()), &expandedResolvers)
203 require.NoError(t, err)
204
205 assert.NotContains(t, expandedResolvers, "auto", "DNS.Resolvers should not contain 'auto'")
206 assert.Equal(t, "https://cloudflare-dns.com/dns-query", expandedResolvers["."])
207 assert.Equal(t, "https://dns.google/dns-query", expandedResolvers["eth."])
208 t.Logf("DNS.Resolvers expanded to: %v", expandedResolvers)
209
210 // Test 3: Routing.DelegatedRouters resolution
211 result = node.RunIPFS("config", "Routing.DelegatedRouters", "--expand-auto")
212 require.Equal(t, 0, result.ExitCode(), "Routing.DelegatedRouters expansion should succeed")
213
214 var expandedRouters []string
215 err = json.Unmarshal([]byte(result.Stdout.String()), &expandedRouters)
216 require.NoError(t, err)
217
218 assert.NotContains(t, expandedRouters, "auto", "DelegatedRouters should not contain 'auto'")
219
220 // Test should strictly require mock autoconf to work - no fallback acceptance
221 // The mock endpoint has Read paths ["/routing/v1/providers", "/routing/v1/ipns"]
222 // so we expect 2 URLs with those paths
223 expectedMockURLs := []string{
224 server.URL + "/routing/v1/providers",
225 server.URL + "/routing/v1/ipns",
226 }
227 require.Equal(t, 2, len(expandedRouters),
228 "Should have exactly 2 routers from mock autoconf (one for each Read path). Got %d routers: %v. "+
229 "This indicates autoconf is not working properly - check if mock server data is being parsed and filtered correctly.",
230 len(expandedRouters), expandedRouters)
231
232 // Check that both expected URLs are present
233 for _, expectedURL := range expectedMockURLs {
234 assert.Contains(t, expandedRouters, expectedURL,
235 "Should contain mock autoconf endpoint with path %s. Got: %v. "+
236 "This indicates autoconf endpoint path generation is not working properly.",
237 expectedURL, expandedRouters)
238 }
239
240 // Test 4: Ipns.DelegatedPublishers resolution
241 result = node.RunIPFS("config", "Ipns.DelegatedPublishers", "--expand-auto")
242 require.Equal(t, 0, result.ExitCode(), "Ipns.DelegatedPublishers expansion should succeed")
243
244 var expandedPublishers []string
245 err = json.Unmarshal([]byte(result.Stdout.String()), &expandedPublishers)
246 require.NoError(t, err)
247
248 assert.NotContains(t, expandedPublishers, "auto", "DelegatedPublishers should not contain 'auto'")
249
250 // Test should require mock autoconf endpoint for IPNS publishing
251 // The mock endpoint supports /routing/v1/ipns write operations, so it should be included with path
252 expectedMockPublisherURL := server.URL + "/routing/v1/ipns"
253 require.Equal(t, 1, len(expandedPublishers),
254 "Should have exactly 1 IPNS publisher from mock autoconf. Got %d publishers: %v. "+
255 "This indicates autoconf IPNS publisher filtering is not working properly.",
256 len(expandedPublishers), expandedPublishers)
257 assert.Equal(t, expectedMockPublisherURL, expandedPublishers[0],
258 "Should use mock autoconf endpoint %s for IPNS publishing, not fallback. Got: %s. "+
259 "This indicates autoconf IPNS publisher resolution is not working properly.",
260 expectedMockPublisherURL, expandedPublishers[0])
261
262 // CRITICAL: Verify that mock server was actually used
263 finalRequestCount := requestCount.Load()
264 require.Greater(t, finalRequestCount, int32(0),
265 "Mock autoconf server should have been called at least once. Got %d requests. "+
266 "This indicates the test is using cached or fallback config instead of mock data.", finalRequestCount)
267 t.Logf("Mock server was called %d times - test is using mock data", finalRequestCount)
268 }
269
270 // testBootstrapCommandConsistency verifies that `ipfs bootstrap list --expand-auto` and
271 // `ipfs config Bootstrap --expand-auto` return identical results when both use autoconf.
272 //
273 // This test is important because:
274 // 1. It ensures consistency between different CLI commands that access the same data
275 // 2. It validates that both the bootstrap-specific command and generic config command
276 // use the same underlying autoconf resolution mechanism
277 // 3. It prevents regression where different commands might resolve "auto" differently
278 // 4. It ensures users get consistent results regardless of which command they use
279 func testBootstrapCommandConsistency(t *testing.T) {
280 // Test scenario: CLI with daemon started and autoconf cached
281 // This ensures both bootstrap commands read from the same cached autoconf data
282
283 // Load test autoconf data
284 autoConfData := loadTestDataComprehensive(t, "valid_autoconf.json")
285
286 // Track HTTP requests to verify daemon fetches autoconf
287 var requestCount atomic.Int32
288 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
289 requestCount.Add(1)
290 t.Logf("Bootstrap consistency test request: %s %s", r.Method, r.URL.Path)
291 w.Header().Set("Content-Type", "application/json")
292 _, _ = w.Write(autoConfData)
293 }))
294 defer server.Close()
295
296 // Create IPFS node with auto bootstrap
297 node := harness.NewT(t).NewNode().Init("--profile=test")
298 node.SetIPFSConfig("AutoConf.URL", server.URL)
299 node.SetIPFSConfig("AutoConf.Enabled", true)
300 node.SetIPFSConfig("Bootstrap", []string{"auto"})
301
302 // Start daemon and wait for autoconf fetch
303 daemon := startDaemonAndWaitForAutoConf(t, node, &requestCount)
304 defer daemon.StopDaemon()
305
306 // Get bootstrap via config command
307 configResult := node.RunIPFS("config", "Bootstrap", "--expand-auto")
308 require.Equal(t, 0, configResult.ExitCode(), "config Bootstrap --expand-auto should succeed")
309
310 // Get bootstrap via bootstrap command
311 bootstrapResult := node.RunIPFS("bootstrap", "list", "--expand-auto")
312 require.Equal(t, 0, bootstrapResult.ExitCode(), "bootstrap list --expand-auto should succeed")
313
314 // Parse both results
315 var configBootstrap, bootstrapBootstrap []string
316 err := json.Unmarshal([]byte(configResult.Stdout.String()), &configBootstrap)
317 require.NoError(t, err)
318
319 // Bootstrap command output is line-separated, not JSON
320 bootstrapOutput := strings.TrimSpace(bootstrapResult.Stdout.String())
321 if bootstrapOutput != "" {
322 bootstrapBootstrap = strings.Split(bootstrapOutput, "\n")
323 }
324
325 // Results should be equivalent
326 assert.Equal(t, len(configBootstrap), len(bootstrapBootstrap), "Both commands should return same number of peers")
327
328 // Both should contain same peers (order might differ due to different output formats)
329 for _, peer := range configBootstrap {
330 found := false
331 for _, bsPeer := range bootstrapBootstrap {
332 if strings.TrimSpace(bsPeer) == peer {
333 found = true
334 break
335 }
336 }
337 assert.True(t, found, "Peer %s should be in both results", peer)
338 }
339
340 t.Logf("Config command result: %v", configBootstrap)
341 t.Logf("Bootstrap command result: %v", bootstrapBootstrap)
342 }
343
344 // testWriteOperationsFailWithExpandAuto verifies that --expand-auto flag is properly
345 // restricted to read-only operations and fails when used with config write operations.
346 //
347 // This test is essential because:
348 // 1. It enforces the security principle that --expand-auto should only be used for reading
349 // 2. It prevents users from accidentally overwriting config with expanded values
350 // 3. It ensures that "auto" placeholders are preserved in the stored configuration
351 // 4. It validates proper error handling and user guidance when misused
352 // 5. It protects against accidental loss of the "auto" semantic meaning
353 func testWriteOperationsFailWithExpandAuto(t *testing.T) {
354 // Test scenario: CLI without daemon (tests error conditions)
355 // This test doesn't need daemon setup since it's testing that write operations
356 // with --expand-auto should fail with appropriate error messages
357
358 // Create IPFS node
359 node := harness.NewT(t).NewNode().Init("--profile=test")
360 node.SetIPFSConfig("Bootstrap", []string{"auto"})
361
362 // Test that setting config with --expand-auto fails
363 testCases := []struct {
364 name string
365 args []string
366 }{
367 {"config set with expand-auto", []string{"config", "Bootstrap", "[\"test\"]", "--expand-auto"}},
368 {"config set JSON with expand-auto", []string{"config", "Bootstrap", "[\"test\"]", "--json", "--expand-auto"}},
369 {"config set bool with expand-auto", []string{"config", "SomeField", "true", "--bool", "--expand-auto"}},
370 }
371
372 for _, tc := range testCases {
373 t.Run(tc.name, func(t *testing.T) {
374 result := node.RunIPFS(tc.args...)
375 assert.NotEqual(t, 0, result.ExitCode(), "Write operation with --expand-auto should fail")
376
377 stderr := result.Stderr.String()
378 assert.Contains(t, stderr, "--expand-auto", "Error should mention --expand-auto")
379 assert.Contains(t, stderr, "reading", "Error should mention reading limitation")
380 t.Logf("Expected error: %s", stderr)
381 })
382 }
383 }
384
385 // testConfigShowExpandAutoComplete verifies that `ipfs config show --expand-auto`
386 // produces a complete configuration with all "auto" values expanded to their resolved forms.
387 //
388 // This test is important because:
389 // 1. It validates the full-config expansion functionality for comprehensive troubleshooting
390 // 2. It ensures that users can see the complete resolved configuration state
391 // 3. It verifies that all "auto" placeholders are replaced, not just individual fields
392 // 4. It tests that the resulting JSON is valid and well-formed
393 // 5. It provides a way to export/backup the fully expanded configuration
394 func testConfigShowExpandAutoComplete(t *testing.T) {
395 // Test scenario: CLI with daemon started and autoconf cached
396
397 // Load test autoconf data
398 autoConfData := loadTestDataComprehensive(t, "valid_autoconf.json")
399
400 // Track HTTP requests to verify daemon fetches autoconf
401 var requestCount atomic.Int32
402 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
403 requestCount.Add(1)
404 t.Logf("Config show test request: %s %s", r.Method, r.URL.Path)
405 w.Header().Set("Content-Type", "application/json")
406 _, _ = w.Write(autoConfData)
407 }))
408 defer server.Close()
409
410 // Create IPFS node with multiple auto values
411 node := harness.NewT(t).NewNode().Init("--profile=test")
412 node.SetIPFSConfig("AutoConf.URL", server.URL)
413 node.SetIPFSConfig("AutoConf.Enabled", true)
414 node.SetIPFSConfig("Bootstrap", []string{"auto"})
415 node.SetIPFSConfig("DNS.Resolvers", map[string]string{".": "auto"})
416
417 // Start daemon and wait for autoconf fetch
418 daemon := startDaemonAndWaitForAutoConf(t, node, &requestCount)
419 defer daemon.StopDaemon()
420
421 // Test config show --expand-auto
422 result := node.RunIPFS("config", "show", "--expand-auto")
423 require.Equal(t, 0, result.ExitCode(), "config show --expand-auto should succeed")
424
425 expandedConfig := result.Stdout.String()
426
427 // Should not contain any literal "auto" values
428 assert.NotContains(t, expandedConfig, `"auto"`, "Expanded config should not contain literal 'auto' values")
429
430 // Should contain expected expanded sections
431 assert.Contains(t, expandedConfig, `"Bootstrap"`, "Should contain Bootstrap section")
432 assert.Contains(t, expandedConfig, `"DNS"`, "Should contain DNS section")
433 assert.Contains(t, expandedConfig, `"Resolvers"`, "Should contain Resolvers section")
434
435 // Should contain expanded peer addresses (not "auto")
436 assert.Contains(t, expandedConfig, "bootstrap.libp2p.io", "Should contain expanded bootstrap peers")
437
438 // Should be valid JSON
439 var configMap map[string]any
440 err := json.Unmarshal([]byte(expandedConfig), &configMap)
441 require.NoError(t, err, "Expanded config should be valid JSON")
442
443 // Verify specific fields were expanded
444 if bootstrap, ok := configMap["Bootstrap"].([]any); ok {
445 assert.Greater(t, len(bootstrap), 0, "Bootstrap should have expanded entries")
446 for _, peer := range bootstrap {
447 assert.NotEqual(t, "auto", peer, "Bootstrap entries should not be 'auto'")
448 }
449 }
450
451 t.Logf("Config show --expand-auto produced %d characters of expanded config", len(expandedConfig))
452 }
453
454 // testMultipleExpandAutoUsesCache verifies that multiple consecutive --expand-auto calls
455 // efficiently use cached autoconf data instead of making repeated HTTP requests.
456 //
457 // This test is critical for performance because:
458 // 1. It validates that the caching mechanism works correctly to reduce network overhead
459 // 2. It ensures that users can make multiple config queries without causing excessive HTTP traffic
460 // 3. It verifies that cached data is shared across different config fields and commands
461 // 4. It tests that HTTP headers (ETag/Last-Modified) are properly used for cache validation
462 // 5. It prevents regression where each --expand-auto call would trigger a new HTTP request
463 // 6. It demonstrates the performance benefit: 5 operations with only 1 network request
464 func testMultipleExpandAutoUsesCache(t *testing.T) {
465 // Test scenario: CLI with daemon started and autoconf cached
466
467 // Create comprehensive autoconf response
468 autoConfData := loadTestDataComprehensive(t, "valid_autoconf.json")
469
470 // Track HTTP requests to verify caching
471 var requestCount atomic.Int32
472 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
473 count := requestCount.Add(1)
474 t.Logf("AutoConf cache test request #%d: %s %s", count, r.Method, r.URL.Path)
475
476 w.Header().Set("Content-Type", "application/json")
477 w.Header().Set("ETag", `"cache-test-123"`)
478 w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
479 _, _ = w.Write(autoConfData)
480 }))
481 defer server.Close()
482
483 // Create IPFS node with all auto values
484 node := harness.NewT(t).NewNode().Init("--profile=test")
485 node.SetIPFSConfig("AutoConf.URL", server.URL)
486 node.SetIPFSConfig("AutoConf.Enabled", true)
487 // Note: Using default RefreshInterval (24h) to ensure caching - explicit setting would require rebuilt binary
488
489 // Set up auto values for multiple fields
490 node.SetIPFSConfig("Bootstrap", []string{"auto"})
491 node.SetIPFSConfig("DNS.Resolvers", map[string]string{"foo.": "auto"})
492 node.SetIPFSConfig("Routing.DelegatedRouters", []string{"auto"})
493 node.SetIPFSConfig("Ipns.DelegatedPublishers", []string{"auto"})
494
495 // Start daemon and wait for autoconf fetch
496 daemon := startDaemonAndWaitForAutoConf(t, node, &requestCount)
497 defer daemon.StopDaemon()
498
499 // Reset counter to only track our expand-auto calls
500 requestCount.Store(0)
501
502 // Make multiple --expand-auto calls on different fields
503 t.Log("Testing multiple --expand-auto calls should use cache...")
504
505 // Call 1: Bootstrap --expand-auto (should trigger HTTP request)
506 result1 := node.RunIPFS("config", "Bootstrap", "--expand-auto")
507 require.Equal(t, 0, result1.ExitCode(), "Bootstrap --expand-auto should succeed")
508
509 var expandedBootstrap []string
510 err := json.Unmarshal([]byte(result1.Stdout.String()), &expandedBootstrap)
511 require.NoError(t, err)
512 assert.NotContains(t, expandedBootstrap, "auto", "Bootstrap should be expanded")
513 assert.Greater(t, len(expandedBootstrap), 0, "Bootstrap should have entries")
514
515 // Call 2: DNS.Resolvers --expand-auto (should use cache, no HTTP)
516 result2 := node.RunIPFS("config", "DNS.Resolvers", "--expand-auto")
517 require.Equal(t, 0, result2.ExitCode(), "DNS.Resolvers --expand-auto should succeed")
518
519 var expandedResolvers map[string]string
520 err = json.Unmarshal([]byte(result2.Stdout.String()), &expandedResolvers)
521 require.NoError(t, err)
522
523 // Call 3: Routing.DelegatedRouters --expand-auto (should use cache, no HTTP)
524 result3 := node.RunIPFS("config", "Routing.DelegatedRouters", "--expand-auto")
525 require.Equal(t, 0, result3.ExitCode(), "Routing.DelegatedRouters --expand-auto should succeed")
526
527 var expandedRouters []string
528 err = json.Unmarshal([]byte(result3.Stdout.String()), &expandedRouters)
529 require.NoError(t, err)
530 assert.NotContains(t, expandedRouters, "auto", "Routers should be expanded")
531
532 // Call 4: Ipns.DelegatedPublishers --expand-auto (should use cache, no HTTP)
533 result4 := node.RunIPFS("config", "Ipns.DelegatedPublishers", "--expand-auto")
534 require.Equal(t, 0, result4.ExitCode(), "Ipns.DelegatedPublishers --expand-auto should succeed")
535
536 var expandedPublishers []string
537 err = json.Unmarshal([]byte(result4.Stdout.String()), &expandedPublishers)
538 require.NoError(t, err)
539 assert.NotContains(t, expandedPublishers, "auto", "Publishers should be expanded")
540
541 // Call 5: config show --expand-auto (should use cache, no HTTP)
542 result5 := node.RunIPFS("config", "show", "--expand-auto")
543 require.Equal(t, 0, result5.ExitCode(), "config show --expand-auto should succeed")
544
545 expandedConfig := result5.Stdout.String()
546 assert.NotContains(t, expandedConfig, `"auto"`, "Full config should not contain 'auto' values")
547
548 // CRITICAL TEST: Verify NO HTTP requests were made for --expand-auto calls (using cache)
549 finalRequestCount := requestCount.Load()
550 assert.Equal(t, int32(0), finalRequestCount,
551 "Multiple --expand-auto calls should result in 0 HTTP requests (using cache). Got %d requests", finalRequestCount)
552
553 t.Logf("Made 5 --expand-auto calls, resulted in %d HTTP request(s) - cache is being used!", finalRequestCount)
554
555 // Now simulate a manual cache refresh (what the background updater would do)
556 t.Log("Simulating manual cache refresh...")
557
558 // Update the mock server to return different data
559 autoConfData2 := loadTestDataComprehensive(t, "updated_autoconf.json")
560 server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
561 count := requestCount.Add(1)
562 t.Logf("Manual refresh request #%d: %s %s", count, r.Method, r.URL.Path)
563 w.Header().Set("Content-Type", "application/json")
564 w.Header().Set("ETag", `"cache-test-456"`)
565 w.Header().Set("Last-Modified", "Thu, 22 Oct 2015 08:00:00 GMT")
566 _, _ = w.Write(autoConfData2)
567 })
568
569 // Note: In the actual daemon, the background updater would call MustGetConfigWithRefresh
570 // For this test, we'll verify that subsequent --expand-auto calls still use cache
571 // and don't trigger additional requests
572
573 // Reset counter before manual refresh simulation
574 beforeRefresh := requestCount.Load()
575
576 // Make another --expand-auto call - should still use cache
577 result6 := node.RunIPFS("config", "Bootstrap", "--expand-auto")
578 require.Equal(t, 0, result6.ExitCode(), "Bootstrap --expand-auto after refresh should succeed")
579
580 afterRefresh := requestCount.Load()
581 assert.Equal(t, beforeRefresh, afterRefresh,
582 "--expand-auto should continue using cache even after server update")
583
584 t.Logf("Cache continues to be used after server update - background updater pattern confirmed!")
585 }
586
587 // testCLIUsesCacheOnlyDaemonUpdatesBackground verifies the correct autoconf behavior:
588 // daemon makes exactly one HTTP request during startup to fetch and cache data, then
589 // CLI commands always use cached data without making additional HTTP requests.
590 //
591 // This test is essential for correctness because:
592 // 1. It validates that daemon startup makes exactly one HTTP request to fetch autoconf
593 // 2. It verifies that CLI --expand-auto never makes HTTP requests (uses cache only)
594 // 3. It ensures CLI commands remain fast by always using cached data
595 // 4. It prevents regression where CLI commands might start making HTTP requests
596 // 5. It confirms the correct separation between daemon (network) and CLI (cache-only) behavior
597 func testCLIUsesCacheOnlyDaemonUpdatesBackground(t *testing.T) {
598 // Test scenario: CLI with daemon and long RefreshInterval (no background updates during test)
599
600 // Create autoconf response
601 autoConfData := loadTestDataComprehensive(t, "valid_autoconf.json")
602
603 // Track HTTP requests with timestamps
604 var requestCount atomic.Int32
605 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
606 count := requestCount.Add(1)
607 t.Logf("Cache expiry test request #%d at %s: %s %s", count, time.Now().Format("15:04:05.000"), r.Method, r.URL.Path)
608
609 w.Header().Set("Content-Type", "application/json")
610 // Use different ETag for each request to ensure we can detect new fetches
611 w.Header().Set("ETag", fmt.Sprintf(`"expiry-test-%d"`, count))
612 w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat))
613 _, _ = w.Write(autoConfData)
614 }))
615 defer server.Close()
616
617 // Create IPFS node with long refresh interval
618 node := harness.NewT(t).NewNode().Init("--profile=test")
619 node.SetIPFSConfig("AutoConf.URL", server.URL)
620 node.SetIPFSConfig("AutoConf.Enabled", true)
621 // Set long RefreshInterval to avoid background updates during test
622 node.SetIPFSConfig("AutoConf.RefreshInterval", "1h")
623
624 node.SetIPFSConfig("Bootstrap", []string{"auto"})
625 node.SetIPFSConfig("DNS.Resolvers", map[string]string{"test.": "auto"})
626
627 // Start daemon and wait for autoconf fetch
628 daemon := startDaemonAndWaitForAutoConf(t, node, &requestCount)
629 defer daemon.StopDaemon()
630
631 // Confirm only one request was made during daemon startup
632 initialRequestCount := requestCount.Load()
633 assert.Equal(t, int32(1), initialRequestCount, "Expected exactly 1 HTTP request during daemon startup, got: %d", initialRequestCount)
634 t.Logf("Daemon startup made exactly 1 HTTP request")
635
636 // Test: CLI commands use cache only (no additional HTTP requests)
637 t.Log("Testing that CLI --expand-auto commands use cache only...")
638
639 // Make several CLI calls - none should trigger HTTP requests
640 result1 := node.RunIPFS("config", "Bootstrap", "--expand-auto")
641 require.Equal(t, 0, result1.ExitCode(), "Bootstrap --expand-auto should succeed")
642
643 result2 := node.RunIPFS("config", "DNS.Resolvers", "--expand-auto")
644 require.Equal(t, 0, result2.ExitCode(), "DNS.Resolvers --expand-auto should succeed")
645
646 result3 := node.RunIPFS("config", "Routing.DelegatedRouters", "--expand-auto")
647 require.Equal(t, 0, result3.ExitCode(), "Routing.DelegatedRouters --expand-auto should succeed")
648
649 // Verify the request count remains at 1 (no additional requests from CLI)
650 finalRequestCount := requestCount.Load()
651 assert.Equal(t, int32(1), finalRequestCount, "Request count should remain at 1 after CLI commands, got: %d", finalRequestCount)
652 t.Log("CLI commands use cache only - request count remains at 1")
653
654 t.Log("Test completed: Daemon makes 1 startup request, CLI commands use cache only")
655 }
656
657 // loadTestDataComprehensive is a helper function that loads test autoconf JSON data files.
658 // It locates the test data directory relative to the test file and reads the specified file.
659 // This centralized helper ensures consistent test data loading across all comprehensive tests.
660 func loadTestDataComprehensive(t *testing.T, filename string) []byte {
661 t.Helper()
662
663 data, err := os.ReadFile("testdata/" + filename)
664 require.NoError(t, err, "Failed to read test data file: %s", filename)
665
666 return data
667 }
668
669 // startDaemonAndWaitForAutoConf starts a daemon and waits for it to fetch autoconf data.
670 // It returns the node with daemon running and ensures autoconf has been cached before returning.
671 // This is a DRY helper to avoid repeating daemon setup and request waiting logic in every test.
672 func startDaemonAndWaitForAutoConf(t *testing.T, node *harness.Node, requestCount *atomic.Int32) *harness.Node {
673 t.Helper()
674
675 // Start daemon to fetch and cache autoconf data
676 t.Log("Starting daemon to fetch and cache autoconf data...")
677 daemon := node.StartDaemon()
678 // StartDaemon returns *Node, no error to check
679
680 // Wait for daemon to fetch autoconf (wait for HTTP request to mock server)
681 t.Log("Waiting for daemon to fetch autoconf from mock server...")
682 timeout := time.After(10 * time.Second) // Safety timeout
683 ticker := time.NewTicker(10 * time.Millisecond)
684 defer ticker.Stop()
685
686 for {
687 select {
688 case <-timeout:
689 t.Fatal("Timeout waiting for autoconf fetch")
690 case <-ticker.C:
691 if requestCount.Load() > 0 {
692 t.Logf("Daemon fetched autoconf (%d requests made)", requestCount.Load())
693 t.Log("AutoConf should now be cached by daemon")
694 return daemon
695 }
696 }
697 }
698 }