azurepricingsource_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. package azure
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "strings"
  6. "testing"
  7. "github.com/opencost/opencost/core/pkg/pricing"
  8. )
  9. func TestMapAzureDiskType(t *testing.T) {
  10. tests := []struct {
  11. name string
  12. skuName string
  13. expected pricing.VolumeType
  14. }{
  15. {
  16. name: "Premium SSD V2",
  17. skuName: "Premium SSD v2 Managed Disk",
  18. expected: pricing.VolumeTypePremiumV2LRS,
  19. },
  20. {
  21. name: "PremiumV2 variant",
  22. skuName: "PremiumV2 LRS Disk",
  23. expected: pricing.VolumeTypePremiumV2LRS,
  24. },
  25. {
  26. name: "Premium SSD",
  27. skuName: "Premium SSD Managed Disk",
  28. expected: pricing.VolumeTypePremiumLRS,
  29. },
  30. {
  31. name: "Standard SSD",
  32. skuName: "Standard SSD Managed Disk",
  33. expected: pricing.VolumeTypeStandardSSDLRS,
  34. },
  35. {
  36. name: "StandardSSD variant",
  37. skuName: "StandardSSD LRS",
  38. expected: pricing.VolumeTypeStandardSSDLRS,
  39. },
  40. {
  41. name: "Standard HDD",
  42. skuName: "Standard HDD Managed Disk",
  43. expected: pricing.VolumeTypeStandardHDDLRS,
  44. },
  45. {
  46. name: "Ultra SSD",
  47. skuName: "Ultra SSD Managed Disk",
  48. expected: pricing.VolumeTypeUltraSSDLRS,
  49. },
  50. {
  51. name: "Unknown type",
  52. skuName: "Some Unknown Disk Type",
  53. expected: pricing.VolumeTypeNil,
  54. },
  55. {
  56. name: "Case insensitive Premium",
  57. skuName: "PREMIUM SSD MANAGED DISK",
  58. expected: pricing.VolumeTypePremiumLRS,
  59. },
  60. }
  61. for _, tt := range tests {
  62. t.Run(tt.name, func(t *testing.T) {
  63. result := mapAzureDiskType(tt.skuName)
  64. if result != tt.expected {
  65. t.Errorf("mapAzureDiskType(%q) = %v, want %v", tt.skuName, result, tt.expected)
  66. }
  67. })
  68. }
  69. }
  70. func TestIncludeItem(t *testing.T) {
  71. source := &AzurePricingSource{
  72. config: AzurePricingSourceConfig{
  73. CurrencyCode: "USD",
  74. },
  75. }
  76. tests := []struct {
  77. name string
  78. item AzurePricingAttributes
  79. expected bool
  80. }{
  81. {
  82. name: "valid Linux VM",
  83. item: AzurePricingAttributes{
  84. ArmSkuName: "Standard_D2s_v3",
  85. ArmRegionName: "eastus",
  86. ProductName: "Virtual Machines Dsv3 Series",
  87. SkuName: "D2s v3",
  88. },
  89. expected: true,
  90. },
  91. {
  92. name: "Windows VM - excluded",
  93. item: AzurePricingAttributes{
  94. ArmSkuName: "Standard_D2s_v3",
  95. ArmRegionName: "eastus",
  96. ProductName: "Virtual Machines Dsv3 Series Windows",
  97. SkuName: "D2s v3",
  98. },
  99. expected: false,
  100. },
  101. {
  102. name: "Spot - included",
  103. item: AzurePricingAttributes{
  104. ArmSkuName: "Standard_D2s_v3",
  105. ArmRegionName: "eastus",
  106. ProductName: "Virtual Machines Dsv3 Series",
  107. SkuName: "D2s v3 Spot",
  108. },
  109. expected: true,
  110. },
  111. {
  112. name: "Low priority - excluded",
  113. item: AzurePricingAttributes{
  114. ArmSkuName: "Standard_D2s_v3",
  115. ArmRegionName: "eastus",
  116. ProductName: "Virtual Machines Dsv3 Series",
  117. SkuName: "D2s v3 Low Priority",
  118. },
  119. expected: false,
  120. },
  121. {
  122. name: "Cloud Services - excluded",
  123. item: AzurePricingAttributes{
  124. ArmSkuName: "Standard_D2s_v3",
  125. ArmRegionName: "eastus",
  126. ProductName: "Cloud Services Dsv3 Series",
  127. SkuName: "D2s v3",
  128. },
  129. expected: false,
  130. },
  131. {
  132. name: "CloudServices variant - excluded",
  133. item: AzurePricingAttributes{
  134. ArmSkuName: "Standard_D2s_v3",
  135. ArmRegionName: "eastus",
  136. ProductName: "CloudServices Dsv3 Series",
  137. SkuName: "D2s v3",
  138. },
  139. expected: false,
  140. },
  141. {
  142. name: "Missing ArmSkuName - excluded",
  143. item: AzurePricingAttributes{
  144. ArmSkuName: "",
  145. ArmRegionName: "eastus",
  146. ProductName: "Virtual Machines Dsv3 Series",
  147. SkuName: "D2s v3",
  148. },
  149. expected: false,
  150. },
  151. {
  152. name: "Missing ArmRegionName - excluded",
  153. item: AzurePricingAttributes{
  154. ArmSkuName: "Standard_D2s_v3",
  155. ArmRegionName: "",
  156. ProductName: "Virtual Machines Dsv3 Series",
  157. SkuName: "D2s v3",
  158. },
  159. expected: false,
  160. },
  161. }
  162. for _, tt := range tests {
  163. t.Run(tt.name, func(t *testing.T) {
  164. result := source.includeItem(tt.item)
  165. if result != tt.expected {
  166. t.Errorf("includeItem() = %v, want %v", result, tt.expected)
  167. }
  168. })
  169. }
  170. }
  171. func TestIncludeDiskItem(t *testing.T) {
  172. source := &AzurePricingSource{
  173. config: AzurePricingSourceConfig{
  174. CurrencyCode: "USD",
  175. },
  176. }
  177. tests := []struct {
  178. name string
  179. item AzurePricingAttributes
  180. expected bool
  181. }{
  182. {
  183. name: "Managed disk - included",
  184. item: AzurePricingAttributes{
  185. ArmRegionName: "eastus",
  186. ProductName: "Premium SSD Managed Disk",
  187. SkuName: "P10 LRS",
  188. },
  189. expected: true,
  190. },
  191. {
  192. name: "Managed Disk uppercase - included",
  193. item: AzurePricingAttributes{
  194. ArmRegionName: "eastus",
  195. ProductName: "PREMIUM SSD MANAGED DISK",
  196. SkuName: "P10 LRS",
  197. },
  198. expected: true,
  199. },
  200. {
  201. name: "Unmanaged disk - excluded",
  202. item: AzurePricingAttributes{
  203. ArmRegionName: "eastus",
  204. ProductName: "Premium SSD Unmanaged Disk",
  205. SkuName: "P10",
  206. },
  207. expected: false,
  208. },
  209. {
  210. name: "Missing region - excluded",
  211. item: AzurePricingAttributes{
  212. ArmRegionName: "",
  213. ProductName: "Premium SSD Managed Disk",
  214. SkuName: "P10 LRS",
  215. },
  216. expected: false,
  217. },
  218. {
  219. name: "Storage account - excluded",
  220. item: AzurePricingAttributes{
  221. ArmRegionName: "eastus",
  222. ProductName: "Storage Account",
  223. SkuName: "Standard LRS",
  224. },
  225. expected: false,
  226. },
  227. }
  228. for _, tt := range tests {
  229. t.Run(tt.name, func(t *testing.T) {
  230. result := source.includeDiskItem(tt.item)
  231. if result != tt.expected {
  232. t.Errorf("includeDiskItem() = %v, want %v", result, tt.expected)
  233. }
  234. })
  235. }
  236. }
  237. func TestBuildVMURL(t *testing.T) {
  238. tests := []struct {
  239. name string
  240. currencyCode string
  241. wantContains []string
  242. }{
  243. {
  244. name: "USD currency",
  245. currencyCode: "USD",
  246. wantContains: []string{
  247. "prices.azure.com",
  248. "serviceName+eq+%27Virtual+Machines%27",
  249. "priceType+eq+%27Consumption%27",
  250. "currencyCode=USD",
  251. },
  252. },
  253. {
  254. name: "EUR currency",
  255. currencyCode: "EUR",
  256. wantContains: []string{
  257. "prices.azure.com",
  258. "currencyCode=EUR",
  259. },
  260. },
  261. {
  262. name: "Empty currency",
  263. currencyCode: "",
  264. wantContains: []string{
  265. "prices.azure.com",
  266. "serviceName+eq+%27Virtual+Machines%27",
  267. },
  268. },
  269. }
  270. for _, tt := range tests {
  271. t.Run(tt.name, func(t *testing.T) {
  272. source := &AzurePricingSource{
  273. config: AzurePricingSourceConfig{
  274. CurrencyCode: tt.currencyCode,
  275. },
  276. }
  277. url := source.buildVMURL()
  278. for _, want := range tt.wantContains {
  279. if !strings.Contains(url, want) {
  280. t.Errorf("buildVMURL() = %v, want to contain %v", url, want)
  281. }
  282. }
  283. })
  284. }
  285. }
  286. func TestBuildDiskURL(t *testing.T) {
  287. tests := []struct {
  288. name string
  289. currencyCode string
  290. wantContains []string
  291. }{
  292. {
  293. name: "USD currency",
  294. currencyCode: "USD",
  295. wantContains: []string{
  296. "prices.azure.com",
  297. "serviceName+eq+%27Storage%27",
  298. "priceType+eq+%27Consumption%27",
  299. "currencyCode=USD",
  300. },
  301. },
  302. {
  303. name: "EUR currency",
  304. currencyCode: "EUR",
  305. wantContains: []string{
  306. "prices.azure.com",
  307. "currencyCode=EUR",
  308. },
  309. },
  310. }
  311. for _, tt := range tests {
  312. t.Run(tt.name, func(t *testing.T) {
  313. source := &AzurePricingSource{
  314. config: AzurePricingSourceConfig{
  315. CurrencyCode: tt.currencyCode,
  316. },
  317. }
  318. url := source.buildDiskURL()
  319. for _, want := range tt.wantContains {
  320. if !strings.Contains(url, want) {
  321. t.Errorf("buildDiskURL() = %v, want to contain %v", url, want)
  322. }
  323. }
  324. })
  325. }
  326. }
  327. func TestNewAzurePricingSource(t *testing.T) {
  328. config := AzurePricingSourceConfig{
  329. CurrencyCode: "USD",
  330. }
  331. source := NewAzurePricingSource(config)
  332. if source == nil {
  333. t.Fatal("NewAzurePricingSource() returned nil")
  334. }
  335. if source.config.CurrencyCode != "USD" {
  336. t.Errorf("CurrencyCode = %v, want USD", source.config.CurrencyCode)
  337. }
  338. }
  339. func TestIsSpotItem(t *testing.T) {
  340. tests := []struct {
  341. name string
  342. skuName string
  343. expected bool
  344. }{
  345. {
  346. name: "Spot suffix",
  347. skuName: "D2s v3 Spot",
  348. expected: true,
  349. },
  350. {
  351. name: "Spot suffix lowercase",
  352. skuName: "d2s v3 spot",
  353. expected: true,
  354. },
  355. {
  356. name: "On-demand SKU",
  357. skuName: "D2s v3",
  358. expected: false,
  359. },
  360. {
  361. name: "Low priority SKU",
  362. skuName: "D2s v3 Low Priority",
  363. expected: false,
  364. },
  365. {
  366. name: "Spot substring but not suffix",
  367. skuName: "Spot Instance D2s v3",
  368. expected: false,
  369. },
  370. }
  371. for _, tt := range tests {
  372. t.Run(tt.name, func(t *testing.T) {
  373. result := isSpotItem(AzurePricingAttributes{SkuName: tt.skuName})
  374. if result != tt.expected {
  375. t.Errorf("isSpotItem(%q) = %v, want %v", tt.skuName, result, tt.expected)
  376. }
  377. })
  378. }
  379. }
  380. func TestParseVMPage_Spot(t *testing.T) {
  381. source := &AzurePricingSource{
  382. config: AzurePricingSourceConfig{
  383. CurrencyCode: "USD",
  384. },
  385. }
  386. const onDemandPrice float32 = 0.1
  387. const spotPrice float32 = 0.03
  388. page := AzurePricing{
  389. Items: []AzurePricingAttributes{
  390. {
  391. ArmSkuName: "Standard_D2s_v3",
  392. ArmRegionName: "eastus",
  393. ProductName: "Virtual Machines Dsv3 Series",
  394. SkuName: "D2s v3",
  395. RetailPrice: onDemandPrice,
  396. },
  397. {
  398. ArmSkuName: "Standard_D2s_v3",
  399. ArmRegionName: "eastus",
  400. ProductName: "Virtual Machines Dsv3 Series",
  401. SkuName: "D2s v3 Spot",
  402. RetailPrice: spotPrice,
  403. },
  404. },
  405. }
  406. data, err := json.Marshal(page)
  407. if err != nil {
  408. t.Fatalf("failed to marshal test page: %v", err)
  409. }
  410. ps := &pricing.PricingSet{
  411. NodePricing: []*pricing.NodePricing{},
  412. PersistentVolumePricing: []*pricing.PersistentVolumePricing{},
  413. }
  414. seen := make(map[nodeKey]struct{})
  415. _, err = source.parseVMPage(bytes.NewReader(data), ps, seen)
  416. if err != nil {
  417. t.Fatalf("parseVMPage() error = %v", err)
  418. }
  419. if len(ps.NodePricing) != 2 {
  420. t.Fatalf("parseVMPage() created %d node pricing entries, want 2", len(ps.NodePricing))
  421. }
  422. var sawOnDemand, sawSpot bool
  423. for _, np := range ps.NodePricing {
  424. switch np.Properties.Provisioning {
  425. case pricing.ProvisioningOnDemand:
  426. sawOnDemand = true
  427. // Compare against the float32 value widened to float64, matching
  428. // the precision that RetailPrice (float32) actually carries.
  429. if np.Prices[pricing.ResourceNode].Price != float64(onDemandPrice) {
  430. t.Errorf("on-demand price = %v, want %v", np.Prices[pricing.ResourceNode].Price, float64(onDemandPrice))
  431. }
  432. case pricing.ProvisioningSpot:
  433. sawSpot = true
  434. if np.Prices[pricing.ResourceNode].Price != float64(spotPrice) {
  435. t.Errorf("spot price = %v, want %v", np.Prices[pricing.ResourceNode].Price, float64(spotPrice))
  436. }
  437. }
  438. }
  439. if !sawOnDemand {
  440. t.Error("expected an on-demand node pricing entry")
  441. }
  442. if !sawSpot {
  443. t.Error("expected a spot node pricing entry")
  444. }
  445. }