| 1 | package autoconf |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "slices" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/ipfs/kubo/config" |
| 13 | "github.com/ipfs/kubo/test/cli/harness" |
| 14 | "github.com/stretchr/testify/require" |
| 15 | ) |
| 16 | |
| 17 | // TestAutoConfExtensibility_NewSystem verifies that the AutoConf system can be extended |
| 18 | // with new routing systems beyond the default AminoDHT and IPNI. |
| 19 | // |
| 20 | // The test verifies that: |
| 21 | // 1. New systems can be added via AutoConf's SystemRegistry |
| 22 | // 2. Native vs delegated system filtering works correctly: |
| 23 | // - Native systems (AminoDHT) provide bootstrap peers and are used for P2P routing |
| 24 | // - Delegated systems (IPNI, NewSystem) provide HTTP endpoints for delegated routing |
| 25 | // |
| 26 | // 3. The system correctly filters endpoints based on routing type |
| 27 | // |
| 28 | // Note: Only native systems contribute bootstrap peers. Delegated systems like "NewSystem" |
| 29 | // only provide HTTP routing endpoints, not P2P bootstrap peers. |
| 30 | func TestAutoConfExtensibility_NewSystem(t *testing.T) { |
| 31 | if testing.Short() { |
| 32 | t.Skip("skipping test in short mode") |
| 33 | } |
| 34 | |
| 35 | // Setup mock autoconf server with NewSystem |
| 36 | var mockServer *httptest.Server |
| 37 | mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 38 | // Create autoconf.json with NewSystem |
| 39 | autoconfData := map[string]any{ |
| 40 | "AutoConfVersion": 2025072901, |
| 41 | "AutoConfSchema": 1, |
| 42 | "AutoConfTTL": 86400, |
| 43 | "SystemRegistry": map[string]any{ |
| 44 | "AminoDHT": map[string]any{ |
| 45 | "URL": "https://github.com/ipfs/specs/pull/497", |
| 46 | "Description": "Public DHT swarm", |
| 47 | "NativeConfig": map[string]any{ |
| 48 | "Bootstrap": []string{ |
| 49 | "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", |
| 50 | }, |
| 51 | }, |
| 52 | "DelegatedConfig": map[string]any{ |
| 53 | "Read": []string{"/routing/v1/providers", "/routing/v1/peers", "/routing/v1/ipns"}, |
| 54 | "Write": []string{"/routing/v1/ipns"}, |
| 55 | }, |
| 56 | }, |
| 57 | "IPNI": map[string]any{ |
| 58 | "URL": "https://ipni.example.com", |
| 59 | "Description": "Network Indexer", |
| 60 | "DelegatedConfig": map[string]any{ |
| 61 | "Read": []string{"/routing/v1/providers"}, |
| 62 | "Write": []string{}, |
| 63 | }, |
| 64 | }, |
| 65 | "NewSystem": map[string]any{ |
| 66 | "URL": "https://example.com/newsystem", |
| 67 | "Description": "Test system for extensibility verification", |
| 68 | "NativeConfig": map[string]any{ |
| 69 | "Bootstrap": []string{ |
| 70 | "/ip4/127.0.0.1/tcp/9999/p2p/12D3KooWPeQ4r3v6CmVmKXoFGtqEqcr3L8P6La9yH5oEWKtoLVVa", |
| 71 | }, |
| 72 | }, |
| 73 | "DelegatedConfig": map[string]any{ |
| 74 | "Read": []string{"/routing/v1/providers"}, |
| 75 | "Write": []string{}, |
| 76 | }, |
| 77 | }, |
| 78 | }, |
| 79 | "DNSResolvers": map[string]any{ |
| 80 | "eth.": []string{"https://dns.eth.limo/dns-query"}, |
| 81 | }, |
| 82 | "DelegatedEndpoints": map[string]any{ |
| 83 | "https://ipni.example.com": map[string]any{ |
| 84 | "Systems": []string{"IPNI"}, |
| 85 | "Read": []string{"/routing/v1/providers"}, |
| 86 | "Write": []string{}, |
| 87 | }, |
| 88 | mockServer.URL + "/newsystem": map[string]any{ |
| 89 | "Systems": []string{"NewSystem"}, |
| 90 | "Read": []string{"/routing/v1/providers"}, |
| 91 | "Write": []string{}, |
| 92 | }, |
| 93 | }, |
| 94 | } |
| 95 | |
| 96 | w.Header().Set("Content-Type", "application/json") |
| 97 | w.Header().Set("Cache-Control", "max-age=300") |
| 98 | _ = json.NewEncoder(w).Encode(autoconfData) |
| 99 | })) |
| 100 | defer mockServer.Close() |
| 101 | |
| 102 | // NewSystem mock server URL will be dynamically assigned |
| 103 | newSystemServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 104 | // Simple mock server for NewSystem endpoint |
| 105 | response := map[string]any{"Providers": []any{}} |
| 106 | w.Header().Set("Content-Type", "application/json") |
| 107 | _ = json.NewEncoder(w).Encode(response) |
| 108 | })) |
| 109 | defer newSystemServer.Close() |
| 110 | |
| 111 | // Update the autoconf to point to the correct NewSystem endpoint |
| 112 | mockServer.Close() |
| 113 | mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 114 | autoconfData := map[string]any{ |
| 115 | "AutoConfVersion": 2025072901, |
| 116 | "AutoConfSchema": 1, |
| 117 | "AutoConfTTL": 86400, |
| 118 | "SystemRegistry": map[string]any{ |
| 119 | "AminoDHT": map[string]any{ |
| 120 | "URL": "https://github.com/ipfs/specs/pull/497", |
| 121 | "Description": "Public DHT swarm", |
| 122 | "NativeConfig": map[string]any{ |
| 123 | "Bootstrap": []string{ |
| 124 | "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", |
| 125 | }, |
| 126 | }, |
| 127 | "DelegatedConfig": map[string]any{ |
| 128 | "Read": []string{"/routing/v1/providers", "/routing/v1/peers", "/routing/v1/ipns"}, |
| 129 | "Write": []string{"/routing/v1/ipns"}, |
| 130 | }, |
| 131 | }, |
| 132 | "IPNI": map[string]any{ |
| 133 | "URL": "https://ipni.example.com", |
| 134 | "Description": "Network Indexer", |
| 135 | "DelegatedConfig": map[string]any{ |
| 136 | "Read": []string{"/routing/v1/providers"}, |
| 137 | "Write": []string{}, |
| 138 | }, |
| 139 | }, |
| 140 | "NewSystem": map[string]any{ |
| 141 | "URL": "https://example.com/newsystem", |
| 142 | "Description": "Test system for extensibility verification", |
| 143 | "NativeConfig": map[string]any{ |
| 144 | "Bootstrap": []string{ |
| 145 | "/ip4/127.0.0.1/tcp/9999/p2p/12D3KooWPeQ4r3v6CmVmKXoFGtqEqcr3L8P6La9yH5oEWKtoLVVa", |
| 146 | }, |
| 147 | }, |
| 148 | "DelegatedConfig": map[string]any{ |
| 149 | "Read": []string{"/routing/v1/providers"}, |
| 150 | "Write": []string{}, |
| 151 | }, |
| 152 | }, |
| 153 | }, |
| 154 | "DNSResolvers": map[string]any{ |
| 155 | "eth.": []string{"https://dns.eth.limo/dns-query"}, |
| 156 | }, |
| 157 | "DelegatedEndpoints": map[string]any{ |
| 158 | "https://ipni.example.com": map[string]any{ |
| 159 | "Systems": []string{"IPNI"}, |
| 160 | "Read": []string{"/routing/v1/providers"}, |
| 161 | "Write": []string{}, |
| 162 | }, |
| 163 | newSystemServer.URL: map[string]any{ |
| 164 | "Systems": []string{"NewSystem"}, |
| 165 | "Read": []string{"/routing/v1/providers"}, |
| 166 | "Write": []string{}, |
| 167 | }, |
| 168 | }, |
| 169 | } |
| 170 | |
| 171 | w.Header().Set("Content-Type", "application/json") |
| 172 | w.Header().Set("Cache-Control", "max-age=300") |
| 173 | _ = json.NewEncoder(w).Encode(autoconfData) |
| 174 | })) |
| 175 | defer mockServer.Close() |
| 176 | |
| 177 | // Create Kubo node with autoconf pointing to mock server |
| 178 | h := harness.NewT(t) |
| 179 | node := h.NewNode().Init() |
| 180 | |
| 181 | // Update config to use mock autoconf server |
| 182 | node.UpdateConfig(func(cfg *config.Config) { |
| 183 | cfg.AutoConf.URL = config.NewOptionalString(mockServer.URL) |
| 184 | cfg.AutoConf.Enabled = config.True |
| 185 | cfg.AutoConf.RefreshInterval = config.NewOptionalDuration(1 * time.Second) |
| 186 | cfg.Routing.Type = config.NewOptionalString("auto") // Should enable native AminoDHT + delegated others |
| 187 | cfg.Bootstrap = []string{"auto"} |
| 188 | cfg.Routing.DelegatedRouters = []string{"auto"} |
| 189 | }) |
| 190 | |
| 191 | // Start the daemon |
| 192 | daemon := node.StartDaemon() |
| 193 | defer daemon.StopDaemon() |
| 194 | |
| 195 | // Give the daemon some time to initialize and make requests |
| 196 | time.Sleep(3 * time.Second) |
| 197 | |
| 198 | // Test 1: Verify bootstrap includes both AminoDHT and NewSystem peers (deduplicated) |
| 199 | bootstrapResult := daemon.IPFS("bootstrap", "list", "--expand-auto") |
| 200 | bootstrapOutput := bootstrapResult.Stdout.String() |
| 201 | t.Logf("Bootstrap output: %s", bootstrapOutput) |
| 202 | |
| 203 | // Should contain original DHT bootstrap peer (AminoDHT is a native system) |
| 204 | require.Contains(t, bootstrapOutput, "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "Should contain AminoDHT bootstrap peer") |
| 205 | |
| 206 | // Note: NewSystem bootstrap peers are NOT included because only native systems |
| 207 | // (AminoDHT for Routing.Type="auto") contribute bootstrap peers. |
| 208 | // Delegated systems like NewSystem only provide HTTP routing endpoints. |
| 209 | |
| 210 | // Test 2: Verify delegated endpoints are filtered correctly |
| 211 | // For Routing.Type=auto, native systems=[AminoDHT], so: |
| 212 | // - AminoDHT endpoints should be filtered out |
| 213 | // - IPNI and NewSystem endpoints should be included |
| 214 | |
| 215 | // Get the expanded delegated routers using --expand-auto |
| 216 | routerResult := daemon.IPFS("config", "Routing.DelegatedRouters", "--expand-auto") |
| 217 | var expandedRouters []string |
| 218 | require.NoError(t, json.Unmarshal([]byte(routerResult.Stdout.String()), &expandedRouters)) |
| 219 | |
| 220 | t.Logf("Expanded delegated routers: %v", expandedRouters) |
| 221 | |
| 222 | // Verify we got exactly 2 delegated routers: IPNI and NewSystem |
| 223 | require.Equal(t, 2, len(expandedRouters), "Should have exactly 2 delegated routers (IPNI and NewSystem). Got %d: %v", len(expandedRouters), expandedRouters) |
| 224 | |
| 225 | // Convert to URLs for checking |
| 226 | routerURLs := expandedRouters |
| 227 | |
| 228 | // Should contain NewSystem endpoint (not native) - now with routing path |
| 229 | foundNewSystem := false |
| 230 | expectedNewSystemURL := newSystemServer.URL + "/routing/v1/providers" // Full URL with path, as returned by DelegatedRoutersWithAutoConf |
| 231 | if slices.Contains(routerURLs, expectedNewSystemURL) { |
| 232 | foundNewSystem = true |
| 233 | } |
| 234 | require.True(t, foundNewSystem, "Should contain NewSystem endpoint (%s) for delegated routing, got: %v", expectedNewSystemURL, routerURLs) |
| 235 | |
| 236 | // Should contain ipni.example.com (IPNI is not native) |
| 237 | foundIPNI := false |
| 238 | for _, url := range routerURLs { |
| 239 | if strings.Contains(url, "ipni.example.com") { |
| 240 | foundIPNI = true |
| 241 | break |
| 242 | } |
| 243 | } |
| 244 | require.True(t, foundIPNI, "Should contain ipni.example.com endpoint for IPNI") |
| 245 | |
| 246 | // Test passes - we've verified that: |
| 247 | // 1. Bootstrap peers are correctly resolved from native systems only |
| 248 | // 2. Delegated routers include both IPNI and NewSystem endpoints |
| 249 | // 3. URL format is correct (base URLs with paths) |
| 250 | // 4. AutoConf extensibility works for unknown systems |
| 251 | |
| 252 | t.Log("NewSystem extensibility test passed - Kubo successfully discovered and used unknown routing system") |
| 253 | } |