From f761f128e11bd16f25e3bbd1246a3082c4339395 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 16 Oct 2026 16:39:05 +0200 Subject: [PATCH 1/2] test(gcp): keep fixture pricing tests off ambient cloud credentials Five Cloud SQL, Memorystore and Compute Engine fixture tests reached the real Cloud Billing client, so they used a developer's ambient Application Default Credentials and could send catalog requests. They now inject the existing MockBillingService. A shared TestMain guard in the four billing packages points ADC at a nonexistent file and fails the package if any test still logs a real billing-client creation, since production swallows that error and the test would otherwise pass silently. Closes #406 Co-Authored-By: Claude Opus 5.5 --- .../gcp/internal/billingguard/billingguard.go | 58 +++++++++++++++++++ .../gcp/services/cloudsql/client_test.go | 2 + providers/gcp/services/cloudsql/main_test.go | 10 ++++ .../gcp/services/cloudstorage/main_test.go | 10 ++++ .../gcp/services/computeengine/client_test.go | 1 + .../gcp/services/computeengine/main_test.go | 10 ++++ .../gcp/services/memorystore/client_test.go | 2 + .../gcp/services/memorystore/main_test.go | 10 ++++ 8 files changed, 103 insertions(+) create mode 100644 providers/gcp/internal/billingguard/billingguard.go create mode 100644 providers/gcp/services/cloudsql/main_test.go create mode 100644 providers/gcp/services/cloudstorage/main_test.go create mode 100644 providers/gcp/services/computeengine/main_test.go create mode 100644 providers/gcp/services/memorystore/main_test.go diff --git a/providers/gcp/internal/billingguard/billingguard.go b/providers/gcp/internal/billingguard/billingguard.go new file mode 100644 index 00000000..3145711e --- /dev/null +++ b/providers/gcp/internal/billingguard/billingguard.go @@ -0,0 +1,58 @@ +// Package billingguard keeps GCP service tests off the real Cloud Billing +// Catalog. Production code logs and swallows a failed billing-service +// construction, so a test that forgot to inject a fake would otherwise pass +// silently on a CI runner and hit the network on a developer machine. +package billingguard + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "sync" + "testing" +) + +const marker = "failed to create billing service" + +type watcher struct { + next io.Writer + mu sync.Mutex + hits []string +} + +func (w *watcher) Write(p []byte) (int, error) { + if bytes.Contains(p, []byte(marker)) { + w.mu.Lock() + w.hits = append(w.hits, string(bytes.TrimSpace(p))) + w.mu.Unlock() + } + return w.next.Write(p) +} + +// Run is for a package's TestMain. It points Application Default Credentials +// at a missing file and the metadata server at a closed port, so the real +// billing client cannot be built, then fails the package if any test tried. +func Run(m *testing.M) int { + for k, v := range map[string]string{ + "GOOGLE_APPLICATION_CREDENTIALS": "/nonexistent/billingguard-no-adc.json", + "GCE_METADATA_HOST": "127.0.0.1:1", + "CLOUDSDK_CONFIG": os.TempDir() + "/billingguard-no-gcloud", + } { + os.Setenv(k, v) + } + os.Unsetenv("GOOGLE_API_KEY") + + w := &watcher{next: os.Stderr} + log.SetOutput(w) + code := m.Run() + if code == 0 && len(w.hits) > 0 { + fmt.Fprintf(os.Stderr, "billingguard: %d test(s) reached the real billing service; inject a BillingService fake:\n", len(w.hits)) + for _, h := range w.hits { + fmt.Fprintln(os.Stderr, " "+h) + } + return 1 + } + return code +} diff --git a/providers/gcp/services/cloudsql/client_test.go b/providers/gcp/services/cloudsql/client_test.go index ae19233f..a34bcc20 100644 --- a/providers/gcp/services/cloudsql/client_test.go +++ b/providers/gcp/services/cloudsql/client_test.go @@ -600,6 +600,7 @@ func TestCloudSQLClient_GetOfferingDetails_APIError(t *testing.T) { func TestCloudSQLClient_GetRecommendations_WithMock(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") + client.SetBillingService(&MockBillingService{err: errors.New("catalog not under test")}) mockIterator := &MockRecommenderIterator{ recommendations: []*recommenderpb.Recommendation{ @@ -704,6 +705,7 @@ func TestCloudSQLClient_SetterMethods(t *testing.T) { func TestCloudSQLClient_ConvertGCPRecommendation(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") + client.SetBillingService(&MockBillingService{err: errors.New("catalog not under test")}) gcpRec := &recommenderpb.Recommendation{ Name: "test-rec", diff --git a/providers/gcp/services/cloudsql/main_test.go b/providers/gcp/services/cloudsql/main_test.go new file mode 100644 index 00000000..90ae499d --- /dev/null +++ b/providers/gcp/services/cloudsql/main_test.go @@ -0,0 +1,10 @@ +package cloudsql + +import ( + "os" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/billingguard" +) + +func TestMain(m *testing.M) { os.Exit(billingguard.Run(m)) } diff --git a/providers/gcp/services/cloudstorage/main_test.go b/providers/gcp/services/cloudstorage/main_test.go new file mode 100644 index 00000000..f0345f57 --- /dev/null +++ b/providers/gcp/services/cloudstorage/main_test.go @@ -0,0 +1,10 @@ +package cloudstorage + +import ( + "os" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/billingguard" +) + +func TestMain(m *testing.M) { os.Exit(billingguard.Run(m)) } diff --git a/providers/gcp/services/computeengine/client_test.go b/providers/gcp/services/computeengine/client_test.go index 6fb3af8d..3ac6bd9c 100644 --- a/providers/gcp/services/computeengine/client_test.go +++ b/providers/gcp/services/computeengine/client_test.go @@ -2367,6 +2367,7 @@ func TestConvertGCPRecommendationVCPUOnlyKeepsNilDetails(t *testing.T) { func TestGetRecommendationsStringMemoryAmountEndToEnd(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") + client.SetBillingService(&MockBillingService{err: errors.New("catalog not under test")}) input := commitmentOnlyCUDRecommendation() input.StateInfo = &recommenderpb.RecommendationStateInfo{State: recommenderpb.RecommendationStateInfo_ACTIVE} memoryOpOf(input).PathValue = &recommenderpb.Operation_Value{Value: structpb.NewStringValue("6144")} diff --git a/providers/gcp/services/computeengine/main_test.go b/providers/gcp/services/computeengine/main_test.go new file mode 100644 index 00000000..1ac33dbc --- /dev/null +++ b/providers/gcp/services/computeengine/main_test.go @@ -0,0 +1,10 @@ +package computeengine + +import ( + "os" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/billingguard" +) + +func TestMain(m *testing.M) { os.Exit(billingguard.Run(m)) } diff --git a/providers/gcp/services/memorystore/client_test.go b/providers/gcp/services/memorystore/client_test.go index 5c00e5b6..f2114fc0 100644 --- a/providers/gcp/services/memorystore/client_test.go +++ b/providers/gcp/services/memorystore/client_test.go @@ -602,6 +602,7 @@ func TestMemorystoreClient_GetRecommendations_WithMockClient(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") + client.SetBillingService(&MockBillingService{err: errors.New("catalog not under test")}) mockClient := &MockRecommenderClient{ recommendations: tt.recommendations, @@ -633,6 +634,7 @@ func TestMemorystoreClient_GetRecommendations_WithMockClient(t *testing.T) { func TestMemorystoreClient_ConvertGCPRecommendation(t *testing.T) { ctx := context.Background() client, _ := NewClient(ctx, "test-project", "us-central1") + client.SetBillingService(&MockBillingService{err: errors.New("catalog not under test")}) tests := []struct { name string diff --git a/providers/gcp/services/memorystore/main_test.go b/providers/gcp/services/memorystore/main_test.go new file mode 100644 index 00000000..e06341ff --- /dev/null +++ b/providers/gcp/services/memorystore/main_test.go @@ -0,0 +1,10 @@ +package memorystore + +import ( + "os" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/providers/gcp/internal/billingguard" +) + +func TestMain(m *testing.M) { os.Exit(billingguard.Run(m)) } From 1454b9b2febfb2d7ef21849d3543317a8b2a3841 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 16 Oct 2026 16:53:28 +0200 Subject: [PATCH 2/2] fix(gcp): fail the billing guard when it cannot isolate credentials gosec G104 flagged the unchecked os.Setenv. If the guard cannot point ADC away from real credentials, running the tests would risk using them, so Run now reports the variable and returns 1 before m.Run. Refs #406 Co-Authored-By: Claude Opus 5.5 --- providers/gcp/internal/billingguard/billingguard.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/providers/gcp/internal/billingguard/billingguard.go b/providers/gcp/internal/billingguard/billingguard.go index 3145711e..0ce21ce9 100644 --- a/providers/gcp/internal/billingguard/billingguard.go +++ b/providers/gcp/internal/billingguard/billingguard.go @@ -10,6 +10,7 @@ import ( "io" "log" "os" + "path/filepath" "sync" "testing" ) @@ -38,11 +39,17 @@ func Run(m *testing.M) int { for k, v := range map[string]string{ "GOOGLE_APPLICATION_CREDENTIALS": "/nonexistent/billingguard-no-adc.json", "GCE_METADATA_HOST": "127.0.0.1:1", - "CLOUDSDK_CONFIG": os.TempDir() + "/billingguard-no-gcloud", + "CLOUDSDK_CONFIG": filepath.Join(os.TempDir(), "billingguard-no-gcloud"), } { - os.Setenv(k, v) + if err := os.Setenv(k, v); err != nil { + fmt.Fprintf(os.Stderr, "billingguard: cannot isolate %s: %v\n", k, err) + return 1 + } + } + if err := os.Unsetenv("GOOGLE_API_KEY"); err != nil { + fmt.Fprintf(os.Stderr, "billingguard: cannot unset GOOGLE_API_KEY: %v\n", err) + return 1 } - os.Unsetenv("GOOGLE_API_KEY") w := &watcher{next: os.Stderr} log.SetOutput(w)