package autoconf import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "strings" "sync/atomic" "testing" "time" "github.com/ipfs/kubo/test/cli/harness" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAutoConf(t *testing.T) { t.Parallel() t.Run("basic functionality", func(t *testing.T) { t.Parallel() testAutoConfBasicFunctionality(t) }) t.Run("background service updates", func(t *testing.T) { t.Parallel() testAutoConfBackgroundService(t) }) t.Run("HTTP error scenarios", func(t *testing.T) { t.Parallel() testAutoConfHTTPErrors(t) }) t.Run("cache-based config expansion", func(t *testing.T) { t.Parallel() testAutoConfCacheBasedExpansion(t) }) t.Run("disabled autoconf", func(t *testing.T) { t.Parallel() testAutoConfDisabled(t) }) t.Run("bootstrap list shows auto as-is", func(t *testing.T) { t.Parallel() testBootstrapListResolved(t) }) t.Run("daemon uses resolved bootstrap values", func(t *testing.T) { t.Parallel() testDaemonUsesResolvedBootstrap(t) }) t.Run("empty cache uses fallback defaults", func(t *testing.T) { t.Parallel() testEmptyCacheUsesFallbacks(t) }) t.Run("stale cache with unreachable server", func(t *testing.T) { t.Parallel() testStaleCacheWithUnreachableServer(t) }) t.Run("autoconf disabled with auto values", func(t *testing.T) { t.Parallel() testAutoConfDisabledWithAutoValues(t) }) t.Run("network behavior - cached vs refresh", func(t *testing.T) { t.Parallel() testAutoConfNetworkBehavior(t) }) t.Run("HTTPS autoconf server", func(t *testing.T) { t.Parallel() testAutoConfWithHTTPS(t) }) } func testAutoConfBasicFunctionality(t *testing.T) { // Load test autoconf data autoConfData := loadTestData(t, "valid_autoconf.json") // Create HTTP server that serves autoconf.json etag := `"test-etag-123"` requestCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ t.Logf("AutoConf server request #%d: %s %s", requestCount, r.Method, r.URL.Path) w.Header().Set("Content-Type", "application/json") w.Header().Set("ETag", etag) w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") _, _ = w.Write(autoConfData) })) defer server.Close() // Create IPFS node and configure it to use our test server // Use test profile to avoid autoconf profile being applied by default node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) // Disable background updates to prevent multiple requests node.SetIPFSConfig("AutoConf.RefreshInterval", "24h") // Test with normal bootstrap peers (not "auto") to avoid multiaddr parsing issues // This tests that autoconf fetching works without complex auto replacement node.SetIPFSConfig("Bootstrap", []string{"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"}) // Start daemon to trigger autoconf fetch node.StartDaemon() defer node.StopDaemon() // Give autoconf some time to fetch time.Sleep(2 * time.Second) // Verify that the autoconf system fetched data from our server t.Logf("Server request count: %d", requestCount) require.GreaterOrEqual(t, requestCount, 1, "AutoConf server should have been called at least once") // Test that daemon is functional result := node.RunIPFS("id") assert.Equal(t, 0, result.ExitCode(), "IPFS daemon should be responsive") assert.Contains(t, result.Stdout.String(), "ID", "IPFS id command should return peer information") // Success! AutoConf system is working: // 1. Server was called (proves fetch works) // 2. Daemon started successfully (proves DNS resolver validation is fixed) // 3. Daemon is functional (proves autoconf doesn't break core functionality) // Note: We skip checking metadata values due to JSON parsing complexity in test harness } func testAutoConfBackgroundService(t *testing.T) { // Test that the startAutoConfUpdater() goroutine makes network requests for background refresh // This is separate from daemon config operations which now use cache-first approach // Load initial and updated test data initialData := loadTestData(t, "valid_autoconf.json") updatedData := loadTestData(t, "updated_autoconf.json") // Track which config is being served currentData := initialData var requestCount atomic.Int32 // Create server that switches payload after first request server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := requestCount.Add(1) t.Logf("Background service request #%d from %s", count, r.UserAgent()) w.Header().Set("Content-Type", "application/json") w.Header().Set("ETag", fmt.Sprintf(`"background-test-etag-%d"`, count)) w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat)) if count > 1 { // After first request, serve updated config currentData = updatedData } _, _ = w.Write(currentData) })) defer server.Close() // Create IPFS node with short refresh interval to trigger background service node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("AutoConf.RefreshInterval", "1s") // Very short for testing background service // Use normal bootstrap values to avoid dependency on autoconf during initialization node.SetIPFSConfig("Bootstrap", []string{"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"}) // Start daemon - this should start the background service via startAutoConfUpdater() node.StartDaemon() defer node.StopDaemon() // Wait for initial request (daemon startup may trigger one) time.Sleep(1 * time.Second) initialCount := requestCount.Load() t.Logf("Initial request count after daemon start: %d", initialCount) // Wait for background service to make additional requests // The background service should make requests at the RefreshInterval (1s) time.Sleep(3 * time.Second) finalCount := requestCount.Load() t.Logf("Final request count after background updates: %d", finalCount) // Background service should have made multiple requests due to 1s refresh interval assert.Greater(t, finalCount, initialCount, "Background service should have made additional requests beyond daemon startup") // Verify that the service is actively making requests (not just relying on cache) assert.GreaterOrEqual(t, finalCount, int32(2), "Should have at least 2 requests total (startup + background refresh)") t.Logf("Successfully verified startAutoConfUpdater() background service makes network requests") } func testAutoConfHTTPErrors(t *testing.T) { tests := []struct { name string statusCode int body string }{ {"404 Not Found", http.StatusNotFound, "Not Found"}, {"500 Internal Server Error", http.StatusInternalServerError, "Internal Server Error"}, {"Invalid JSON", http.StatusOK, "invalid json content"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create server that returns error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tt.statusCode) _, _ = w.Write([]byte(tt.body)) })) defer server.Close() // Create node with failing AutoConf URL // Use test profile to avoid autoconf profile being applied by default node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Start daemon - it should start but autoconf should fail gracefully node.StartDaemon() defer node.StopDaemon() // Daemon should still be functional even with autoconf HTTP errors result := node.RunIPFS("version") assert.Equal(t, 0, result.ExitCode(), "Daemon should start even with HTTP errors in autoconf") }) } } func testAutoConfCacheBasedExpansion(t *testing.T) { // Test that config expansion works correctly with cached autoconf data // without requiring active network requests during expansion operations autoConfData := loadTestData(t, "valid_autoconf.json") // Create server that serves autoconf data server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("ETag", `"cache-test-etag"`) w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") _, _ = w.Write(autoConfData) })) defer server.Close() // Create IPFS node with autoconf enabled node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) // Set configuration with "auto" values to test expansion node.SetIPFSConfig("Bootstrap", []string{"auto"}) node.SetIPFSConfig("Routing.DelegatedRouters", []string{"auto"}) node.SetIPFSConfig("DNS.Resolvers", map[string]string{"test.": "auto"}) // Populate cache by running a command that triggers autoconf (without daemon) result := node.RunIPFS("bootstrap", "list", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "Initial bootstrap expansion should succeed") expandedBootstrap := result.Stdout.String() assert.NotContains(t, expandedBootstrap, "auto", "Expanded bootstrap should not contain 'auto' literal") assert.Greater(t, len(strings.Fields(expandedBootstrap)), 0, "Should have expanded bootstrap peers") // Test that subsequent config operations work with cached data (no network required) // This simulates the cache-first behavior our architecture now uses // Test Bootstrap expansion result = node.RunIPFS("config", "Bootstrap", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "Cached bootstrap expansion should succeed") var expandedBootstrapList []string err := json.Unmarshal([]byte(result.Stdout.String()), &expandedBootstrapList) require.NoError(t, err) assert.NotContains(t, expandedBootstrapList, "auto", "Expanded bootstrap list should not contain 'auto'") assert.Greater(t, len(expandedBootstrapList), 0, "Should have expanded bootstrap peers from cache") // Test Routing.DelegatedRouters expansion result = node.RunIPFS("config", "Routing.DelegatedRouters", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "Cached router expansion should succeed") var expandedRouters []string err = json.Unmarshal([]byte(result.Stdout.String()), &expandedRouters) require.NoError(t, err) assert.NotContains(t, expandedRouters, "auto", "Expanded routers should not contain 'auto'") // Test DNS.Resolvers expansion result = node.RunIPFS("config", "DNS.Resolvers", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "Cached DNS resolver expansion should succeed") var expandedResolvers map[string]string err = json.Unmarshal([]byte(result.Stdout.String()), &expandedResolvers) require.NoError(t, err) // Should have expanded the "auto" value for test. domain, or removed it if no autoconf data available testResolver, exists := expandedResolvers["test."] if exists { assert.NotEqual(t, "auto", testResolver, "test. resolver should not be literal 'auto'") t.Logf("Found expanded resolver for test.: %s", testResolver) } else { t.Logf("No resolver found for test. domain (autoconf may not have DNS resolver data)") } // Test full config expansion result = node.RunIPFS("config", "show", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "Full config expansion should succeed") expandedConfig := result.Stdout.String() // Should not contain literal "auto" values after expansion assert.NotContains(t, expandedConfig, `"auto"`, "Expanded config should not contain literal 'auto' values") assert.Contains(t, expandedConfig, `"Bootstrap"`, "Should contain Bootstrap section") assert.Contains(t, expandedConfig, `"DNS"`, "Should contain DNS section") t.Logf("Successfully tested cache-based config expansion without active network requests") } func testAutoConfDisabled(t *testing.T) { // Create node with AutoConf disabled but "auto" values // Use test profile to avoid autoconf profile being applied by default node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.Enabled", false) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Test by trying to list bootstrap - when AutoConf is disabled, it should show literal "auto" result := node.RunIPFS("bootstrap", "list") if result.ExitCode() == 0 { // If command succeeds, it should show literal "auto" (no resolution) output := result.Stdout.String() assert.Contains(t, output, "auto", "Should show literal 'auto' when AutoConf is disabled") } else { // If command fails, error should mention autoconf issue assert.Contains(t, result.Stderr.String(), "auto", "Should mention 'auto' values in error") } } // Helper function to load test data files func loadTestData(t *testing.T, filename string) []byte { t.Helper() data, err := os.ReadFile("testdata/" + filename) require.NoError(t, err, "Failed to read test data file: %s", filename) return data } func testBootstrapListResolved(t *testing.T) { // Test that bootstrap list shows "auto" as-is (not expanded) // Load test autoconf data autoConfData := loadTestData(t, "valid_autoconf.json") // Create HTTP server that serves autoconf.json server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(autoConfData) })) defer server.Close() // Create IPFS node with "auto" bootstrap value node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Test 1: bootstrap list (without --expand-auto) shows "auto" as-is - NO DAEMON NEEDED! result := node.RunIPFS("bootstrap", "list") require.Equal(t, 0, result.ExitCode(), "bootstrap list command should succeed") output := result.Stdout.String() t.Logf("Bootstrap list output: %s", output) assert.Contains(t, output, "auto", "bootstrap list should show 'auto' value as-is") // Should NOT contain expanded bootstrap peers without --expand-auto unexpectedPeers := []string{ "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", } for _, peer := range unexpectedPeers { assert.NotContains(t, output, peer, "bootstrap list should not contain expanded peer: %s", peer) } // Test 2: bootstrap list --expand-auto shows expanded values (no daemon needed!) result = node.RunIPFS("bootstrap", "list", "--expand-auto") require.Equal(t, 0, result.ExitCode(), "bootstrap list --expand-auto command should succeed") expandedOutput := result.Stdout.String() t.Logf("Bootstrap list --expand-auto output: %s", expandedOutput) // Should NOT contain "auto" literal when expanded assert.NotContains(t, expandedOutput, "auto", "bootstrap list --expand-auto should not show 'auto' literal") // Should contain at least one expanded bootstrap peer expectedPeers := []string{ "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", } foundExpectedPeer := false for _, peer := range expectedPeers { if strings.Contains(expandedOutput, peer) { foundExpectedPeer = true t.Logf("Found expected expanded peer: %s", peer) break } } assert.True(t, foundExpectedPeer, "bootstrap list --expand-auto should contain at least one expanded bootstrap peer") } func testDaemonUsesResolvedBootstrap(t *testing.T) { // Test that daemon actually uses expanded bootstrap values for P2P connections // even though bootstrap list shows "auto" // Step 1: Create bootstrap node (target for connections) bootstrapNode := harness.NewT(t).NewNode().Init("--profile=test") // Set a specific swarm port for the bootstrap node to avoid port 0 issues bootstrapNode.SetIPFSConfig("Addresses.Swarm", []string{"/ip4/127.0.0.1/tcp/14001"}) // Disable routing and discovery to ensure it's only discoverable via explicit multiaddr bootstrapNode.SetIPFSConfig("Routing.Type", "none") bootstrapNode.SetIPFSConfig("Discovery.MDNS.Enabled", false) bootstrapNode.SetIPFSConfig("Bootstrap", []string{}) // No bootstrap peers // Start the bootstrap node first bootstrapNode.StartDaemon() defer bootstrapNode.StopDaemon() // Get bootstrap node's peer ID and swarm address bootstrapPeerID := bootstrapNode.PeerID() // Use the configured swarm address (we set it to a specific port above) bootstrapMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/14001/p2p/%s", bootstrapPeerID.String()) t.Logf("Bootstrap node configured at: %s", bootstrapMultiaddr) // Step 2: Create autoconf server that returns bootstrap node's address autoConfData := fmt.Sprintf(`{ "AutoConfVersion": 2025072301, "AutoConfSchema": 1, "AutoConfTTL": 86400, "SystemRegistry": { "AminoDHT": { "Description": "Test AminoDHT system", "NativeConfig": { "Bootstrap": ["%s"] } } }, "DNSResolvers": {}, "DelegatedEndpoints": {} }`, bootstrapMultiaddr) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(autoConfData)) })) defer server.Close() // Step 3: Create autoconf-enabled node that should connect to bootstrap node autoconfNode := harness.NewT(t).NewNode().Init("--profile=test") autoconfNode.SetIPFSConfig("AutoConf.URL", server.URL) autoconfNode.SetIPFSConfig("AutoConf.Enabled", true) autoconfNode.SetIPFSConfig("Bootstrap", []string{"auto"}) // This should resolve to bootstrap node // Disable other discovery methods to force bootstrap-only connectivity autoconfNode.SetIPFSConfig("Routing.Type", "none") autoconfNode.SetIPFSConfig("Discovery.MDNS.Enabled", false) // Start the autoconf node autoconfNode.StartDaemon() defer autoconfNode.StopDaemon() // Step 4: Give time for autoconf resolution and connection attempts time.Sleep(8 * time.Second) // Step 5: Verify both nodes are responsive result := bootstrapNode.RunIPFS("id") require.Equal(t, 0, result.ExitCode(), "Bootstrap node should be responsive: %s", result.Stderr.String()) result = autoconfNode.RunIPFS("id") require.Equal(t, 0, result.ExitCode(), "AutoConf node should be responsive: %s", result.Stderr.String()) // Step 6: Verify that autoconf node connected to bootstrap node // Check swarm peers on autoconf node - it should show bootstrap node's peer ID result = autoconfNode.RunIPFS("swarm", "peers") if result.ExitCode() == 0 { peerOutput := result.Stdout.String() if strings.Contains(peerOutput, bootstrapPeerID.String()) { t.Logf("SUCCESS: AutoConf node connected to bootstrap peer %s", bootstrapPeerID.String()) } else { t.Logf("No active connection found. Peers output: %s", peerOutput) // This might be OK if connection attempt was made but didn't persist } } else { // If swarm peers fails, try alternative verification via daemon logs t.Logf("Swarm peers command failed, checking daemon logs for connection attempts") daemonOutput := autoconfNode.Daemon.Stderr.String() if strings.Contains(daemonOutput, bootstrapPeerID.String()) { t.Logf("SUCCESS: Found bootstrap peer %s in daemon logs, connection attempted", bootstrapPeerID.String()) } else { t.Logf("Daemon stderr: %s", daemonOutput) } } // Step 7: Verify bootstrap configuration still shows "auto" (not resolved values) result = autoconfNode.RunIPFS("bootstrap", "list") require.Equal(t, 0, result.ExitCode(), "Bootstrap list command should work") assert.Contains(t, result.Stdout.String(), "auto", "Bootstrap list should still show 'auto' even though values were resolved for networking") } func testEmptyCacheUsesFallbacks(t *testing.T) { // Test that daemon uses fallback defaults when no cache exists and server is unreachable // Create IPFS node with auto values and unreachable autoconf server node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", "http://127.0.0.1:9999/nonexistent") node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("Bootstrap", []string{"auto"}) node.SetIPFSConfig("Routing.DelegatedRouters", []string{"auto"}) // Start daemon - should succeed using fallback values node.StartDaemon() defer node.StopDaemon() // Verify daemon started successfully (uses fallback bootstrap) result := node.RunIPFS("id") require.Equal(t, 0, result.ExitCode(), "Daemon should start successfully with fallback values") // Verify config commands still show "auto" result = node.RunIPFS("config", "Bootstrap") require.Equal(t, 0, result.ExitCode()) assert.Contains(t, result.Stdout.String(), "auto", "Bootstrap config should still show 'auto'") result = node.RunIPFS("config", "Routing.DelegatedRouters") require.Equal(t, 0, result.ExitCode()) assert.Contains(t, result.Stdout.String(), "auto", "DelegatedRouters config should still show 'auto'") // Check daemon logs for error about failed autoconf fetch logOutput := node.Daemon.Stderr.String() // The daemon should attempt to fetch autoconf but will use fallbacks on failure // We don't require specific log messages as long as the daemon starts successfully if logOutput != "" { t.Logf("Daemon logs: %s", logOutput) } } func testStaleCacheWithUnreachableServer(t *testing.T) { // Test that daemon uses stale cache when server is unreachable // First create a working autoconf server and cache autoConfData := loadTestData(t, "valid_autoconf.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write(autoConfData) })) // Create node and fetch autoconf to populate cache node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Start daemon briefly to populate cache node.StartDaemon() time.Sleep(1 * time.Second) // Allow cache population node.StopDaemon() // Close the server to make it unreachable server.Close() // Update config to point to unreachable server node.SetIPFSConfig("AutoConf.URL", "http://127.0.0.1:9999/unreachable") // Start daemon again - should use stale cache node.StartDaemon() defer node.StopDaemon() // Verify daemon started successfully (uses cached autoconf) result := node.RunIPFS("id") require.Equal(t, 0, result.ExitCode(), "Daemon should start successfully with cached autoconf") // Check daemon logs for error about using stale config logOutput := node.Daemon.Stderr.String() // The daemon should use cached config when server is unreachable // We don't require specific log messages as long as the daemon starts successfully if logOutput != "" { t.Logf("Daemon logs: %s", logOutput) } } func testAutoConfDisabledWithAutoValues(t *testing.T) { // Test that daemon fails to start when AutoConf is disabled but "auto" values are present // Create IPFS node with AutoConf disabled but "auto" values configured node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.Enabled", false) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Test by trying to list bootstrap - when AutoConf is disabled, it should show literal "auto" result := node.RunIPFS("bootstrap", "list") if result.ExitCode() == 0 { // If command succeeds, it should show literal "auto" (no resolution) output := result.Stdout.String() assert.Contains(t, output, "auto", "Should show literal 'auto' when AutoConf is disabled") } else { // If command fails, error should mention autoconf issue logOutput := result.Stderr.String() assert.Contains(t, logOutput, "auto", "Error should mention 'auto' values") // Check that the error message contains information about disabled state assert.True(t, strings.Contains(logOutput, "disabled") || strings.Contains(logOutput, "AutoConf.Enabled=false"), "Error should mention that AutoConf is disabled or show AutoConf.Enabled=false") } } func testAutoConfNetworkBehavior(t *testing.T) { // Test the network behavior differences between MustGetConfigCached and MustGetConfigWithRefresh // This validates that our cache-first architecture works as expected autoConfData := loadTestData(t, "valid_autoconf.json") var requestCount atomic.Int32 // Create server that tracks all requests server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { count := requestCount.Add(1) t.Logf("Network behavior test request #%d: %s %s", count, r.Method, r.URL.Path) w.Header().Set("Content-Type", "application/json") w.Header().Set("ETag", fmt.Sprintf(`"network-test-etag-%d"`, count)) w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat)) _, _ = w.Write(autoConfData) })) defer server.Close() // Create IPFS node with autoconf node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("Bootstrap", []string{"auto"}) // Phase 1: Test cache-first behavior (no network requests expected) t.Logf("=== Phase 1: Testing cache-first behavior ===") initialCount := requestCount.Load() // Multiple config operations should NOT trigger network requests (cache-first) result := node.RunIPFS("config", "Bootstrap") require.Equal(t, 0, result.ExitCode(), "Bootstrap config read should succeed") result = node.RunIPFS("config", "show") require.Equal(t, 0, result.ExitCode(), "Config show should succeed") result = node.RunIPFS("bootstrap", "list") require.Equal(t, 0, result.ExitCode(), "Bootstrap list should succeed") // Check that cache-first operations didn't trigger network requests afterCacheOpsCount := requestCount.Load() cachedRequestDiff := afterCacheOpsCount - initialCount t.Logf("Network requests during cache-first operations: %d", cachedRequestDiff) // Phase 2: Test explicit expansion (may trigger cache population) t.Logf("=== Phase 2: Testing expansion operations ===") beforeExpansionCount := requestCount.Load() // Expansion operations may need to populate cache if empty result = node.RunIPFS("bootstrap", "list", "--expand-auto") if result.ExitCode() == 0 { output := result.Stdout.String() assert.NotContains(t, output, "auto", "Expanded bootstrap should not contain 'auto' literal") t.Logf("Bootstrap expansion succeeded") } else { t.Logf("Bootstrap expansion failed (may be due to network/cache issues): %s", result.Stderr.String()) } result = node.RunIPFS("config", "Bootstrap", "--expand-auto") if result.ExitCode() == 0 { t.Logf("Config Bootstrap expansion succeeded") } else { t.Logf("Config Bootstrap expansion failed: %s", result.Stderr.String()) } afterExpansionCount := requestCount.Load() expansionRequestDiff := afterExpansionCount - beforeExpansionCount t.Logf("Network requests during expansion operations: %d", expansionRequestDiff) // Phase 3: Test background service behavior (if daemon is started) t.Logf("=== Phase 3: Testing background service behavior ===") beforeDaemonCount := requestCount.Load() // Set short refresh interval to test background service node.SetIPFSConfig("AutoConf.RefreshInterval", "1s") // Start daemon - this triggers startAutoConfUpdater() which should make network requests node.StartDaemon() defer node.StopDaemon() // Wait for background service to potentially make requests time.Sleep(2 * time.Second) afterDaemonCount := requestCount.Load() daemonRequestDiff := afterDaemonCount - beforeDaemonCount t.Logf("Network requests from background service: %d", daemonRequestDiff) // Verify expected behavior patterns t.Logf("=== Summary ===") t.Logf("Cache-first operations: %d requests", cachedRequestDiff) t.Logf("Expansion operations: %d requests", expansionRequestDiff) t.Logf("Background service: %d requests", daemonRequestDiff) // Cache-first operations should minimize network requests assert.LessOrEqual(t, cachedRequestDiff, int32(1), "Cache-first config operations should make minimal network requests") // Background service should make requests for refresh if daemonRequestDiff > 0 { t.Logf("✓ Background service is making network requests as expected") } else { t.Logf("⚠ Background service made no requests (may be using existing cache)") } t.Logf("Successfully verified network behavior patterns in autoconf architecture") } func testAutoConfWithHTTPS(t *testing.T) { // Test autoconf with HTTPS server and TLSInsecureSkipVerify enabled autoConfData := loadTestData(t, "valid_autoconf.json") // Create HTTPS server with self-signed certificate server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("HTTPS autoconf request from %s", r.UserAgent()) w.Header().Set("Content-Type", "application/json") w.Header().Set("ETag", `"https-test-etag"`) w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT") _, _ = w.Write(autoConfData) })) // Enable HTTP/2 and start with TLS (self-signed certificate) server.EnableHTTP2 = true server.StartTLS() defer server.Close() // Create IPFS node with HTTPS autoconf server and TLS skip verify node := harness.NewT(t).NewNode().Init("--profile=test") node.SetIPFSConfig("AutoConf.URL", server.URL) node.SetIPFSConfig("AutoConf.Enabled", true) node.SetIPFSConfig("AutoConf.TLSInsecureSkipVerify", true) // Allow self-signed cert node.SetIPFSConfig("AutoConf.RefreshInterval", "24h") // Disable background updates // Use normal bootstrap peers to test HTTPS fetching without complex auto replacement node.SetIPFSConfig("Bootstrap", []string{"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"}) // Start daemon to trigger HTTPS autoconf fetch node.StartDaemon() defer node.StopDaemon() // Give autoconf time to fetch over HTTPS time.Sleep(2 * time.Second) // Verify daemon is functional with HTTPS autoconf result := node.RunIPFS("id") assert.Equal(t, 0, result.ExitCode(), "IPFS daemon should be responsive with HTTPS autoconf") assert.Contains(t, result.Stdout.String(), "ID", "IPFS id command should return peer information") // Test that config operations work with HTTPS-fetched autoconf cache result = node.RunIPFS("config", "show") assert.Equal(t, 0, result.ExitCode(), "Config show should work with HTTPS autoconf") // Test bootstrap list functionality result = node.RunIPFS("bootstrap", "list") assert.Equal(t, 0, result.ExitCode(), "Bootstrap list should work with HTTPS autoconf") t.Logf("Successfully tested AutoConf with HTTPS server and TLS skip verify") }