| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- package azure
- import (
- "bytes"
- "fmt"
- "io"
- "net/http"
- "testing"
- "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute"
- "github.com/Azure/azure-sdk-for-go/services/preview/commerce/mgmt/2015-06-01-preview/commerce"
- "github.com/stretchr/testify/require"
- "github.com/opencost/opencost/core/pkg/util/mathutil"
- "github.com/opencost/opencost/pkg/cloud/models"
- )
- func TestParseAzureSubscriptionID(t *testing.T) {
- cases := []struct {
- input string
- expected string
- }{
- {
- input: "azure:///subscriptions/0badafdf-1234-abcd-wxyz-123456789/...",
- expected: "0badafdf-1234-abcd-wxyz-123456789",
- },
- {
- input: "azure:/subscriptions/0badafdf-1234-abcd-wxyz-123456789/...",
- expected: "",
- },
- {
- input: "azure:///subscriptions//",
- expected: "",
- },
- {
- input: "",
- expected: "",
- },
- }
- for _, test := range cases {
- result := ParseAzureSubscriptionID(test.input)
- if result != test.expected {
- t.Errorf("Input: %s, Expected: %s, Actual: %s", test.input, test.expected, result)
- }
- }
- }
- func TestConvertMeterToPricings(t *testing.T) {
- regions := map[string]string{
- "useast": "US East",
- "japanwest": "Japan West",
- "australiasoutheast": "Australia Southeast",
- "norwaywest": "Norway West",
- }
- baseCPUPrice := "0.30000"
- meterInfo := func(category, subcategory, name, region string, rate float64) commerce.MeterInfo {
- return commerce.MeterInfo{
- MeterCategory: &category,
- MeterSubCategory: &subcategory,
- MeterName: &name,
- MeterRegion: ®ion,
- MeterRates: map[string]*float64{"0": &rate},
- }
- }
- t.Run("windows", func(t *testing.T) {
- info := meterInfo("Virtual Machines", "D2 Series Windows", "D2s v3", "AU Southeast", 0.3)
- results, err := convertMeterToPricings(info, regions, baseCPUPrice)
- require.NoError(t, err)
- require.Nil(t, results)
- })
- t.Run("storage", func(t *testing.T) {
- info := meterInfo("Storage", "Some SSD type", "P4 are good", "US East", 2000)
- results, err := convertMeterToPricings(info, regions, baseCPUPrice)
- require.NoError(t, err)
- expected := map[string]*AzurePricing{
- "useast,premium_ssd": {
- PV: &models.PV{Cost: "0.085616", Region: "useast"},
- },
- }
- require.Equal(t, expected, results)
- })
- t.Run("virtual machines", func(t *testing.T) {
- info := meterInfo("Virtual Machines", "Eav4/Easv4 Series", "E96a v4/E96as v4 Low Priority", "JA West", 10)
- results, err := convertMeterToPricings(info, regions, baseCPUPrice)
- require.NoError(t, err)
- expected := map[string]*AzurePricing{
- "japanwest,Standard_E96a_v4,preemptible": {
- Node: &models.Node{Cost: "10.000000", BaseCPUPrice: "0.30000", UsageType: "preemptible"},
- },
- "japanwest,Standard_E96as_v4,preemptible": {
- Node: &models.Node{Cost: "10.000000", BaseCPUPrice: "0.30000", UsageType: "preemptible"},
- },
- }
- require.Equal(t, expected, results)
- })
- }
- func TestAzure_findCostForDisk(t *testing.T) {
- var loc string = "location"
- var size int32 = 1
- az := &Azure{
- Pricing: map[string]*AzurePricing{
- "location,nil": nil,
- "location,nilpv": {
- PV: nil,
- },
- "location,ssd": {
- PV: &models.PV{
- Cost: "1",
- },
- },
- },
- }
- testCases := []struct {
- name string
- disk *compute.Disk
- exp float64
- expErr error
- }{
- {
- "disk is nil",
- nil,
- 0.0,
- fmt.Errorf("disk is empty"),
- },
- {
- "nil location",
- &compute.Disk{
- Location: nil,
- Sku: &compute.DiskSku{
- Name: "ssd",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: &size,
- },
- },
- 0.0,
- fmt.Errorf("failed to find pricing for key: ,ssd"),
- },
- {
- "nil disk properties",
- &compute.Disk{
- Location: &loc,
- Sku: &compute.DiskSku{
- Name: "ssd",
- },
- DiskProperties: nil,
- },
- 0.0,
- fmt.Errorf("disk properties are nil"),
- },
- {
- "nil disk size",
- &compute.Disk{
- Location: &loc,
- Sku: &compute.DiskSku{
- Name: "ssd",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: nil,
- },
- },
- 0.0,
- fmt.Errorf("disk size is nil"),
- },
- {
- "sku does not exist",
- &compute.Disk{
- Location: &loc,
- Sku: &compute.DiskSku{
- Name: "doesnotexist",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: &size,
- },
- },
- 0.0,
- fmt.Errorf("failed to find pricing for key: location,doesnotexist"),
- },
- {
- "pricing is nil",
- &compute.Disk{
- Sku: &compute.DiskSku{
- Name: "nil",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: &size,
- },
- },
- 0.0,
- fmt.Errorf("failed to find pricing for key: location,nil"),
- },
- {
- "pricing.PV is nil",
- &compute.Disk{
- Sku: &compute.DiskSku{
- Name: "nilpv",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: &size,
- },
- },
- 0.0,
- fmt.Errorf("pricing for key 'location,nilpv' has nil PV"),
- },
- {
- "valid (ssd)",
- &compute.Disk{
- Location: &loc,
- Sku: &compute.DiskSku{
- Name: "ssd",
- },
- DiskProperties: &compute.DiskProperties{
- DiskSizeGB: &size,
- },
- },
- 730.0,
- nil,
- },
- {
- "nil sku",
- &compute.Disk{
- Location: nil,
- Sku: nil,
- DiskProperties: nil,
- },
- 0.0,
- fmt.Errorf("disk sku is nil"),
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- act, actErr := az.findCostForDisk(tc.disk)
- if actErr != nil && tc.expErr == nil {
- t.Fatalf("unexpected error: %s", actErr)
- }
- if tc.expErr != nil && actErr == nil {
- t.Fatalf("missing expected error: %s", tc.expErr)
- }
- if !mathutil.Approximately(tc.exp, act) {
- t.Fatalf("expected value %f; got %f", tc.exp, act)
- }
- })
- }
- }
- func Test_buildAzureRetailPricesURL(t *testing.T) {
- testCases := []struct {
- name string
- region string
- skuName string
- currencyCode string
- expected string
- }{
- {
- name: "all parameters provided",
- region: "eastus",
- skuName: "Standard_D8ds_v5",
- currencyCode: "USD",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='USD'&$filter=armRegionName+eq+%27eastus%27+and+armSkuName+eq+%27Standard_D8ds_v5%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "no currency code",
- region: "westus",
- skuName: "Standard_D4s_v3",
- currencyCode: "",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0&$filter=armRegionName+eq+%27westus%27+and+armSkuName+eq+%27Standard_D4s_v3%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "no region",
- region: "",
- skuName: "Standard_D8s_v3",
- currencyCode: "EUR",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='EUR'&$filter=armSkuName+eq+%27Standard_D8s_v3%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "no sku name",
- region: "northeurope",
- skuName: "",
- currencyCode: "GBP",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='GBP'&$filter=armRegionName+eq+%27northeurope%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "only currency code",
- region: "",
- skuName: "",
- currencyCode: "JPY",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='JPY'&$filter=serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "no parameters",
- region: "",
- skuName: "",
- currencyCode: "",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0&$filter=serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "region with special characters",
- region: "south-central-us",
- skuName: "Standard_B2s",
- currencyCode: "USD",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='USD'&$filter=armRegionName+eq+%27south-central-us%27+and+armSkuName+eq+%27Standard_B2s%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- {
- name: "sku name with underscores",
- region: "eastus2",
- skuName: "Standard_E16_v3",
- currencyCode: "CAD",
- expected: "https://prices.azure.com/api/retail/prices?$skip=0¤cyCode='CAD'&$filter=armRegionName+eq+%27eastus2%27+and+armSkuName+eq+%27Standard_E16_v3%27+and+serviceFamily+eq+%27Compute%27+and+type+eq+%27Consumption%27+and+contains%28meterName%2C%27Low+Priority%27%29+eq+false",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result := buildAzureRetailPricesURL(tc.region, tc.skuName, tc.currencyCode)
- require.Equal(t, tc.expected, result, "URL mismatch for test case: %s", tc.name)
- })
- }
- }
- func Test_extractAzureVMRetailAndSpotPrices(t *testing.T) {
- testCases := []struct {
- name string
- jsonResponse string
- expectedRetail string
- expectedSpot string
- expectedError bool
- expectedErrorMsg string
- }{
- {
- name: "valid response with retail and spot prices",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [
- {
- "currencyCode": "USD",
- "tierMinimumUnits": 0,
- "retailPrice": 0.384,
- "unitPrice": 0.384,
- "armRegionName": "eastus2",
- "location": "US East 2",
- "effectiveStartDate": "2023-01-01T00:00:00Z",
- "meterId": "abc-123",
- "meterName": "D8ds v5",
- "productId": "DZH318Z0BQ4B",
- "skuId": "DZH318Z0BQ4B/00G1",
- "productName": "Virtual Machines Ddsv5 Series",
- "skuName": "D8ds v5",
- "serviceName": "Virtual Machines",
- "serviceId": "DZH313Z7MMC8",
- "serviceFamily": "Compute",
- "unitOfMeasure": "1 Hour",
- "type": "Consumption",
- "isPrimaryMeterRegion": true,
- "armSkuName": "Standard_D8ds_v5"
- },
- {
- "currencyCode": "USD",
- "tierMinimumUnits": 0,
- "retailPrice": 0.0768,
- "unitPrice": 0.0768,
- "armRegionName": "eastus2",
- "location": "US East 2",
- "effectiveStartDate": "2023-01-01T00:00:00Z",
- "meterId": "def-456",
- "meterName": "D8ds v5 Spot",
- "productId": "DZH318Z0BQ4B",
- "skuId": "DZH318Z0BQ4B/00G2",
- "productName": "Virtual Machines Ddsv5 Series",
- "skuName": "D8ds v5 Spot",
- "serviceName": "Virtual Machines",
- "serviceId": "DZH313Z7MMC8",
- "serviceFamily": "Compute",
- "unitOfMeasure": "1 Hour",
- "type": "Consumption",
- "isPrimaryMeterRegion": true,
- "armSkuName": "Standard_D8ds_v5"
- }
- ],
- "NextPageLink": "",
- "Count": 2
- }`,
- expectedRetail: "0.384000",
- expectedSpot: "0.076800",
- expectedError: false,
- },
- {
- name: "only retail price available",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [
- {
- "currencyCode": "USD",
- "retailPrice": 0.192,
- "armRegionName": "westus",
- "productName": "Virtual Machines Dsv3 Series",
- "skuName": "D4s v3",
- "armSkuName": "Standard_D4s_v3"
- }
- ],
- "Count": 1
- }`,
- expectedRetail: "0.192000",
- expectedSpot: "",
- expectedError: false,
- },
- {
- name: "only spot price available",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [
- {
- "currencyCode": "USD",
- "retailPrice": 0.0384,
- "armRegionName": "eastus",
- "productName": "Virtual Machines Dsv3 Series",
- "skuName": "D4s v3 Spot",
- "armSkuName": "Standard_D4s_v3"
- }
- ],
- "Count": 1
- }`,
- expectedRetail: "",
- expectedSpot: "0.038400",
- expectedError: false,
- },
- {
- name: "filters out Windows instances",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [
- {
- "currencyCode": "USD",
- "retailPrice": 0.5,
- "armRegionName": "eastus",
- "productName": "Virtual Machines Dsv3 Series Windows",
- "skuName": "D4s v3",
- "armSkuName": "Standard_D4s_v3"
- },
- {
- "currencyCode": "USD",
- "retailPrice": 0.192,
- "armRegionName": "eastus",
- "productName": "Virtual Machines Dsv3 Series",
- "skuName": "D4s v3",
- "armSkuName": "Standard_D4s_v3"
- }
- ],
- "Count": 2
- }`,
- expectedRetail: "0.192000",
- expectedSpot: "",
- expectedError: false,
- },
- {
- name: "filters out low priority instances",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [
- {
- "currencyCode": "USD",
- "retailPrice": 0.05,
- "armRegionName": "eastus",
- "productName": "Virtual Machines Dsv3 Series",
- "skuName": "D4s v3 Low Priority",
- "armSkuName": "Standard_D4s_v3"
- },
- {
- "currencyCode": "USD",
- "retailPrice": 0.192,
- "armRegionName": "eastus",
- "productName": "Virtual Machines Dsv3 Series",
- "skuName": "D4s v3",
- "armSkuName": "Standard_D4s_v3"
- }
- ],
- "Count": 2
- }`,
- expectedRetail: "0.192000",
- expectedSpot: "",
- expectedError: false,
- },
- {
- name: "empty items array",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "CustomerEntityId": "Default",
- "CustomerEntityType": "Retail",
- "Items": [],
- "Count": 0
- }`,
- expectedRetail: "",
- expectedSpot: "",
- expectedError: false,
- },
- {
- name: "invalid JSON",
- jsonResponse: `{
- "BillingCurrency": "USD",
- "Items": [
- {
- "retailPrice": "invalid"
- }
- ]
- `,
- expectedRetail: "",
- expectedSpot: "",
- expectedError: true,
- expectedErrorMsg: "error unmarshalling data",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // Create a mock http.Response with the JSON response as the body
- resp := &http.Response{
- StatusCode: 200,
- Body: io.NopCloser(bytes.NewBufferString(tc.jsonResponse)),
- }
- retailPrice, spotPrice, err := extractAzureVMRetailAndSpotPrices(resp)
- if tc.expectedError {
- require.Error(t, err)
- if tc.expectedErrorMsg != "" {
- require.Contains(t, err.Error(), tc.expectedErrorMsg)
- }
- } else {
- require.NoError(t, err)
- require.Equal(t, tc.expectedRetail, retailPrice, "Retail price mismatch")
- require.Equal(t, tc.expectedSpot, spotPrice, "Spot price mismatch")
- }
- })
- }
- }
|