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