storagebillingparser_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. package azure
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "io"
  6. "os"
  7. "strings"
  8. "testing"
  9. "time"
  10. )
  11. func TestAzureStorageBillingParser_getMonthStrings(t *testing.T) {
  12. asbp := AzureStorageBillingParser{}
  13. loc := time.UTC // Use time.UTC constant instead of LoadLocation
  14. testCases := map[string]struct {
  15. start time.Time
  16. end time.Time
  17. expected []string
  18. }{
  19. "Single Month": {
  20. start: time.Date(2021, 2, 1, 00, 00, 00, 00, loc),
  21. end: time.Date(2021, 2, 3, 00, 00, 00, 00, loc),
  22. expected: []string{
  23. "20210201-20210228",
  24. },
  25. },
  26. "Two Month": {
  27. start: time.Date(2021, 2, 1, 00, 00, 00, 00, loc),
  28. end: time.Date(2021, 3, 3, 00, 00, 00, 00, loc),
  29. expected: []string{
  30. "20210201-20210228",
  31. "20210301-20210331",
  32. },
  33. },
  34. }
  35. for name, tc := range testCases {
  36. t.Run(name, func(t *testing.T) {
  37. months, err := asbp.getMonthStrings(tc.start, tc.end)
  38. if err != nil {
  39. t.Errorf("Could not retrieve month strings %v", err)
  40. }
  41. if len(months) != len(tc.expected) {
  42. t.Errorf("Did not create the expected number of month strings. Expected: %d, Actual: %d", len(tc.expected), len(months))
  43. }
  44. for i, monthStr := range months {
  45. if monthStr != tc.expected[i] {
  46. t.Errorf("Incorrect month string at index %d. Expected: %s, Actual: %s", i, tc.expected[i], monthStr)
  47. }
  48. }
  49. })
  50. }
  51. }
  52. func TestAzureStorageBillingParser_parseCSV(t *testing.T) {
  53. loc := time.UTC // Use time.UTC constant instead of LoadLocation
  54. start := time.Date(2021, 2, 1, 00, 00, 00, 00, loc)
  55. end := time.Date(2021, 2, 3, 00, 00, 00, 00, loc)
  56. tests := map[string]struct {
  57. input string
  58. expected []BillingRowValues
  59. }{
  60. "Virtual Machine": {
  61. input: "VirtualMachine.csv",
  62. expected: []BillingRowValues{
  63. {
  64. Date: start,
  65. MeterCategory: "Virtual Machines",
  66. SubscriptionID: "11111111-12ab-34dc-56ef-123456abcdef",
  67. InvoiceEntityID: "11111111-12ab-34dc-56ef-123456billing",
  68. InstanceID: "/subscriptions/11111111-12ab-34dc-56ef-123456abcdef/resourceGroups/Example-Resource-Group/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-12345678-vmss",
  69. Service: "Microsoft.Compute",
  70. Tags: map[string]string{
  71. "resourceNameSuffix": "12345678",
  72. "aksEngineVersion": "aks-release-v0.47.0-1-aks",
  73. "creationSource": "aks-aks-nodepool1-12345678-vmss",
  74. },
  75. AdditionalInfo: map[string]any{
  76. "ServiceType": "Standard_DS2_v2",
  77. "VMName": "aks-nodepool1-12345678-vmss_0",
  78. "VCPUs": 2.0,
  79. },
  80. Cost: 5,
  81. NetCost: 4,
  82. },
  83. },
  84. },
  85. "Missing Brackets": {
  86. input: "MissingBrackets.csv",
  87. expected: []BillingRowValues{
  88. {
  89. Date: start,
  90. MeterCategory: "Virtual Machines",
  91. SubscriptionID: "11111111-12ab-34dc-56ef-123456abcdef",
  92. InvoiceEntityID: "11111111-12ab-34dc-56ef-123456abcdef",
  93. InstanceID: "/subscriptions/11111111-12ab-34dc-56ef-123456abcdef/resourceGroups/Example-Resource-Group/providers/Microsoft.Compute/virtualMachineScaleSets/aks-nodepool1-12345678-vmss",
  94. Service: "Microsoft.Compute",
  95. Tags: map[string]string{
  96. "resourceNameSuffix": "12345678",
  97. "aksEngineVersion": "aks-release-v0.47.0-1-aks",
  98. "creationSource": "aks-aks-nodepool1-12345678-vmss",
  99. },
  100. AdditionalInfo: map[string]any{
  101. "ServiceType": "Standard_DS2_v2",
  102. "VMName": "aks-nodepool1-12345678-vmss_0",
  103. "VCPUs": 2.0,
  104. },
  105. Cost: 5,
  106. NetCost: 4,
  107. },
  108. },
  109. },
  110. }
  111. asbp := &AzureStorageBillingParser{}
  112. for name, tc := range tests {
  113. t.Run(name, func(t *testing.T) {
  114. csvRetriever := &TestCSVRetriever{
  115. CSVName: valueCasesPath + tc.input,
  116. }
  117. csvs, err := csvRetriever.getCSVReaders(start, end)
  118. if err != nil {
  119. t.Errorf("Failed to read specified CSV: %s", err.Error())
  120. }
  121. reader := csvs[0]
  122. var actual []*BillingRowValues
  123. resultFn := func(abv *BillingRowValues) error {
  124. actual = append(actual, abv)
  125. return nil
  126. }
  127. err = asbp.parseCSV(start, end, reader, resultFn)
  128. if err != nil {
  129. t.Errorf("Error generating BillingRowValues: %s", err.Error())
  130. }
  131. if len(actual) != len(tc.expected) {
  132. t.Errorf("Actual output length did not match expected. Expected: %d, Actual: %d", len(tc.expected), len(actual))
  133. }
  134. for i, this := range actual {
  135. that := tc.expected[i]
  136. if !this.Date.Equal(that.Date) {
  137. t.Errorf("Parsed data at index %d has incorrect Date value. Expected: %s, Actual: %s", i, this.Date.String(), that.Date.String())
  138. }
  139. if this.MeterCategory != that.MeterCategory {
  140. t.Errorf("Parsed data at index %d has incorrect MeterCategroy value. Expected: %s, Actual: %s", i, this.MeterCategory, that.MeterCategory)
  141. }
  142. if this.SubscriptionID != that.SubscriptionID {
  143. t.Errorf("Parsed data at index %d has incorrect SubscriptionID value. Expected: %s, Actual: %s", i, this.SubscriptionID, that.SubscriptionID)
  144. }
  145. if this.InvoiceEntityID != that.InvoiceEntityID {
  146. t.Errorf("Parsed data at index %d has incorrect InvoiceEntityID value. Expected: %s, Actual: %s", i, this.InvoiceEntityID, that.InvoiceEntityID)
  147. }
  148. if this.InstanceID != that.InstanceID {
  149. t.Errorf("Parsed data at index %d has incorrect InstanceID value. Expected: %s, Actual: %s", i, this.InstanceID, that.InstanceID)
  150. }
  151. if this.Service != that.Service {
  152. t.Errorf("Parsed data at index %d has incorrect Service value. Expected: %s, Actual: %s", i, this.Service, that.Service)
  153. }
  154. if this.Cost != that.Cost {
  155. t.Errorf("Parsed data at index %d has incorrect Cost value. Expected: %f, Actual: %f", i, this.Cost, that.Cost)
  156. }
  157. if this.NetCost != that.NetCost {
  158. t.Errorf("Parsed data at index %d has incorrect NetCost value. Expected: %f, Actual: %f", i, this.NetCost, that.NetCost)
  159. }
  160. if len(this.Tags) != len(that.Tags) {
  161. t.Errorf("Parsed data at index %d did not have the expected number of tags. Expected: %d, Actual: %d", i, len(that.Tags), len(this.Tags))
  162. }
  163. for key, thisTag := range this.Tags {
  164. thatTag, ok := that.Tags[key]
  165. if !ok {
  166. t.Errorf("Parsed data at index %d is has unexpected entry in Tags with key: %s", i, key)
  167. }
  168. if thisTag != thatTag {
  169. t.Errorf("Parsed data at index %d is has unexpected value in Tags for key: %s. Expected: %s, Actual: %s", i, key, thatTag, thisTag)
  170. }
  171. }
  172. for key, thisAI := range this.AdditionalInfo {
  173. thatAI, ok := that.AdditionalInfo[key]
  174. if !ok {
  175. t.Errorf("Parsed data at index %d is has unexpected entry in Additional Inforamation with key: %s", i, key)
  176. }
  177. if thisAI != thatAI {
  178. t.Errorf("Parsed data at index %d is has unexpected value in Tags for key: %s. Expected: %v, Actual: %v", i, key, thisAI, thatAI)
  179. }
  180. }
  181. }
  182. })
  183. }
  184. }
  185. func TestAzureStorageBillingParser_processLocalBillingFile(t *testing.T) {
  186. loc := time.UTC
  187. start := time.Date(2024, 10, 1, 0, 0, 0, 0, loc)
  188. end := time.Date(2024, 11, 30, 0, 0, 0, 0, loc)
  189. testCases := map[string]struct {
  190. fileName string
  191. expectedRows int
  192. expectError bool
  193. }{
  194. "Gzipped file": {
  195. fileName: "test_azure_billing.csv.gz",
  196. expectedRows: 5,
  197. expectError: false,
  198. },
  199. "Non-gzipped file": {
  200. fileName: "test_azure_billing.csv",
  201. expectedRows: 5,
  202. expectError: false,
  203. },
  204. }
  205. for name, tc := range testCases {
  206. t.Run(name, func(t *testing.T) {
  207. asbp := &AzureStorageBillingParser{}
  208. filePath := valueCasesPath + tc.fileName
  209. var rowCount int
  210. resultFn := func(abv *BillingRowValues) error {
  211. rowCount++
  212. if abv == nil {
  213. t.Error("Received nil BillingRowValues")
  214. }
  215. return nil
  216. }
  217. err := asbp.processLocalBillingFile(filePath, tc.fileName, start, end, resultFn)
  218. if tc.expectError && err == nil {
  219. t.Error("Expected error but got none")
  220. }
  221. if !tc.expectError && err != nil {
  222. t.Fatalf("Unexpected error: %v", err)
  223. }
  224. if rowCount != tc.expectedRows {
  225. t.Errorf("Expected %d rows, got %d rows", tc.expectedRows, rowCount)
  226. }
  227. })
  228. }
  229. }
  230. func TestAzureStorageBillingParser_processStreamBillingData(t *testing.T) {
  231. loc := time.UTC
  232. start := time.Date(2024, 10, 1, 0, 0, 0, 0, loc)
  233. end := time.Date(2024, 11, 30, 0, 0, 0, 0, loc)
  234. testCases := map[string]struct {
  235. fileName string
  236. expectedRows int
  237. }{
  238. "Gzipped stream": {
  239. fileName: "test_azure_billing.csv.gz",
  240. expectedRows: 5,
  241. },
  242. "Non-gzipped stream": {
  243. fileName: "test_azure_billing.csv",
  244. expectedRows: 5,
  245. },
  246. }
  247. for name, tc := range testCases {
  248. t.Run(name, func(t *testing.T) {
  249. asbp := &AzureStorageBillingParser{}
  250. // Read file into memory to simulate stream
  251. data, err := os.ReadFile(valueCasesPath + tc.fileName)
  252. if err != nil {
  253. t.Fatalf("Failed to read test file: %v", err)
  254. }
  255. streamReader := bytes.NewReader(data)
  256. var rowCount int
  257. resultFn := func(abv *BillingRowValues) error {
  258. rowCount++
  259. if abv == nil {
  260. t.Error("Received nil BillingRowValues")
  261. }
  262. return nil
  263. }
  264. err = asbp.processStreamBillingData(streamReader, tc.fileName, start, end, resultFn)
  265. if err != nil {
  266. t.Fatalf("Unexpected error: %v", err)
  267. }
  268. if rowCount != tc.expectedRows {
  269. t.Errorf("Expected %d rows, got %d rows", tc.expectedRows, rowCount)
  270. }
  271. })
  272. }
  273. }
  274. func TestDecompressIfGzipped(t *testing.T) {
  275. testCases := map[string]struct {
  276. blobName string
  277. content string
  278. shouldGzip bool
  279. expectError bool
  280. }{
  281. "Gzipped file with .gz extension": {
  282. blobName: "billing_export.csv.gz",
  283. content: "test,data\n1,2\n",
  284. shouldGzip: true,
  285. expectError: false,
  286. },
  287. "Gzipped file with .GZ extension (case insensitive)": {
  288. blobName: "billing_export.CSV.GZ",
  289. content: "test,data\n1,2\n",
  290. shouldGzip: true,
  291. expectError: false,
  292. },
  293. "Non-gzipped CSV file": {
  294. blobName: "billing_export.csv",
  295. content: "test,data\n1,2\n",
  296. shouldGzip: false,
  297. expectError: false,
  298. },
  299. "Non-gzipped file without extension": {
  300. blobName: "billing_export",
  301. content: "test,data\n1,2\n",
  302. shouldGzip: false,
  303. expectError: false,
  304. },
  305. }
  306. for name, tc := range testCases {
  307. t.Run(name, func(t *testing.T) {
  308. var inputReader io.Reader
  309. if tc.shouldGzip {
  310. // Create gzipped content
  311. var buf bytes.Buffer
  312. gw := gzip.NewWriter(&buf)
  313. _, err := gw.Write([]byte(tc.content))
  314. if err != nil {
  315. t.Fatalf("Failed to write gzip content: %v", err)
  316. }
  317. gw.Close()
  318. inputReader = &buf
  319. } else {
  320. // Use plain content
  321. inputReader = strings.NewReader(tc.content)
  322. }
  323. // Call decompressIfGzipped
  324. reader, err := decompressIfGzipped(inputReader, tc.blobName)
  325. if tc.expectError {
  326. if err == nil {
  327. t.Errorf("Expected error but got none")
  328. }
  329. return
  330. }
  331. if err != nil {
  332. t.Fatalf("Unexpected error: %v", err)
  333. }
  334. defer reader.Close()
  335. // Read and verify content
  336. output, err := io.ReadAll(reader)
  337. if err != nil {
  338. t.Fatalf("Failed to read from reader: %v", err)
  339. }
  340. if string(output) != tc.content {
  341. t.Errorf("Content mismatch. Expected: %q, Got: %q", tc.content, string(output))
  342. }
  343. })
  344. }
  345. }
  346. func TestDecompressIfGzipped_InvalidGzip(t *testing.T) {
  347. // Test with invalid gzip data
  348. blobName := "invalid.csv.gz"
  349. invalidData := strings.NewReader("this is not gzipped data")
  350. reader, err := decompressIfGzipped(invalidData, blobName)
  351. if err == nil {
  352. if reader != nil {
  353. reader.Close()
  354. }
  355. t.Error("Expected error for invalid gzip data, but got none")
  356. }
  357. }
  358. func TestDecompressIfGzipped_EmptyGzipFile(t *testing.T) {
  359. // Test with empty gzipped file
  360. blobName := "empty.csv.gz"
  361. var buf bytes.Buffer
  362. gw := gzip.NewWriter(&buf)
  363. gw.Close()
  364. reader, err := decompressIfGzipped(&buf, blobName)
  365. if err != nil {
  366. t.Fatalf("Unexpected error for empty gzip file: %v", err)
  367. }
  368. defer reader.Close()
  369. output, err := io.ReadAll(reader)
  370. if err != nil {
  371. t.Fatalf("Failed to read empty gzip file: %v", err)
  372. }
  373. if len(output) != 0 {
  374. t.Errorf("Expected empty output, got %d bytes", len(output))
  375. }
  376. }
  377. // TestDecompressIfGzipped_MultipleFiles tests processing multiple files in sequence
  378. // to ensure proper resource cleanup between iterations
  379. func TestDecompressIfGzipped_MultipleFiles(t *testing.T) {
  380. testFiles := []struct {
  381. name string
  382. content string
  383. shouldGzip bool
  384. }{
  385. {"file1.csv.gz", "data1,data2\nvalue1,value2\n", true},
  386. {"file2.csv", "data3,data4\nvalue3,value4\n", false},
  387. {"file3.csv.GZ", "data5,data6\nvalue5,value6\n", true},
  388. }
  389. for _, tf := range testFiles {
  390. t.Run(tf.name, func(t *testing.T) {
  391. var input io.Reader
  392. if tf.shouldGzip {
  393. var buf bytes.Buffer
  394. gw := gzip.NewWriter(&buf)
  395. _, err := gw.Write([]byte(tf.content))
  396. if err != nil {
  397. t.Fatalf("Failed to write gzip data: %v", err)
  398. }
  399. gw.Close()
  400. input = &buf
  401. } else {
  402. input = strings.NewReader(tf.content)
  403. }
  404. reader, err := decompressIfGzipped(input, tf.name)
  405. if err != nil {
  406. t.Fatalf("Failed to decompress %s: %v", tf.name, err)
  407. }
  408. defer reader.Close()
  409. output, err := io.ReadAll(reader)
  410. if err != nil {
  411. t.Fatalf("Failed to read from reader for %s: %v", tf.name, err)
  412. }
  413. if string(output) != tf.content {
  414. t.Errorf("Content mismatch for %s. Expected: %q, Got: %q", tf.name, tf.content, string(output))
  415. }
  416. })
  417. }
  418. }
  419. // TestDecompressIfGzipped_CaseInsensitiveExtension tests various case combinations
  420. func TestDecompressIfGzipped_CaseInsensitiveExtension(t *testing.T) {
  421. testCases := []string{
  422. "file.gz",
  423. "file.GZ",
  424. "file.Gz",
  425. "file.gZ",
  426. }
  427. content := "test,data\n1,2\n"
  428. for _, blobName := range testCases {
  429. t.Run(blobName, func(t *testing.T) {
  430. var buf bytes.Buffer
  431. gw := gzip.NewWriter(&buf)
  432. _, err := gw.Write([]byte(content))
  433. if err != nil {
  434. t.Fatalf("Failed to write gzip data: %v", err)
  435. }
  436. gw.Close()
  437. reader, err := decompressIfGzipped(&buf, blobName)
  438. if err != nil {
  439. t.Fatalf("Failed to decompress %s: %v", blobName, err)
  440. }
  441. defer reader.Close()
  442. output, err := io.ReadAll(reader)
  443. if err != nil {
  444. t.Fatalf("Failed to read from reader: %v", err)
  445. }
  446. if string(output) != content {
  447. t.Errorf("Content mismatch. Expected: %q, Got: %q", content, string(output))
  448. }
  449. })
  450. }
  451. }
  452. // TestDecompressIfGzipped_LargeFile tests handling of larger gzipped files
  453. func TestDecompressIfGzipped_LargeFile(t *testing.T) {
  454. // Create a larger CSV content (1000 rows)
  455. var contentBuilder strings.Builder
  456. contentBuilder.WriteString("col1,col2,col3,col4\n")
  457. for i := 0; i < 1000; i++ {
  458. contentBuilder.WriteString("value1,value2,value3,value4\n")
  459. }
  460. content := contentBuilder.String()
  461. // Gzip the content
  462. var buf bytes.Buffer
  463. gw := gzip.NewWriter(&buf)
  464. _, err := gw.Write([]byte(content))
  465. if err != nil {
  466. t.Fatalf("Failed to write gzip data: %v", err)
  467. }
  468. gw.Close()
  469. blobName := "large_file.csv.gz"
  470. reader, err := decompressIfGzipped(&buf, blobName)
  471. if err != nil {
  472. t.Fatalf("Failed to decompress large file: %v", err)
  473. }
  474. defer reader.Close()
  475. output, err := io.ReadAll(reader)
  476. if err != nil {
  477. t.Fatalf("Failed to read large file: %v", err)
  478. }
  479. if string(output) != content {
  480. t.Errorf("Content mismatch for large file. Expected %d bytes, got %d bytes", len(content), len(output))
  481. }
  482. t.Logf("Successfully processed large gzipped file with %d bytes", len(output))
  483. }