athenaintegration_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. package aws
  2. import (
  3. "fmt"
  4. "os"
  5. "reflect"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/aws/aws-sdk-go-v2/service/athena/types"
  10. "github.com/opencost/opencost/core/pkg/opencost"
  11. "github.com/opencost/opencost/core/pkg/util/json"
  12. "github.com/opencost/opencost/core/pkg/util/timeutil"
  13. )
  14. func TestAthenaIntegration_GetCloudCost(t *testing.T) {
  15. athenaConfigPath := os.Getenv("ATHENA_CONFIGURATION")
  16. if athenaConfigPath == "" {
  17. t.Skip("skipping integration test, set environment variable ATHENA_CONFIGURATION")
  18. }
  19. athenaConfigBin, err := os.ReadFile(athenaConfigPath)
  20. if err != nil {
  21. t.Fatalf("failed to read config file: %s", err.Error())
  22. }
  23. var athenaConfig AthenaConfiguration
  24. err = json.Unmarshal(athenaConfigBin, &athenaConfig)
  25. if err != nil {
  26. t.Fatalf("failed to unmarshal config from JSON: %s", err.Error())
  27. }
  28. testCases := map[string]struct {
  29. integration *AthenaIntegration
  30. start time.Time
  31. end time.Time
  32. expected bool
  33. }{
  34. // No CUR data is expected within 2 days of now
  35. "too_recent_window": {
  36. integration: &AthenaIntegration{
  37. AthenaQuerier: AthenaQuerier{
  38. AthenaConfiguration: athenaConfig,
  39. },
  40. },
  41. end: time.Now(),
  42. start: time.Now().Add(-timeutil.Day),
  43. expected: true,
  44. },
  45. // CUR data should be available
  46. "last week window": {
  47. integration: &AthenaIntegration{
  48. AthenaQuerier: AthenaQuerier{
  49. AthenaConfiguration: athenaConfig,
  50. },
  51. },
  52. end: time.Now().Add(-7 * timeutil.Day),
  53. start: time.Now().Add(-8 * timeutil.Day),
  54. expected: false,
  55. },
  56. }
  57. for name, testCase := range testCases {
  58. t.Run(name, func(t *testing.T) {
  59. actual, err := testCase.integration.GetCloudCost(testCase.start, testCase.end)
  60. if err != nil {
  61. t.Errorf("Other error during testing %s", err)
  62. } else if actual.IsEmpty() != testCase.expected {
  63. t.Errorf("Incorrect result, actual emptiness: %t, expected: %t", actual.IsEmpty(), testCase.expected)
  64. }
  65. })
  66. }
  67. }
  68. func Test_athenaRowToCloudCost(t *testing.T) {
  69. aqi := AthenaQueryIndexes{
  70. ColumnIndexes: map[string]int{
  71. "ListCostColumn": 0,
  72. "NetCostColumn": 1,
  73. "AmortizedNetCostColumn": 2,
  74. "AmortizedCostColumn": 3,
  75. "IsK8sColumn": 4,
  76. AthenaDateTruncColumn: 5,
  77. "line_item_resource_id": 6,
  78. "bill_payer_account_id": 7,
  79. "line_item_usage_account_id": 8,
  80. "line_item_product_code": 9,
  81. "line_item_usage_type": 10,
  82. "product_region_code": 11,
  83. "line_item_availability_zone": 12,
  84. "resource_tags_user_test": 13,
  85. "resource_tags_aws_test": 14,
  86. },
  87. TagColumns: []string{"resource_tags_user_test"},
  88. AWSTagColumns: []string{"resource_tags_aws_test"},
  89. ListCostColumn: "ListCostColumn",
  90. NetCostColumn: "NetCostColumn",
  91. AmortizedNetCostColumn: "AmortizedNetCostColumn",
  92. AmortizedCostColumn: "AmortizedCostColumn",
  93. IsK8sColumn: "IsK8sColumn",
  94. }
  95. tests := []struct {
  96. name string
  97. row []string
  98. aqi AthenaQueryIndexes
  99. want *opencost.CloudCost
  100. wantErr bool
  101. }{
  102. {
  103. name: "incorrect row length",
  104. row: []string{"not enough elements"},
  105. aqi: aqi,
  106. want: nil,
  107. wantErr: true,
  108. },
  109. {
  110. name: "invalid list cost",
  111. row: []string{"invalid", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  112. aqi: aqi,
  113. want: nil,
  114. wantErr: true,
  115. },
  116. {
  117. name: "invalid net cost",
  118. row: []string{"1", "invalid", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  119. aqi: aqi,
  120. want: nil,
  121. wantErr: true,
  122. },
  123. {
  124. name: "invalid amortized net cost",
  125. row: []string{"1", "2", "invalid", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  126. aqi: aqi,
  127. want: nil,
  128. wantErr: true,
  129. },
  130. {
  131. name: "invalid amortized cost",
  132. row: []string{"1", "2", "3", "invalid", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  133. aqi: aqi,
  134. want: nil,
  135. wantErr: true,
  136. },
  137. {
  138. name: "invalid date",
  139. row: []string{"1", "2", "3", "4", "true", "invalid", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  140. aqi: aqi,
  141. want: nil,
  142. wantErr: true,
  143. },
  144. {
  145. name: "valid kubernetes with labels",
  146. row: []string{"1", "2", "3", "4", "true", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "userTagTestValue", "awsTagTestValue"},
  147. aqi: aqi,
  148. want: &opencost.CloudCost{
  149. Properties: &opencost.CloudCostProperties{
  150. ProviderID: "resourceID",
  151. Provider: "AWS",
  152. AccountID: "usageAccountID",
  153. AccountName: "usageAccountID",
  154. InvoiceEntityID: "payerAccountID",
  155. InvoiceEntityName: "payerAccountID",
  156. RegionID: "regionCode",
  157. AvailabilityZone: "availabilityZone",
  158. Service: "productCode",
  159. Category: opencost.OtherCategory,
  160. Labels: opencost.CloudCostLabels{
  161. "test": "userTagTestValue",
  162. "aws_test": "awsTagTestValue",
  163. },
  164. },
  165. Window: opencost.NewClosedWindow(
  166. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  167. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  168. ),
  169. ListCost: opencost.CostMetric{
  170. Cost: 1,
  171. KubernetesPercent: 1,
  172. },
  173. NetCost: opencost.CostMetric{
  174. Cost: 2,
  175. KubernetesPercent: 1,
  176. },
  177. AmortizedNetCost: opencost.CostMetric{
  178. Cost: 3,
  179. KubernetesPercent: 1,
  180. },
  181. InvoicedCost: opencost.CostMetric{
  182. Cost: 2,
  183. KubernetesPercent: 1,
  184. },
  185. AmortizedCost: opencost.CostMetric{
  186. Cost: 4,
  187. KubernetesPercent: 1,
  188. },
  189. },
  190. wantErr: false,
  191. },
  192. {
  193. name: "valid non-kubernetes, no labels",
  194. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID", "payerAccountID", "usageAccountID", "productCode", "usageType", "regionCode", "availabilityZone", "", ""},
  195. aqi: aqi,
  196. want: &opencost.CloudCost{
  197. Properties: &opencost.CloudCostProperties{
  198. ProviderID: "resourceID",
  199. Provider: "AWS",
  200. AccountID: "usageAccountID",
  201. AccountName: "usageAccountID",
  202. InvoiceEntityID: "payerAccountID",
  203. InvoiceEntityName: "payerAccountID",
  204. RegionID: "regionCode",
  205. AvailabilityZone: "availabilityZone",
  206. Service: "productCode",
  207. Category: opencost.OtherCategory,
  208. Labels: opencost.CloudCostLabels{},
  209. },
  210. Window: opencost.NewClosedWindow(
  211. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  212. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  213. ),
  214. ListCost: opencost.CostMetric{
  215. Cost: 1,
  216. KubernetesPercent: 0,
  217. },
  218. NetCost: opencost.CostMetric{
  219. Cost: 2,
  220. KubernetesPercent: 0,
  221. },
  222. AmortizedNetCost: opencost.CostMetric{
  223. Cost: 3,
  224. KubernetesPercent: 0,
  225. },
  226. InvoicedCost: opencost.CostMetric{
  227. Cost: 2,
  228. KubernetesPercent: 0,
  229. },
  230. AmortizedCost: opencost.CostMetric{
  231. Cost: 4,
  232. KubernetesPercent: 0,
  233. },
  234. },
  235. wantErr: false,
  236. },
  237. {
  238. name: "valid load balancer product code",
  239. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "resourceID/lbID", "payerAccountID", "usageAccountID", "AWSELB", "usageType", "regionCode", "availabilityZone", "", ""},
  240. aqi: aqi,
  241. want: &opencost.CloudCost{
  242. Properties: &opencost.CloudCostProperties{
  243. ProviderID: "lbID",
  244. Provider: "AWS",
  245. AccountID: "usageAccountID",
  246. AccountName: "usageAccountID",
  247. InvoiceEntityID: "payerAccountID",
  248. InvoiceEntityName: "payerAccountID",
  249. RegionID: "regionCode",
  250. AvailabilityZone: "availabilityZone",
  251. Service: "AWSELB",
  252. Category: opencost.NetworkCategory,
  253. Labels: opencost.CloudCostLabels{},
  254. },
  255. Window: opencost.NewClosedWindow(
  256. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  257. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  258. ),
  259. ListCost: opencost.CostMetric{
  260. Cost: 1,
  261. KubernetesPercent: 0,
  262. },
  263. NetCost: opencost.CostMetric{
  264. Cost: 2,
  265. KubernetesPercent: 0,
  266. },
  267. AmortizedNetCost: opencost.CostMetric{
  268. Cost: 3,
  269. KubernetesPercent: 0,
  270. },
  271. InvoicedCost: opencost.CostMetric{
  272. Cost: 2,
  273. KubernetesPercent: 0,
  274. },
  275. AmortizedCost: opencost.CostMetric{
  276. Cost: 4,
  277. KubernetesPercent: 0,
  278. },
  279. },
  280. wantErr: false,
  281. },
  282. {
  283. name: "valid non-kubernetes, Fargate CPU",
  284. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "CPU", "regionCode", "availabilityZone", "", ""},
  285. aqi: aqi,
  286. want: &opencost.CloudCost{
  287. Properties: &opencost.CloudCostProperties{
  288. ProviderID: "123:pod/resource/CPU",
  289. Provider: "AWS",
  290. AccountID: "usageAccountID",
  291. AccountName: "usageAccountID",
  292. InvoiceEntityID: "payerAccountID",
  293. InvoiceEntityName: "payerAccountID",
  294. RegionID: "regionCode",
  295. AvailabilityZone: "availabilityZone",
  296. Service: "AmazonEKS",
  297. Category: opencost.ComputeCategory,
  298. Labels: opencost.CloudCostLabels{},
  299. },
  300. Window: opencost.NewClosedWindow(
  301. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  302. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  303. ),
  304. ListCost: opencost.CostMetric{
  305. Cost: 1,
  306. KubernetesPercent: 0,
  307. },
  308. NetCost: opencost.CostMetric{
  309. Cost: 2,
  310. KubernetesPercent: 0,
  311. },
  312. AmortizedNetCost: opencost.CostMetric{
  313. Cost: 3,
  314. KubernetesPercent: 0,
  315. },
  316. InvoicedCost: opencost.CostMetric{
  317. Cost: 2,
  318. KubernetesPercent: 0,
  319. },
  320. AmortizedCost: opencost.CostMetric{
  321. Cost: 4,
  322. KubernetesPercent: 0,
  323. },
  324. },
  325. wantErr: false,
  326. },
  327. {
  328. name: "valid non-kubernetes, Fargate RAM",
  329. row: []string{"1", "2", "3", "4", "false", "2024-09-01 00:00:00.000", "123:pod/resource", "payerAccountID", "usageAccountID", "AmazonEKS", "GB", "regionCode", "availabilityZone", "", ""},
  330. aqi: aqi,
  331. want: &opencost.CloudCost{
  332. Properties: &opencost.CloudCostProperties{
  333. ProviderID: "123:pod/resource/RAM",
  334. Provider: "AWS",
  335. AccountID: "usageAccountID",
  336. AccountName: "usageAccountID",
  337. InvoiceEntityID: "payerAccountID",
  338. InvoiceEntityName: "payerAccountID",
  339. RegionID: "regionCode",
  340. AvailabilityZone: "availabilityZone",
  341. Service: "AmazonEKS",
  342. Category: opencost.ComputeCategory,
  343. Labels: opencost.CloudCostLabels{},
  344. },
  345. Window: opencost.NewClosedWindow(
  346. time.Date(2024, 9, 1, 0, 0, 0, 0, time.UTC),
  347. time.Date(2024, 9, 2, 0, 0, 0, 0, time.UTC),
  348. ),
  349. ListCost: opencost.CostMetric{
  350. Cost: 1,
  351. KubernetesPercent: 0,
  352. },
  353. NetCost: opencost.CostMetric{
  354. Cost: 2,
  355. KubernetesPercent: 0,
  356. },
  357. AmortizedNetCost: opencost.CostMetric{
  358. Cost: 3,
  359. KubernetesPercent: 0,
  360. },
  361. InvoicedCost: opencost.CostMetric{
  362. Cost: 2,
  363. KubernetesPercent: 0,
  364. },
  365. AmortizedCost: opencost.CostMetric{
  366. Cost: 4,
  367. KubernetesPercent: 0,
  368. },
  369. },
  370. wantErr: false,
  371. },
  372. }
  373. for _, tt := range tests {
  374. t.Run(tt.name, func(t *testing.T) {
  375. row := stringsToRow(tt.row)
  376. got, err := athenaRowToCloudCost(row, tt.aqi)
  377. if (err != nil) != tt.wantErr {
  378. t.Errorf("RowToCloudCost() error = %v, wantErr %v", err, tt.wantErr)
  379. return
  380. }
  381. if !reflect.DeepEqual(got, tt.want) {
  382. t.Errorf("RowToCloudCost() got = %v, want %v", got, tt.want)
  383. }
  384. })
  385. }
  386. }
  387. func stringsToRow(strings []string) types.Row {
  388. var data []types.Datum
  389. for _, str := range strings {
  390. varChar := str
  391. data = append(data, types.Datum{VarCharValue: &varChar})
  392. }
  393. return types.Row{Data: data}
  394. }
  395. // mockAthenaQuerier is a mock that overrides HasBillingPeriodPartitions for testing
  396. type mockAthenaQuerier struct {
  397. AthenaQuerier
  398. hasBillingPeriodPartitions bool
  399. }
  400. func (m *mockAthenaQuerier) HasBillingPeriodPartitions() (bool, error) {
  401. return m.hasBillingPeriodPartitions, nil
  402. }
  403. // mockAthenaIntegration is a mock that uses mockAthenaQuerier
  404. type mockAthenaIntegration struct {
  405. *mockAthenaQuerier
  406. }
  407. func (m *mockAthenaIntegration) GetPartitionWhere(start, end time.Time) string {
  408. // The partition logic using our mock's HasBillingPeriodPartitions result
  409. month := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC)
  410. endMonth := time.Date(end.Year(), end.Month(), 1, 0, 0, 0, 0, time.UTC)
  411. var disjuncts []string
  412. // Using our mock's result for billing period partitions
  413. useBillingPeriodPartitions := false
  414. if m.mockAthenaQuerier.AthenaConfiguration.CURVersion != "1.0" {
  415. useBillingPeriodPartitions = m.mockAthenaQuerier.hasBillingPeriodPartitions
  416. }
  417. for !month.After(endMonth) {
  418. if m.mockAthenaQuerier.AthenaConfiguration.CURVersion == "1.0" {
  419. // CUR 1.0 uses year and month columns for partitioning
  420. disjuncts = append(disjuncts, fmt.Sprintf("(year = '%d' AND month = '%d')", month.Year(), month.Month()))
  421. } else if useBillingPeriodPartitions {
  422. // CUR 2.0 with billing_period partitions
  423. disjuncts = append(disjuncts, fmt.Sprintf("(billing_period = '%d-%02d')", month.Year(), month.Month()))
  424. } else {
  425. // CUR 2.0 fallback - use date_format functions
  426. disjuncts = append(disjuncts, fmt.Sprintf("(date_format(line_item_usage_start_date, '%%Y') = '%d' AND date_format(line_item_usage_start_date, '%%m') = '%02d')",
  427. month.Year(), month.Month()))
  428. }
  429. month = month.AddDate(0, 1, 0)
  430. }
  431. return fmt.Sprintf("(%s)", strings.Join(disjuncts, " OR "))
  432. }
  433. func TestAthenaIntegration_GetPartitionWhere(t *testing.T) {
  434. testCases := map[string]struct {
  435. integration interface {
  436. GetPartitionWhere(time.Time, time.Time) string
  437. }
  438. start time.Time
  439. end time.Time
  440. expected string
  441. }{
  442. "CUR 1.0 single month": {
  443. integration: &AthenaIntegration{
  444. AthenaQuerier: AthenaQuerier{
  445. AthenaConfiguration: AthenaConfiguration{
  446. Bucket: "bucket",
  447. Region: "region",
  448. Database: "database",
  449. Table: "table",
  450. Workgroup: "workgroup",
  451. Account: "account",
  452. Authorizer: &ServiceAccount{},
  453. CURVersion: "1.0",
  454. },
  455. },
  456. },
  457. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  458. end: time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
  459. expected: "((year = '2024' AND month = '1'))",
  460. },
  461. "CUR 2.0 single month": {
  462. integration: &mockAthenaIntegration{
  463. mockAthenaQuerier: &mockAthenaQuerier{
  464. AthenaQuerier: AthenaQuerier{
  465. AthenaConfiguration: AthenaConfiguration{
  466. Bucket: "bucket",
  467. Region: "region",
  468. Database: "database",
  469. Table: "table",
  470. Workgroup: "workgroup",
  471. Account: "account",
  472. Authorizer: &ServiceAccount{},
  473. CURVersion: "2.0",
  474. },
  475. },
  476. hasBillingPeriodPartitions: true,
  477. },
  478. },
  479. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  480. end: time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
  481. expected: "((billing_period = '2024-01'))",
  482. },
  483. "CUR 1.0 multiple months": {
  484. integration: &AthenaIntegration{
  485. AthenaQuerier: AthenaQuerier{
  486. AthenaConfiguration: AthenaConfiguration{
  487. Bucket: "bucket",
  488. Region: "region",
  489. Database: "database",
  490. Table: "table",
  491. Workgroup: "workgroup",
  492. Account: "account",
  493. Authorizer: &ServiceAccount{},
  494. CURVersion: "1.0",
  495. },
  496. },
  497. },
  498. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  499. end: time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
  500. expected: "((year = '2024' AND month = '1') OR (year = '2024' AND month = '2') OR (year = '2024' AND month = '3'))",
  501. },
  502. "CUR 2.0 multiple months": {
  503. integration: &mockAthenaIntegration{
  504. mockAthenaQuerier: &mockAthenaQuerier{
  505. AthenaQuerier: AthenaQuerier{
  506. AthenaConfiguration: AthenaConfiguration{
  507. Bucket: "bucket",
  508. Region: "region",
  509. Database: "database",
  510. Table: "table",
  511. Workgroup: "workgroup",
  512. Account: "account",
  513. Authorizer: &ServiceAccount{},
  514. CURVersion: "2.0",
  515. },
  516. },
  517. hasBillingPeriodPartitions: true,
  518. },
  519. },
  520. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  521. end: time.Date(2024, 3, 10, 0, 0, 0, 0, time.UTC),
  522. expected: "((billing_period = '2024-01') OR (billing_period = '2024-02') OR (billing_period = '2024-03'))",
  523. },
  524. "CUR 2.0 across year boundary": {
  525. integration: &mockAthenaIntegration{
  526. mockAthenaQuerier: &mockAthenaQuerier{
  527. AthenaQuerier: AthenaQuerier{
  528. AthenaConfiguration: AthenaConfiguration{
  529. Bucket: "bucket",
  530. Region: "region",
  531. Database: "database",
  532. Table: "table",
  533. Workgroup: "workgroup",
  534. Account: "account",
  535. Authorizer: &ServiceAccount{},
  536. CURVersion: "2.0",
  537. },
  538. },
  539. hasBillingPeriodPartitions: true,
  540. },
  541. },
  542. start: time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
  543. end: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
  544. expected: "((billing_period = '2023-12') OR (billing_period = '2024-01') OR (billing_period = '2024-02'))",
  545. },
  546. "CUR 1.0 across year boundary": {
  547. integration: &AthenaIntegration{
  548. AthenaQuerier: AthenaQuerier{
  549. AthenaConfiguration: AthenaConfiguration{
  550. Bucket: "bucket",
  551. Region: "region",
  552. Database: "database",
  553. Table: "table",
  554. Workgroup: "workgroup",
  555. Account: "account",
  556. Authorizer: &ServiceAccount{},
  557. CURVersion: "1.0",
  558. },
  559. },
  560. },
  561. start: time.Date(2023, 12, 15, 0, 0, 0, 0, time.UTC),
  562. end: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC),
  563. expected: "((year = '2023' AND month = '12') OR (year = '2024' AND month = '1') OR (year = '2024' AND month = '2'))",
  564. },
  565. "Default CUR version (empty string defaults to 2.0)": {
  566. integration: &mockAthenaIntegration{
  567. mockAthenaQuerier: &mockAthenaQuerier{
  568. AthenaQuerier: AthenaQuerier{
  569. AthenaConfiguration: AthenaConfiguration{
  570. Bucket: "bucket",
  571. Region: "region",
  572. Database: "database",
  573. Table: "table",
  574. Workgroup: "workgroup",
  575. Account: "account",
  576. Authorizer: &ServiceAccount{},
  577. CURVersion: "",
  578. },
  579. },
  580. hasBillingPeriodPartitions: true,
  581. },
  582. },
  583. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  584. end: time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
  585. expected: "((billing_period = '2024-01'))",
  586. },
  587. "CUR 2.0 fallback when no billing_period partitions": {
  588. integration: &mockAthenaIntegration{
  589. mockAthenaQuerier: &mockAthenaQuerier{
  590. AthenaQuerier: AthenaQuerier{
  591. AthenaConfiguration: AthenaConfiguration{
  592. Bucket: "bucket",
  593. Region: "region",
  594. Database: "database",
  595. Table: "table",
  596. Workgroup: "workgroup",
  597. Account: "account",
  598. Authorizer: &ServiceAccount{},
  599. CURVersion: "2.0",
  600. },
  601. },
  602. hasBillingPeriodPartitions: false, // No billing_period partitions
  603. },
  604. },
  605. start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
  606. end: time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC),
  607. expected: "((date_format(line_item_usage_start_date, '%Y') = '2024' AND date_format(line_item_usage_start_date, '%m') = '01'))",
  608. },
  609. }
  610. for name, testCase := range testCases {
  611. t.Run(name, func(t *testing.T) {
  612. actual := testCase.integration.GetPartitionWhere(testCase.start, testCase.end)
  613. if actual != testCase.expected {
  614. t.Errorf("GetPartitionWhere() mismatch:\nActual: %s\nExpected: %s", actual, testCase.expected)
  615. }
  616. })
  617. }
  618. }