diff --git a/providers/gcp/internal/billingguard/billingguard.go b/providers/gcp/internal/billingguard/billingguard.go new file mode 100644 index 00000000..0ce21ce9 --- /dev/null +++ b/providers/gcp/internal/billingguard/billingguard.go @@ -0,0 +1,65 @@ +// 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" + "path/filepath" + "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": filepath.Join(os.TempDir(), "billingguard-no-gcloud"), + } { + 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 + } + + 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)) }