package rest import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" "github.com/stretchr/testify/require" ) func resetMissionControlTestGlobals() { liquidityPoolsCache = sync.Map{} registryOnce = sync.Once{} registryAddrToKey = nil registryLoadErr = nil } func TestHandleMissionControlLiquidityTokenPathRequiresEnv(t *testing.T) { resetMissionControlTestGlobals() t.Setenv("TOKEN_AGGREGATION_BASE_URL", "") t.Setenv("TOKEN_AGGREGATION_URL", "") s := NewServer(nil, 138) req := httptest.NewRequest(http.MethodGet, "/api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools", nil) w := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w, req) require.Equal(t, http.StatusServiceUnavailable, w.Code) require.Contains(t, w.Body.String(), "TOKEN_AGGREGATION_BASE_URL not configured") } func TestHandleMissionControlLiquidityTokenPathCachesSuccess(t *testing.T) { resetMissionControlTestGlobals() var hitCount int upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hitCount++ w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":{"count":1,"pools":[]}}`)) })) defer upstream.Close() t.Setenv("TOKEN_AGGREGATION_BASE_URL", upstream.URL) t.Setenv("CHAIN_ID", "138") s := NewServer(nil, 138) path := "/api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools" w1 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w1, httptest.NewRequest(http.MethodGet, path, nil)) require.Equal(t, http.StatusOK, w1.Code) require.Equal(t, "miss", w1.Header().Get("X-Mission-Control-Cache")) w2 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w2, httptest.NewRequest(http.MethodGet, path, nil)) require.Equal(t, http.StatusOK, w2.Code) require.Equal(t, "hit", w2.Header().Get("X-Mission-Control-Cache")) require.Equal(t, 1, hitCount, "second request should be served from the in-memory cache") require.JSONEq(t, w1.Body.String(), w2.Body.String()) } func TestHandleMissionControlLiquidityTokenPathBypassesCacheWhenRequested(t *testing.T) { resetMissionControlTestGlobals() var hitCount int upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hitCount++ w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":{"count":1,"pools":[]}}`)) })) defer upstream.Close() t.Setenv("TOKEN_AGGREGATION_BASE_URL", upstream.URL) t.Setenv("CHAIN_ID", "138") s := NewServer(nil, 138) path := "/api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools" w1 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w1, httptest.NewRequest(http.MethodGet, path, nil)) require.Equal(t, http.StatusOK, w1.Code) require.Equal(t, "miss", w1.Header().Get("X-Mission-Control-Cache")) req2 := httptest.NewRequest(http.MethodGet, path+"?refresh=1", nil) req2.Header.Set("Cache-Control", "no-cache") w2 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w2, req2) require.Equal(t, http.StatusOK, w2.Code) require.Equal(t, "bypass", w2.Header().Get("X-Mission-Control-Cache")) require.Equal(t, 2, hitCount, "refresh=1 should force a fresh upstream read") } func TestHandleMissionControlLiquidityTokenPathTreatsGenericSuccessErrorEnvelopeAsBadGateway(t *testing.T) { resetMissionControlTestGlobals() var hitCount int upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hitCount++ w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"error":"Internal server error"}`)) })) defer upstream.Close() t.Setenv("TOKEN_AGGREGATION_BASE_URL", upstream.URL) t.Setenv("CHAIN_ID", "138") s := NewServer(nil, 138) path := "/api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools" w1 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w1, httptest.NewRequest(http.MethodGet, path, nil)) require.Equal(t, http.StatusBadGateway, w1.Code) require.Equal(t, "miss", w1.Header().Get("X-Mission-Control-Cache")) require.JSONEq(t, `{"error":"Internal server error"}`, w1.Body.String()) w2 := httptest.NewRecorder() s.handleMissionControlLiquidityTokenPath(w2, httptest.NewRequest(http.MethodGet, path, nil)) require.Equal(t, http.StatusBadGateway, w2.Code) require.Equal(t, "miss", w2.Header().Get("X-Mission-Control-Cache")) require.Equal(t, 2, hitCount, "generic error envelopes must not be cached as success") } func TestHandleMissionControlBridgeTraceLabelsFromRegistry(t *testing.T) { resetMissionControlTestGlobals() fromAddr := "0x1111111111111111111111111111111111111111" toAddr := "0x2222222222222222222222222222222222222222" registryJSON := `{ "chains": { "138": { "contracts": { "CHAIN138_SOURCE_BRIDGE": "` + fromAddr + `", "CHAIN138_DEST_BRIDGE": "` + toAddr + `" } } } }` registryPath := filepath.Join(t.TempDir(), "smart-contracts-master.json") require.NoError(t, os.WriteFile(registryPath, []byte(registryJSON), 0o644)) blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/api/v2/transactions/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", r.URL.Path) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "from": {"hash":"` + fromAddr + `"}, "to": {"hash":"` + toAddr + `"} }`)) })) defer blockscout.Close() t.Setenv("SMART_CONTRACTS_MASTER_JSON", registryPath) t.Setenv("BLOCKSCOUT_INTERNAL_URL", blockscout.URL) t.Setenv("EXPLORER_PUBLIC_BASE", "https://explorer.example.org") s := NewServer(nil, 138) req := httptest.NewRequest(http.MethodGet, "/api/v1/mission-control/bridge/trace?tx=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil) w := httptest.NewRecorder() s.HandleMissionControlBridgeTrace(w, req) require.Equal(t, http.StatusOK, w.Code) var out struct { Data map[string]any `json:"data"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out)) require.Equal(t, strings.ToLower(fromAddr), out.Data["from"]) require.Equal(t, strings.ToLower(toAddr), out.Data["to"]) require.Equal(t, "CHAIN138_SOURCE_BRIDGE", out.Data["from_registry"]) require.Equal(t, "CHAIN138_DEST_BRIDGE", out.Data["to_registry"]) require.Equal(t, "https://explorer.example.org/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", out.Data["blockscout_url"]) } func TestHandleMissionControlBridgeTraceFallsBackToAddressInventoryLabels(t *testing.T) { resetMissionControlTestGlobals() fromAddr := "0x4A666F96fC8764181194447A7dFdb7d471b301C8" toAddr := "0x152ed3e9912161b76bdfd368d0c84b7c31c10de7" tempDir := t.TempDir() registryPath := filepath.Join(tempDir, "smart-contracts-master.json") inventoryPath := filepath.Join(tempDir, "address-inventory.json") require.NoError(t, os.WriteFile(registryPath, []byte(`{ "chains": { "138": { "contracts": { "CCIP_Router": "0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817" } } } }`), 0o644)) require.NoError(t, os.WriteFile(inventoryPath, []byte(`{ "inventory": { "DEPLOYER_ADMIN_138": "`+fromAddr+`", "CW_L1_BRIDGE_CHAIN138": "`+toAddr+`" } }`), 0o644)) blockscout := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "from": {"hash":"` + fromAddr + `"}, "to": {"hash":"` + toAddr + `"} }`)) })) defer blockscout.Close() t.Setenv("SMART_CONTRACTS_MASTER_JSON", registryPath) t.Setenv("EXPLORER_ADDRESS_INVENTORY_FILE", inventoryPath) t.Setenv("BLOCKSCOUT_INTERNAL_URL", blockscout.URL) s := NewServer(nil, 138) req := httptest.NewRequest(http.MethodGet, "/api/v1/mission-control/bridge/trace?tx=0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", nil) w := httptest.NewRecorder() s.HandleMissionControlBridgeTrace(w, req) require.Equal(t, http.StatusOK, w.Code) var out struct { Data map[string]any `json:"data"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out)) require.Equal(t, strings.ToLower(fromAddr), out.Data["from"]) require.Equal(t, strings.ToLower(toAddr), out.Data["to"]) require.Equal(t, "DEPLOYER_ADMIN_138", out.Data["from_registry"]) require.Equal(t, "CW_L1_BRIDGE_CHAIN138", out.Data["to_registry"]) } func TestHandleMissionControlBridgeTraceRejectsBadHash(t *testing.T) { resetMissionControlTestGlobals() s := NewServer(nil, 138) req := httptest.NewRequest(http.MethodGet, "/api/v1/mission-control/bridge/trace?tx=not-a-tx", nil) w := httptest.NewRecorder() s.HandleMissionControlBridgeTrace(w, req) require.Equal(t, http.StatusBadRequest, w.Code) require.Contains(t, w.Body.String(), "invalid transaction hash") }