pricesheetdownloader_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. package azure
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/commerce/mgmt/commerce"
  8. "github.com/opencost/opencost/pkg/cloud/models"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func TestDownloader(t *testing.T) {
  12. d := PriceSheetDownloader{
  13. TenantID: "test-tenant-id",
  14. ClientID: "test-client-id",
  15. ClientSecret: "test-client-secret",
  16. BillingAccount: "test-billing-account",
  17. OfferID: "my-offer-id",
  18. ConvertMeterInfo: convertMeter,
  19. }
  20. t.Run("read prices", func(t *testing.T) {
  21. results, err := d.readPricesheet(context.Background(), strings.NewReader(pricesheetData))
  22. require.NoError(t, err)
  23. // Units and prices are normalised.
  24. // Info for saving plans and other offers is skipped.
  25. expected := map[string]*AzurePricing{
  26. "DC96as_v4 1 Hour": {Node: &models.Node{Cost: "10.505"}},
  27. "DC2as_v4 1 Hour": {Node: &models.Node{Cost: "0.219"}},
  28. "VM1 1 Hour": {Node: &models.Node{Cost: "1.0"}},
  29. "VM2 1 Hour": {Node: &models.Node{Cost: "2.0"}},
  30. }
  31. require.Equal(t, expected, results)
  32. })
  33. t.Run("bad header", func(t *testing.T) {
  34. data := "\n\nMeter ID,Meter name,Meter category,Something else,,,,,,,,,,,,,,\n"
  35. _, err := d.readPricesheet(context.Background(), strings.NewReader(data))
  36. require.ErrorContains(t, err, `unexpected header at col 3 "Something else", expected "Meter sub-category"`)
  37. })
  38. t.Run("short header", func(t *testing.T) {
  39. data := "\n\nMeter ID, Meter name, Meter category, Meter sub-category\n"
  40. _, err := d.readPricesheet(context.Background(), strings.NewReader(data))
  41. require.ErrorContains(t, err, "too few header columns: got 4, expected 14")
  42. })
  43. t.Run("no matching prices", func(t *testing.T) {
  44. d := PriceSheetDownloader{
  45. TenantID: "test-tenant-id",
  46. ClientID: "test-client-id",
  47. ClientSecret: "test-client-secret",
  48. BillingAccount: "test-billing-account",
  49. OfferID: "my-offer-id",
  50. ConvertMeterInfo: func(commerce.MeterInfo) (map[string]*AzurePricing, error) {
  51. return nil, nil
  52. },
  53. }
  54. _, err := d.readPricesheet(context.Background(), strings.NewReader(pricesheetData))
  55. require.ErrorContains(t, err, "no matching pricing from price sheet")
  56. })
  57. }
  58. func convertMeter(info commerce.MeterInfo) (map[string]*AzurePricing, error) {
  59. switch *info.MeterName {
  60. case "skip-this":
  61. return nil, nil
  62. case "multiple-prices":
  63. return map[string]*AzurePricing{
  64. "VM1 1 Hour": {Node: &models.Node{Cost: "1.0"}},
  65. "VM2 1 Hour": {Node: &models.Node{Cost: "2.0"}},
  66. }, nil
  67. case "error":
  68. return nil, fmt.Errorf("there was an error handling this row!")
  69. default:
  70. return map[string]*AzurePricing{
  71. *info.MeterName + " " + *info.Unit: {
  72. Node: &models.Node{Cost: fmt.Sprintf("%0.3f", *info.MeterRates["0"])},
  73. },
  74. }, nil
  75. }
  76. }
  77. const pricesheetData = `Price Sheet Report for billing period - 202304
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. `