diff --git a/providers/azure/services/cache/client_test.go b/providers/azure/services/cache/client_test.go index 282a917c..bc845026 100644 --- a/providers/azure/services/cache/client_test.go +++ b/providers/azure/services/cache/client_test.go @@ -204,6 +204,7 @@ func TestCacheClient_GetRegion(t *testing.T) { func TestCacheClient_GetValidResourceTypes_Fallback(t *testing.T) { // When API calls fail, GetValidResourceTypes should return common SKUs client := NewClient(nil, "invalid-subscription", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) skus, err := client.GetValidResourceTypes(context.Background()) require.NoError(t, err) @@ -217,6 +218,7 @@ func TestCacheClient_GetValidResourceTypes_Fallback(t *testing.T) { func TestCacheClient_ValidateOffering_InvalidSKU(t *testing.T) { client := NewClient(nil, "sub", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) rec := common.Recommendation{ ResourceType: "InvalidSKU_X99", } @@ -462,6 +464,7 @@ func TestCacheClient_GetExistingCommitments_Empty(t *testing.T) { func TestCacheClient_ValidateOffering_ValidSKU(t *testing.T) { ctx := context.Background() client := NewClient(nil, "test-subscription", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) rec := common.Recommendation{ ResourceType: "Premium_P1", @@ -477,6 +480,7 @@ func TestCacheClient_ValidateOffering_CaseInsensitive(t *testing.T) { t.Run("case_insensitive", func(t *testing.T) { client := NewClient(nil, "test-subscription", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) rec := common.Recommendation{ResourceType: "premium_p1"} err := client.ValidateOffering(ctx, rec) assert.NoError(t, err) @@ -484,6 +488,7 @@ func TestCacheClient_ValidateOffering_CaseInsensitive(t *testing.T) { t.Run("whitespace_trimmed", func(t *testing.T) { client := NewClient(nil, "test-subscription", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) rec := common.Recommendation{ResourceType: " Premium_P1 "} err := client.ValidateOffering(ctx, rec) assert.NoError(t, err) diff --git a/providers/azure/services/cache/network_guard_test.go b/providers/azure/services/cache/network_guard_test.go new file mode 100644 index 00000000..cf563129 --- /dev/null +++ b/providers/azure/services/cache/network_guard_test.go @@ -0,0 +1,55 @@ +package cache + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" +) + +// externalRequests counts requests that reached the loopback proxy that +// TestMain installs. The Azure SDK builds its own http.Transport, so swapping +// http.DefaultTransport would not see its calls; that transport honors +// HTTPS_PROXY, which does. +var externalRequests atomic.Int64 + +func TestMain(m *testing.M) { + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + externalRequests.Add(1) + http.Error(w, "network access denied in tests", http.StatusForbidden) + })) + // Set before any request so the proxy lookup, which is cached per process, sees it. + os.Setenv("HTTPS_PROXY", proxy.URL) + os.Setenv("HTTP_PROXY", proxy.URL) + os.Unsetenv("NO_PROXY") + os.Unsetenv("no_proxy") + code := m.Run() + proxy.Close() + if n := externalRequests.Load(); n > 0 && code == 0 { + fmt.Fprintf(os.Stderr, "FAIL: %d test request(s) reached the network; stub the client or pager\n", n) + code = 1 + } + os.Exit(code) +} + +// TestCacheClient_SKUValidation_MakesNoExternalRequests pins that SKU +// validation with a stubbed Redis caches pager never reaches the network. +func TestCacheClient_SKUValidation_MakesNoExternalRequests(t *testing.T) { + before := externalRequests.Load() + client := NewClient(nil, "test-subscription", "eastus") + client.SetRedisCachesPager(&MockRedisCachesPager{}) + + skus, err := client.GetValidResourceTypes(context.Background()) + require.NoError(t, err) + assert.Contains(t, skus, "Premium_P1") + require.NoError(t, client.ValidateOffering(context.Background(), common.Recommendation{ResourceType: "Premium_P1"})) + assert.Zero(t, externalRequests.Load()-before, "SKU validation must not reach the network") +} diff --git a/providers/azure/services/compute/client_test.go b/providers/azure/services/compute/client_test.go index 81160593..09e14448 100644 --- a/providers/azure/services/compute/client_test.go +++ b/providers/azure/services/compute/client_test.go @@ -228,6 +228,7 @@ func TestComputeClient_GetRecommendations_WithMock(t *testing.T) { func TestComputeClient_GetRecommendations_EmitsBothPaymentVariants(t *testing.T) { ctx := context.Background() client := NewClient(nil, "test-subscription", "eastus") + client.SetResourceSKUsPager(&mocks.MockResourceSKUsPager{}) // Inject a single recommendation via the mock pager. apiRec := mocks.BuildLegacyReservationRecommendation( @@ -1127,6 +1128,7 @@ func TestComputeClient_ConvertAzureVMRecommendation_NilGuards(t *testing.T) { // PaymentOption). Subscription/Account comes from the client, not the rec. func TestComputeClient_ConvertAzureVMRecommendation_PopulatesAllFields(t *testing.T) { client := NewClient(nil, "test-subscription", "eastus") + client.SetResourceSKUsPager(&mocks.MockResourceSKUsPager{}) rec := mocks.BuildLegacyReservationRecommendation( mocks.WithRegion("westeurope"), mocks.WithScope("Shared"), @@ -1674,7 +1676,7 @@ func TestPurchaseBody_SKUAndQuantityStayInMatchingUnits(t *testing.T) { mocks.WithCosts(1000, 600, 400), )...) - c := &Client{subscriptionID: "sub-1", region: "eastus"} + c := &Client{subscriptionID: "sub-1", region: "eastus", resourceSKUsPager: &mocks.MockResourceSKUsPager{}} rec := c.convertAzureVMRecommendation(context.Background(), apiRec) require.NotNil(t, rec) diff --git a/providers/azure/services/compute/network_guard_test.go b/providers/azure/services/compute/network_guard_test.go new file mode 100644 index 00000000..5e3ade96 --- /dev/null +++ b/providers/azure/services/compute/network_guard_test.go @@ -0,0 +1,53 @@ +package compute + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/providers/azure/mocks" +) + +// externalRequests counts requests that reached the loopback proxy that +// TestMain installs. The Azure SDK builds its own http.Transport, so swapping +// http.DefaultTransport would not see its calls; that transport honors +// HTTPS_PROXY, which does. +var externalRequests atomic.Int64 + +func TestMain(m *testing.M) { + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + externalRequests.Add(1) + http.Error(w, "network access denied in tests", http.StatusForbidden) + })) + // Set before any request so the proxy lookup, which is cached per process, sees it. + os.Setenv("HTTPS_PROXY", proxy.URL) + os.Setenv("HTTP_PROXY", proxy.URL) + os.Unsetenv("NO_PROXY") + os.Unsetenv("no_proxy") + code := m.Run() + proxy.Close() + if n := externalRequests.Load(); n > 0 && code == 0 { + fmt.Fprintf(os.Stderr, "FAIL: %d test request(s) reached the network; stub the client or pager\n", n) + code = 1 + } + os.Exit(code) +} + +// TestComputeClient_ConvertAzureVMRecommendation_MakesNoExternalRequests pins +// that conversion with a stubbed resource SKUs pager never reaches the network. +func TestComputeClient_ConvertAzureVMRecommendation_MakesNoExternalRequests(t *testing.T) { + before := externalRequests.Load() + client := NewClient(nil, "test-subscription", "eastus") + client.SetResourceSKUsPager(&mocks.MockResourceSKUsPager{}) + + rec := mocks.BuildLegacyReservationRecommendation(mocks.WithRegion("eastus")) + require.NotNil(t, client.convertAzureVMRecommendation(context.Background(), rec)) + assert.Zero(t, externalRequests.Load()-before, "conversion must not reach the network") +} diff --git a/providers/azure/services/managedredis/client_test.go b/providers/azure/services/managedredis/client_test.go index af532bea..756a8634 100644 --- a/providers/azure/services/managedredis/client_test.go +++ b/providers/azure/services/managedredis/client_test.go @@ -246,6 +246,7 @@ func TestGetRegion(t *testing.T) { func TestGetValidResourceTypes_Fallback(t *testing.T) { c := NewClient(nil, "invalid-sub", "eastus") + c.SetRedisCachesPager(&mockRedisPager{}) skus, err := c.GetValidResourceTypes(context.Background()) require.NoError(t, err) require.NotEmpty(t, skus) @@ -322,12 +323,14 @@ func TestGetValidResourceTypes_MultipleCaches(t *testing.T) { func TestValidateOffering_ValidSKU(t *testing.T) { c := NewClient(nil, "sub", "eastus") + c.SetRedisCachesPager(&mockRedisPager{}) err := c.ValidateOffering(context.Background(), common.Recommendation{ResourceType: "Premium_P1"}) assert.NoError(t, err) } func TestValidateOffering_InvalidSKU(t *testing.T) { c := NewClient(nil, "sub", "eastus") + c.SetRedisCachesPager(&mockRedisPager{}) err := c.ValidateOffering(context.Background(), common.Recommendation{ResourceType: "Bogus_Z99"}) require.Error(t, err) assert.Contains(t, err.Error(), "invalid Azure Cache for Redis SKU") diff --git a/providers/azure/services/managedredis/network_guard_test.go b/providers/azure/services/managedredis/network_guard_test.go new file mode 100644 index 00000000..99dcad39 --- /dev/null +++ b/providers/azure/services/managedredis/network_guard_test.go @@ -0,0 +1,55 @@ +package managedredis + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" +) + +// externalRequests counts requests that reached the loopback proxy that +// TestMain installs. The Azure SDK builds its own http.Transport, so swapping +// http.DefaultTransport would not see its calls; that transport honors +// HTTPS_PROXY, which does. +var externalRequests atomic.Int64 + +func TestMain(m *testing.M) { + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + externalRequests.Add(1) + http.Error(w, "network access denied in tests", http.StatusForbidden) + })) + // Set before any request so the proxy lookup, which is cached per process, sees it. + os.Setenv("HTTPS_PROXY", proxy.URL) + os.Setenv("HTTP_PROXY", proxy.URL) + os.Unsetenv("NO_PROXY") + os.Unsetenv("no_proxy") + code := m.Run() + proxy.Close() + if n := externalRequests.Load(); n > 0 && code == 0 { + fmt.Fprintf(os.Stderr, "FAIL: %d test request(s) reached the network; stub the client or pager\n", n) + code = 1 + } + os.Exit(code) +} + +// TestClient_SKUValidation_MakesNoExternalRequests pins that SKU +// validation with a stubbed Redis caches pager never reaches the network. +func TestClient_SKUValidation_MakesNoExternalRequests(t *testing.T) { + before := externalRequests.Load() + c := NewClient(nil, "test-subscription", "eastus") + c.SetRedisCachesPager(&mockRedisPager{}) + + skus, err := c.GetValidResourceTypes(context.Background()) + require.NoError(t, err) + assert.Contains(t, skus, "Premium_P1") + require.NoError(t, c.ValidateOffering(context.Background(), common.Recommendation{ResourceType: "Premium_P1"})) + assert.Zero(t, externalRequests.Load()-before, "SKU validation must not reach the network") +}