package gcp import ( "bytes" "encoding/json" "math" "net/http" "net/http/httptest" "testing" "github.com/opencost/opencost/core/pkg/pricing" ) const floatTolerance = 0.0000001 func TestNewGCPPricingSource(t *testing.T) { config := GCPPricingSourceConfig{ APIKey: "test-api-key", CurrencyCode: "USD", } source := NewGCPPricingSource(config) if source == nil { t.Fatal("NewGCPPricingSource() returned nil") } if source.config.APIKey != "test-api-key" { t.Errorf("APIKey = %v, want test-api-key", source.config.APIKey) } if source.config.CurrencyCode != "USD" { t.Errorf("CurrencyCode = %v, want USD", source.config.CurrencyCode) } } func TestBuildURL(t *testing.T) { tests := []struct { name string apiKey string currencyCode string pageToken string wantContains []string wantNotContain string }{ { name: "Basic URL without page token", apiKey: "test-key", currencyCode: "USD", pageToken: "", wantContains: []string{ "cloudbilling.googleapis.com", "key=test-key", "currencyCode=USD", }, wantNotContain: "pageToken", }, { name: "URL with page token", apiKey: "test-key", currencyCode: "EUR", pageToken: "next-page-123", wantContains: []string{ "cloudbilling.googleapis.com", "key=test-key", "currencyCode=EUR", "pageToken=next-page-123", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{ config: GCPPricingSourceConfig{ APIKey: tt.apiKey, CurrencyCode: tt.currencyCode, }, } url := source.buildURL(tt.pageToken) for _, want := range tt.wantContains { if !contains(url, want) { t.Errorf("buildURL() = %v, want to contain %v", url, want) } } if tt.wantNotContain != "" && contains(url, tt.wantNotContain) { t.Errorf("buildURL() = %v, should not contain %v", url, tt.wantNotContain) } }) } } func TestExtractHourlyPrice(t *testing.T) { tests := []struct { name string sku *GCPPricing wantPrice float64 wantErr bool }{ { name: "Valid pricing with single tier", sku: &GCPPricing{ PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 41600000, }, }, }, }, }, }, }, wantPrice: 0.0416, wantErr: false, }, { name: "Valid pricing with multiple tiers", sku: &GCPPricing{ PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 10000000, }, }, { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 50000000, }, }, }, }, }, }, }, wantPrice: 0.05, // Should use last tier wantErr: false, }, { name: "Pricing with whole units", sku: &GCPPricing{ PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "1", Nanos: 500000000, }, }, }, }, }, }, }, wantPrice: 1.5, wantErr: false, }, { name: "No pricing info", sku: &GCPPricing{ PricingInfo: []*PricingInfo{}, }, wantPrice: 0, wantErr: true, }, { name: "No tiered rates", sku: &GCPPricing{ PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{}, }, }, }, }, wantPrice: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{} price, err := source.extractHourlyPrice(tt.sku) if (err != nil) != tt.wantErr { t.Errorf("extractHourlyPrice() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && math.Abs(price-tt.wantPrice) > floatTolerance { t.Errorf("extractHourlyPrice() = %v, want %v", price, tt.wantPrice) } }) } } func TestExpandInstanceTypes(t *testing.T) { tests := []struct { name string instanceType string resourceGroup string want []string }{ { name: "E2 CPU expands", instanceType: "e2", resourceGroup: "CPU", want: []string{"e2-micro", "e2-small", "e2-medium", "e2-standard", "e2-custom"}, }, { name: "E2 RAM expands", instanceType: "e2", resourceGroup: "RAM", want: []string{"e2-micro", "e2-small", "e2-medium", "e2-standard", "e2-custom"}, }, { name: "A2 CPU expands", instanceType: "a2", resourceGroup: "CPU", want: []string{"a2-highgpu", "a2-megagpu", "a2-ultragpu"}, }, { name: "A2 RAM expands", instanceType: "a2", resourceGroup: "RAM", want: []string{"a2-highgpu", "a2-megagpu", "a2-ultragpu"}, }, { name: "N2 does not expand", instanceType: "n2-standard", resourceGroup: "CPU", want: []string{"n2-standard"}, }, { name: "Custom does not expand", instanceType: "custom", resourceGroup: "CPU", want: []string{"custom"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{} got := source.expandInstanceTypes(tt.instanceType, tt.resourceGroup) if len(got) != len(tt.want) { t.Errorf("expandInstanceTypes() returned %d types, want %d", len(got), len(tt.want)) return } for i, wantType := range tt.want { if got[i] != wantType { t.Errorf("expandInstanceTypes()[%d] = %v, want %v", i, got[i], wantType) } } }) } } func TestParsePage(t *testing.T) { tests := []struct { name string response GCPPricingResponse wantNextPageToken string wantErr bool checkCPUCosts bool checkRAMCosts bool checkVolumeCosts bool }{ { name: "Valid compute SKU", response: GCPPricingResponse{ Skus: []*GCPPricing{ { Description: "N2 Instance Core running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "CPU", UsageType: "OnDemand", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 31611000, }, }, }, }, }, }, }, }, NextPageToken: "next-token-123", }, wantNextPageToken: "next-token-123", wantErr: false, checkCPUCosts: true, }, { name: "Valid storage SKU", response: GCPPricingResponse{ Skus: []*GCPPricing{ { Description: "SSD backed PD Capacity", Category: &GCPResourceInfo{ ResourceGroup: "SSD", UsageType: "OnDemand", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 170000000, }, }, }, }, }, }, }, }, NextPageToken: "", }, wantNextPageToken: "", wantErr: false, checkVolumeCosts: true, }, { name: "SKU with no category", response: GCPPricingResponse{ Skus: []*GCPPricing{ { Description: "Some SKU", Category: nil, PricingInfo: []*PricingInfo{}, }, }, NextPageToken: "", }, wantNextPageToken: "", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{ config: GCPPricingSourceConfig{ CurrencyCode: "USD", }, } // Marshal response to JSON data, err := json.Marshal(tt.response) if err != nil { t.Fatalf("Failed to marshal test response: %v", err) } nodeCPUCosts := make(map[nodeKey]float64) nodeRAMCosts := make(map[nodeKey]float64) volumeCosts := make(map[volumeKey]float64) nextToken, err := source.parsePage(bytes.NewReader(data), nodeCPUCosts, nodeRAMCosts, volumeCosts) if (err != nil) != tt.wantErr { t.Errorf("parsePage() error = %v, wantErr %v", err, tt.wantErr) return } if nextToken != tt.wantNextPageToken { t.Errorf("parsePage() nextToken = %v, want %v", nextToken, tt.wantNextPageToken) } if tt.checkCPUCosts && len(nodeCPUCosts) == 0 { t.Error("parsePage() should have populated CPU costs") } if tt.checkRAMCosts && len(nodeRAMCosts) == 0 { t.Error("parsePage() should have populated RAM costs") } if tt.checkVolumeCosts && len(volumeCosts) == 0 { t.Error("parsePage() should have populated volume costs") } }) } } func TestParseVolumeSKU(t *testing.T) { tests := []struct { name string sku *GCPPricing wantCosts int // Expected number of volume cost entries }{ { name: "Valid PD-SSD", sku: &GCPPricing{ Description: "SSD backed PD Capacity", Category: &GCPResourceInfo{ ResourceGroup: "SSD", }, ServiceRegions: []string{"us-central1", "us-east1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 170000000, }, }, }, }, }, }, }, wantCosts: 2, // Two regions }, { name: "Regional disk", sku: &GCPPricing{ Description: "Regional SSD backed PD Capacity", Category: &GCPResourceInfo{ ResourceGroup: "SSD", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 204000000, }, }, }, }, }, }, }, wantCosts: 1, }, { name: "Unknown volume type", sku: &GCPPricing{ Description: "Unknown Disk Type", Category: &GCPResourceInfo{ ResourceGroup: "UnknownType", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 100000000, }, }, }, }, }, }, }, wantCosts: 0, // Should not add unknown types }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{} volumeCosts := make(map[volumeKey]float64) source.parseVolumeSKU(tt.sku, volumeCosts) if len(volumeCosts) != tt.wantCosts { t.Errorf("parseVolumeSKU() added %d costs, want %d", len(volumeCosts), tt.wantCosts) } }) } } func TestParseComputeSKU(t *testing.T) { tests := []struct { name string sku *GCPPricing usageType string wantCPUCosts int wantRAMCosts int }{ { name: "CPU SKU", sku: &GCPPricing{ Description: "N2 Instance Core running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "CPU", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 31611000, }, }, }, }, }, }, }, usageType: "ondemand", wantCPUCosts: 1, wantRAMCosts: 0, }, { name: "RAM SKU", sku: &GCPPricing{ Description: "N2 Instance RAM running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "RAM", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 4237000, }, }, }, }, }, }, }, usageType: "ondemand", wantCPUCosts: 0, wantRAMCosts: 1, }, { name: "E2 CPU expands to multiple types", sku: &GCPPricing{ Description: "E2 Instance Core running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "CPU", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ { UnitPrice: &UnitPriceInfo{ Units: "0", Nanos: 21811000, }, }, }, }, }, }, }, usageType: "ondemand", wantCPUCosts: 5, // e2-micro, e2-small, e2-medium, e2-standard, e2-custom wantRAMCosts: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{} nodeCPUCosts := make(map[nodeKey]float64) nodeRAMCosts := make(map[nodeKey]float64) source.parseComputeSKU(tt.sku, tt.usageType, nodeCPUCosts, nodeRAMCosts) if len(nodeCPUCosts) != tt.wantCPUCosts { t.Errorf("parseComputeSKU() added %d CPU costs, want %d", len(nodeCPUCosts), tt.wantCPUCosts) } if len(nodeRAMCosts) != tt.wantRAMCosts { t.Errorf("parseComputeSKU() added %d RAM costs, want %d", len(nodeRAMCosts), tt.wantRAMCosts) } }) } } func TestBuildNodePricing(t *testing.T) { tests := []struct { name string cpuCosts map[nodeKey]float64 ramCosts map[nodeKey]float64 wantNodes int currencyCode string }{ { name: "Complete node with CPU and RAM", cpuCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.031611, }, ramCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.004237, }, wantNodes: 1, currencyCode: "USD", }, { name: "Missing RAM cost", cpuCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.031611, }, ramCosts: map[nodeKey]float64{}, wantNodes: 0, currencyCode: "USD", }, { name: "Missing CPU cost", cpuCosts: map[nodeKey]float64{}, ramCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.004237, }, wantNodes: 0, currencyCode: "USD", }, { name: "Preemptible instance - excluded", cpuCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "preemptible"}: 0.007583, }, ramCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "preemptible"}: 0.001017, }, wantNodes: 0, currencyCode: "USD", }, { name: "Multiple regions", cpuCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.031611, {Region: "us-east1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.031611, }, ramCosts: map[nodeKey]float64{ {Region: "us-central1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.004237, {Region: "us-east1", InstanceType: "n2-standard", UsageType: "ondemand"}: 0.004237, }, wantNodes: 2, currencyCode: "USD", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{ config: GCPPricingSourceConfig{ CurrencyCode: tt.currencyCode, }, } ps := &pricing.PricingSet{ NodePricing: []*pricing.NodePricing{}, PersistentVolumePricing: []*pricing.PersistentVolumePricing{}, } source.buildNodePricing(ps, tt.cpuCosts, tt.ramCosts) if len(ps.NodePricing) != tt.wantNodes { t.Errorf("buildNodePricing() created %d nodes, want %d", len(ps.NodePricing), tt.wantNodes) } }) } } func TestBuildVolumePricing(t *testing.T) { tests := []struct { name string volumeCosts map[volumeKey]float64 wantVolumes int currencyCode string }{ { name: "Single volume type", volumeCosts: map[volumeKey]float64{ {Region: "us-central1", VolumeType: pricing.VolumeTypePDSSD, Regional: false}: 0.000232876, }, wantVolumes: 1, currencyCode: "USD", }, { name: "Multiple volume types", volumeCosts: map[volumeKey]float64{ {Region: "us-central1", VolumeType: pricing.VolumeTypePDSSD, Regional: false}: 0.000232876, {Region: "us-central1", VolumeType: pricing.VolumeTypePDStandard, Regional: false}: 0.000054795, }, wantVolumes: 2, currencyCode: "USD", }, { name: "Regional and zonal", volumeCosts: map[volumeKey]float64{ {Region: "us-central1", VolumeType: pricing.VolumeTypePDSSD, Regional: false}: 0.000232876, {Region: "us-central1", VolumeType: pricing.VolumeTypePDSSD, Regional: true}: 0.000279452, }, wantVolumes: 2, currencyCode: "USD", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { source := &GCPPricingSource{ config: GCPPricingSourceConfig{ CurrencyCode: tt.currencyCode, }, } ps := &pricing.PricingSet{ NodePricing: []*pricing.NodePricing{}, PersistentVolumePricing: []*pricing.PersistentVolumePricing{}, } source.buildVolumePricing(ps, tt.volumeCosts) if len(ps.PersistentVolumePricing) != tt.wantVolumes { t.Errorf("buildVolumePricing() created %d volumes, want %d", len(ps.PersistentVolumePricing), tt.wantVolumes) } }) } } func TestGetPricing_Integration(t *testing.T) { // Serve one page of pricing: one N2 CPU SKU, one N2 RAM SKU, one SSD volume SKU. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { response := GCPPricingResponse{ Skus: []*GCPPricing{ { Description: "N2 Instance Core running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "CPU", UsageType: "OnDemand", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ {UnitPrice: &UnitPriceInfo{Units: "0", Nanos: 31611000}}, }, }, }, }, }, { Description: "N2 Instance RAM running in Americas", Category: &GCPResourceInfo{ ResourceGroup: "RAM", UsageType: "OnDemand", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ {UnitPrice: &UnitPriceInfo{Units: "0", Nanos: 4237000}}, }, }, }, }, }, { Description: "SSD backed PD Capacity in Americas", Category: &GCPResourceInfo{ ResourceGroup: "SSD", UsageType: "OnDemand", }, ServiceRegions: []string{"us-central1"}, PricingInfo: []*PricingInfo{ { PricingExpression: &PricingExpression{ TieredRates: []*TieredRates{ {UnitPrice: &UnitPriceInfo{Units: "0", Nanos: 170000000}}, }, }, }, }, }, }, NextPageToken: "", } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) })) defer server.Close() // Point the package-level base URL at the test server. originalURL := BillingAPIBaseURL BillingAPIBaseURL = server.URL defer func() { BillingAPIBaseURL = originalURL }() // Use the test server's own client so TLS/redirect rules match. originalClient := gcpHTTPClient gcpHTTPClient = server.Client() defer func() { gcpHTTPClient = originalClient }() source := NewGCPPricingSource(GCPPricingSourceConfig{ APIKey: "test-key", CurrencyCode: "USD", }) ps, err := source.GetPricing() if err != nil { t.Fatalf("GetPricing() unexpected error: %v", err) } if len(ps.NodePricing) != 1 { t.Errorf("NodePricing len = %d, want 1", len(ps.NodePricing)) } if len(ps.PersistentVolumePricing) != 1 { t.Errorf("PersistentVolumePricing len = %d, want 1", len(ps.PersistentVolumePricing)) } node := ps.NodePricing[0] if node.Properties.Region != "us-central1" { t.Errorf("NodePricing region = %q, want us-central1", node.Properties.Region) } if node.Properties.InstanceType != "n2-standard" { t.Errorf("NodePricing instance type = %q, want n2-standard", node.Properties.InstanceType) } } func TestMapGCPVolumeType(t *testing.T) { tests := []struct { name string resourceGroup string description string wantType pricing.VolumeType wantRegional bool }{ { name: "PD-SSD zonal", resourceGroup: "SSD", description: "SSD backed PD Capacity", wantType: pricing.VolumeTypePDSSD, wantRegional: false, }, { name: "PD-SSD regional", resourceGroup: "SSD", description: "Regional SSD backed PD Capacity", wantType: pricing.VolumeTypePDSSD, wantRegional: true, }, { name: "PD-Standard", resourceGroup: "PDStandard", description: "Storage PD Capacity", wantType: pricing.VolumeTypePDStandard, wantRegional: false, }, { name: "PD-Balanced", resourceGroup: "PDBalanced", description: "Balanced PD Capacity", wantType: pricing.VolumeTypePDBalanced, wantRegional: false, }, { name: "PD-Extreme", resourceGroup: "PDExtreme", description: "Extreme PD Capacity", wantType: pricing.VolumeTypePDExtreme, wantRegional: false, }, { name: "Hyperdisk Balanced", resourceGroup: "HyperdiskBalanced", description: "Hyperdisk Balanced", wantType: pricing.VolumeTypeHyperdiskBalanced, wantRegional: false, }, { name: "Hyperdisk Extreme", resourceGroup: "HyperdiskExtreme", description: "Hyperdisk Extreme", wantType: pricing.VolumeTypeHyperdiskExtreme, wantRegional: false, }, { name: "Hyperdisk Throughput", resourceGroup: "HyperdiskThroughput", description: "Hyperdisk Throughput", wantType: pricing.VolumeTypeHyperdiskThroughput, wantRegional: false, }, { name: "Unknown type", resourceGroup: "UnknownDisk", description: "Some unknown disk", wantType: pricing.VolumeTypeNil, wantRegional: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotType, gotRegional := mapGCPVolumeType(tt.resourceGroup, tt.description) if gotType != tt.wantType { t.Errorf("mapGCPVolumeType() type = %v, want %v", gotType, tt.wantType) } if gotRegional != tt.wantRegional { t.Errorf("mapGCPVolumeType() regional = %v, want %v", gotRegional, tt.wantRegional) } }) } } func TestNormalizeInstanceType(t *testing.T) { tests := []struct { name string resourceGroup string description string want string }{ { name: "Custom instance", resourceGroup: "CPU", description: "Custom Instance Core running in Americas", want: "custom", }, { name: "N2 standard", resourceGroup: "RAM", description: "N2 Instance Ram running in Americas", want: "n2-standard", }, { name: "N2D AMD", resourceGroup: "CPU", description: "N2D AMD Instance Core running in Americas", want: "n2d-standard", }, { name: "N4 instance", resourceGroup: "CPU", description: "N4 Instance Core running in Americas", want: "n4-standard", }, { name: "A2 instance", resourceGroup: "RAM", description: "A2 Instance Ram running in Americas", want: "a2", }, { name: "C2 compute optimized", resourceGroup: "CPU", description: "Compute Optimized Core running in Americas", want: "c2-standard", }, { name: "E2 instance", resourceGroup: "CPU", description: "E2 Instance Core running in Americas", want: "e2", }, { name: "T2D AMD", resourceGroup: "RAM", description: "T2D AMD Instance Ram running in Americas", want: "t2d-standard", }, { name: "T2A ARM", resourceGroup: "CPU", description: "T2A ARM Instance Core running in Americas", want: "t2a-standard", }, { name: "Unknown type defaults to resource group", resourceGroup: "SomeType", description: "Some Instance Type", want: "sometype", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := normalizeInstanceType(tt.resourceGroup, tt.description) if got != tt.want { t.Errorf("normalizeInstanceType() = %v, want %v", got, tt.want) } }) } } func TestIsComputeResource(t *testing.T) { tests := []struct { name string resourceGroup string want bool }{ { name: "CPU is compute", resourceGroup: "CPU", want: true, }, { name: "RAM is compute", resourceGroup: "RAM", want: true, }, { name: "cpu lowercase is compute", resourceGroup: "cpu", want: true, }, { name: "ram lowercase is compute", resourceGroup: "ram", want: true, }, { name: "SSD is not compute", resourceGroup: "SSD", want: false, }, { name: "GPU is not compute", resourceGroup: "GPU", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isComputeResource(tt.resourceGroup) if got != tt.want { t.Errorf("isComputeResource() = %v, want %v", got, tt.want) } }) } } func TestIsStorageResource(t *testing.T) { tests := []struct { name string resourceGroup string want bool }{ { name: "SSD is storage", resourceGroup: "SSD", want: true, }, { name: "PDStandard is storage", resourceGroup: "PDStandard", want: true, }, { name: "PDBalanced is storage", resourceGroup: "PDBalanced", want: true, }, { name: "PDExtreme is storage", resourceGroup: "PDExtreme", want: true, }, { name: "HyperdiskBalanced is storage", resourceGroup: "HyperdiskBalanced", want: true, }, { name: "HyperdiskExtreme is storage", resourceGroup: "HyperdiskExtreme", want: true, }, { name: "HyperdiskThroughput is storage", resourceGroup: "HyperdiskThroughput", want: true, }, { name: "CPU is not storage", resourceGroup: "CPU", want: false, }, { name: "RAM is not storage", resourceGroup: "RAM", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isStorageResource(tt.resourceGroup) if got != tt.want { t.Errorf("isStorageResource() = %v, want %v", got, tt.want) } }) } } // Helper function to check if a string contains a substring func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && (s[0:len(substr)] == substr || contains(s[1:], substr)))) } // Made with Bob