Pārlūkot izejas kodu

Unit tests for Downloader.readPricesheet

Signed-off-by: Christian Muirhead <christian.muirhead@microsoft.com>
Christian Muirhead 3 gadi atpakaļ
vecāks
revīzija
c5fa6516d0

+ 21 - 16
pkg/cloud/azurepricesheet/downloader.go

@@ -169,15 +169,13 @@ func (d *Downloader[T]) readPricesheet(ctx context.Context, data io.Reader) (map
 			units[*meterInfo.Unit] = true
 		}
 
-		// TODO: add pricings for AzureFileStandardStorageClass
-
 		for key, pricing := range pricings {
 			results[key] = pricing
 		}
 	}
 
 	if len(results) == 0 {
-		return nil, fmt.Errorf("no matching pricing from pricesheet")
+		return nil, fmt.Errorf("no matching pricing from price sheet")
 	}
 
 	// Keep track of units seen so we can detect if there are any that
@@ -193,9 +191,12 @@ func (d *Downloader[T]) readPricesheet(ctx context.Context, data io.Reader) (map
 }
 
 func checkPricesheetHeader(header []string) error {
-	for name, col := range pricesheetCols {
+	if len(header) < len(pricesheetCols) {
+		return fmt.Errorf("too few header columns: got %d, expected %d", len(header), len(pricesheetCols))
+	}
+	for col, name := range pricesheetCols {
 		if !strings.EqualFold(header[col], name) {
-			return fmt.Errorf("unexpected header %q, expected %q", header[col], name)
+			return fmt.Errorf("unexpected header at col %d %q, expected %q", col, header[col], name)
 		}
 	}
 	return nil
@@ -217,17 +218,21 @@ func makeMeterInfo(row []string) (commerce.MeterInfo, error) {
 	}, nil
 }
 
-var pricesheetCols = map[string]int{
-	"Meter ID":           pricesheetMeterID,
-	"Meter name":         pricesheetMeterName,
-	"Meter category":     pricesheetMeterCategory,
-	"Meter sub-category": pricesheetMeterSubCategory,
-	"Meter region":       pricesheetMeterRegion,
-	"Unit":               pricesheetUnit,
-	"Unit price":         pricesheetUnitPrice,
-	"Currency code":      pricesheetCurrencyCode,
-	"Offer Id":           pricesheetOfferID,
-	"Price type":         pricesheetPriceType,
+var pricesheetCols = []string{
+	"Meter ID",
+	"Meter name",
+	"Meter category",
+	"Meter sub-category",
+	"Meter region",
+	"Unit",
+	"Unit of measure",
+	"Part number",
+	"Unit price",
+	"Currency code",
+	"Included quantity",
+	"Offer Id",
+	"Term",
+	"Price type",
 }
 
 const (

+ 114 - 0
pkg/cloud/azurepricesheet/downloader_test.go

@@ -0,0 +1,114 @@
+package azurepricesheet
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/commerce/mgmt/commerce"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDownloader_readPricesheet(t *testing.T) {
+	d := Downloader[fakePricing]{
+		TenantID:         "test-tenant-id",
+		ClientID:         "test-client-id",
+		ClientSecret:     "test-client-secret",
+		BillingAccount:   "test-billing-account",
+		OfferID:          "my-offer-id",
+		ConvertMeterInfo: convertMeter,
+	}
+	results, err := d.readPricesheet(context.Background(), strings.NewReader(pricesheetData))
+	require.NoError(t, err)
+
+	// Units and prices are normalised.
+	// Info for saving plans and other offers is skipped.
+	expected := map[string]*fakePricing{
+		"DC96as_v4": {price: "10.505", unit: "1 Hour"},
+		"DC2as_v4":  {price: "0.219", unit: "1 Hour"},
+		"VM1":       {price: "1.0", unit: "1 Hour"},
+		"VM2":       {price: "2.0", unit: "1 Hour"},
+	}
+	require.Equal(t, expected, results)
+}
+
+func TestDownloader_badHeader(t *testing.T) {
+	d := Downloader[fakePricing]{
+		TenantID:         "test-tenant-id",
+		ClientID:         "test-client-id",
+		ClientSecret:     "test-client-secret",
+		BillingAccount:   "test-billing-account",
+		OfferID:          "my-offer-id",
+		ConvertMeterInfo: convertMeter,
+	}
+	data := "\n\nMeter ID,Meter name,Meter category,Something else,,,,,,,,,,,,,,\n"
+	_, err := d.readPricesheet(context.Background(), strings.NewReader(data))
+	require.ErrorContains(t, err, `unexpected header at col 3 "Something else", expected "Meter sub-category"`)
+}
+
+func TestDownloader_shortHeader(t *testing.T) {
+	d := Downloader[fakePricing]{
+		TenantID:         "test-tenant-id",
+		ClientID:         "test-client-id",
+		ClientSecret:     "test-client-secret",
+		BillingAccount:   "test-billing-account",
+		OfferID:          "my-offer-id",
+		ConvertMeterInfo: convertMeter,
+	}
+	data := "\n\nMeter ID, Meter name, Meter category, Meter sub-category\n"
+	_, err := d.readPricesheet(context.Background(), strings.NewReader(data))
+	require.ErrorContains(t, err, "too few header columns: got 4, expected 14")
+}
+
+func TestDownloader_noMatchingPrices(t *testing.T) {
+	d := Downloader[fakePricing]{
+		TenantID:       "test-tenant-id",
+		ClientID:       "test-client-id",
+		ClientSecret:   "test-client-secret",
+		BillingAccount: "test-billing-account",
+		OfferID:        "my-offer-id",
+		ConvertMeterInfo: func(commerce.MeterInfo) (map[string]*fakePricing, error) {
+			return nil, nil
+		},
+	}
+	_, err := d.readPricesheet(context.Background(), strings.NewReader(pricesheetData))
+	require.ErrorContains(t, err, "no matching pricing from price sheet")
+}
+
+func convertMeter(info commerce.MeterInfo) (map[string]*fakePricing, error) {
+	switch *info.MeterName {
+	case "skip-this":
+		return nil, nil
+	case "multiple-prices":
+		return map[string]*fakePricing{
+			"VM1": {price: "1.0", unit: "1 Hour"},
+			"VM2": {price: "2.0", unit: "1 Hour"},
+		}, nil
+	case "error":
+		return nil, fmt.Errorf("there was an error handling this row!")
+	default:
+		return map[string]*fakePricing{
+			*info.MeterName: {price: fmt.Sprintf("%0.3f", *info.MeterRates["0"]), unit: *info.Unit},
+		}, nil
+	}
+}
+
+type fakePricing struct {
+	price string
+	unit  string
+}
+
+const pricesheetData = `Price Sheet Report for billing period - 202304
+
+Meter ID,Meter name,Meter category,Meter sub-category,Meter region,Unit,Unit of measure,Part number,Unit price,Currency code,Included quantity,Offer Id,Term,Price type
+d4236f8f-3ba6-5a9a-8c6b-14556538c44c,DC96as_v4,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70822,105.050000000000000,USD,0.00,my-offer-id,,Consumption
+d4236f8f-3ba6-5a9a-8c6b-14556538c44c,DC96as_v4,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70831,60.890000000000000,USD,0.00,other-offer-id,,Consumption
+e47a2c4c-4dc4-55d5-a8d7-ec5b1dcc9c08,DC2as_v4,Virtual Machines,DCasv4 Series,US East,100 Hours,100 Hours,AAF-70890,21.900000000000000,USD,0.000,my-offer-id,,Consumption
+e47a2c4c-4dc4-55d5-a8d7-ec5b1dcc9c08,DC2as_v4,Virtual Machines,DCasv4 Series,US East,100 Hours,100 Hours,AAF-70886,12.700000000000000,USD,0.000,other-offer-id,,Consumption
+cb8d72c0-2b02-5b41-9ac9-2809c04f17ff,DC16as_v4,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70911,17.510000000000000,USD,0.00,my-offer-id,,Savings Plan
+cb8d72c0-2b02-5b41-9ac9-2809c04f17ff,DC16as_v4,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70910,10.150000000000000,USD,0.00,other-offer-id,,Consumption
+d4236f8f-3ba6-5a9a-8c6b-14556538c44c,skip-this,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70822,105.050000000000000,USD,0.00,my-offer-id,,Consumption
+d4236f8f-3ba6-5a9a-8c6b-14556538c44c,multiple-prices,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70822,105.050000000000000,USD,0.00,my-offer-id,,Consumption
+d4236f8f-3ba6-5a9a-8c6b-14556538c44c,error,Virtual Machines,DCasv4 Series,US East,10 Hours,10 Hours,AAF-70822,105.050000000000000,USD,0.00,my-offer-id,,Consumption
+`