usageapiintegration_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package oracle
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "testing"
  8. "time"
  9. "github.com/opencost/opencost/core/pkg/util/timeutil"
  10. "github.com/opencost/opencost/pkg/cloud"
  11. "github.com/oracle/oci-go-sdk/v65/common"
  12. "github.com/oracle/oci-go-sdk/v65/usageapi"
  13. )
  14. func TestParseAttributedCost(t *testing.T) {
  15. strPtr := func(s string) *string { return &s }
  16. cases := map[string]struct {
  17. input *string
  18. want float64
  19. wantErr bool
  20. }{
  21. "nil": {nil, 0, false},
  22. "empty": {strPtr(""), 0, false},
  23. "zero": {strPtr("0"), 0, false},
  24. "valid": {strPtr("1.23"), 1.23, false},
  25. "negative": {strPtr("-0.5"), -0.5, false},
  26. "unparsable": {strPtr("abc"), 0, true},
  27. }
  28. for name, c := range cases {
  29. t.Run(name, func(t *testing.T) {
  30. got, err := parseAttributedCost(c.input)
  31. if (err != nil) != c.wantErr {
  32. t.Fatalf("err = %v, wantErr = %v", err, c.wantErr)
  33. }
  34. if got != c.want {
  35. t.Errorf("got %v, want %v", got, c.want)
  36. }
  37. })
  38. }
  39. }
  40. func TestUsageAPIIntegration_GetCloudCost(t *testing.T) {
  41. usageApiConfigPath := os.Getenv("USAGEAPI_CONFIGURATION")
  42. if usageApiConfigPath == "" {
  43. t.Skip("skipping integration test, set environment variable USAGEAPI_CONFIGURATION")
  44. }
  45. usageApiConfigBin, err := os.ReadFile(usageApiConfigPath)
  46. if err != nil {
  47. t.Fatalf("failed to read config file: %s", err.Error())
  48. }
  49. var usageApiConfig UsageApiConfiguration
  50. err = json.Unmarshal(usageApiConfigBin, &usageApiConfig)
  51. if err != nil {
  52. t.Fatalf("failed to unmarshal config from JSON: %s", err.Error())
  53. }
  54. testCases := map[string]struct {
  55. integration *UsageApiIntegration
  56. start time.Time
  57. end time.Time
  58. expected bool
  59. }{
  60. // No CUR data is expected within 2 days of now
  61. "too_recent_window": {
  62. integration: &UsageApiIntegration{
  63. UsageApiConfiguration: usageApiConfig,
  64. },
  65. end: time.Now(),
  66. start: time.Now().Add(-timeutil.Day),
  67. expected: true,
  68. },
  69. // CUR data should be available
  70. "last week window": {
  71. integration: &UsageApiIntegration{
  72. UsageApiConfiguration: usageApiConfig,
  73. },
  74. end: time.Now().Add(-7 * timeutil.Day),
  75. start: time.Now().Add(-8 * timeutil.Day),
  76. expected: false,
  77. },
  78. }
  79. for name, testCase := range testCases {
  80. t.Run(name, func(t *testing.T) {
  81. actual, err := testCase.integration.GetCloudCost(testCase.start, testCase.end)
  82. if err != nil {
  83. t.Errorf("Other error during testing %s", err)
  84. } else if actual.IsEmpty() != testCase.expected {
  85. t.Errorf("Incorrect result, actual emptiness: %t, expected: %t", actual.IsEmpty(), testCase.expected)
  86. }
  87. })
  88. }
  89. }
  90. type fakeUsageAPIClient struct {
  91. responses []usageapi.RequestSummarizedUsagesResponse
  92. requests []usageapi.RequestSummarizedUsagesRequest
  93. }
  94. func (f *fakeUsageAPIClient) RequestSummarizedUsages(_ context.Context, request usageapi.RequestSummarizedUsagesRequest) (usageapi.RequestSummarizedUsagesResponse, error) {
  95. f.requests = append(f.requests, request)
  96. if len(f.responses) == 0 {
  97. return usageapi.RequestSummarizedUsagesResponse{}, fmt.Errorf("unexpected request")
  98. }
  99. response := f.responses[0]
  100. f.responses = f.responses[1:]
  101. return response, nil
  102. }
  103. func TestUsageAPIIntegrationGetCloudCostLoadsEachDailyItem(t *testing.T) {
  104. start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
  105. client := &fakeUsageAPIClient{
  106. responses: []usageapi.RequestSummarizedUsagesResponse{
  107. {
  108. UsageAggregation: usageapi.UsageAggregation{
  109. Items: []usageapi.UsageSummary{
  110. testUsageSummary(start, "resource-1", 1),
  111. testUsageSummary(start.AddDate(0, 0, 1), "resource-2", 2),
  112. testUsageSummary(start.AddDate(0, 0, 2), "resource-3", 3),
  113. },
  114. },
  115. },
  116. },
  117. }
  118. integration := &UsageApiIntegration{
  119. UsageApiConfiguration: UsageApiConfiguration{
  120. TenancyID: "tenancy-id",
  121. Region: "region",
  122. },
  123. }
  124. ccsr, err := integration.getCloudCost(context.Background(), client, start, start.AddDate(0, 0, 3))
  125. if err != nil {
  126. t.Fatalf("getCloudCost() error = %v", err)
  127. }
  128. if len(ccsr.CloudCostSets) != 3 {
  129. t.Fatalf("expected 3 daily CloudCostSets, got %d", len(ccsr.CloudCostSets))
  130. }
  131. for i, ccs := range ccsr.CloudCostSets {
  132. if len(ccs.CloudCosts) != 1 {
  133. t.Fatalf("day %d: expected 1 CloudCost, got %d", i+1, len(ccs.CloudCosts))
  134. }
  135. for _, cloudCost := range ccs.CloudCosts {
  136. wantCost := float64(i + 1)
  137. if cloudCost.NetCost.Cost != wantCost {
  138. t.Errorf("day %d: NetCost = %v, want %v", i+1, cloudCost.NetCost.Cost, wantCost)
  139. }
  140. }
  141. }
  142. }
  143. func TestUsageAPIIntegrationGetCloudCostFollowsPagination(t *testing.T) {
  144. start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
  145. firstPageItems := make([]usageapi.UsageSummary, 500)
  146. for i := range firstPageItems {
  147. firstPageItems[i] = testUsageSummary(start, fmt.Sprintf("resource-%d", i), 1)
  148. }
  149. client := &fakeUsageAPIClient{
  150. responses: []usageapi.RequestSummarizedUsagesResponse{
  151. {
  152. UsageAggregation: usageapi.UsageAggregation{Items: firstPageItems},
  153. OpcNextPage: common.String("next-page"),
  154. },
  155. {
  156. UsageAggregation: usageapi.UsageAggregation{
  157. Items: []usageapi.UsageSummary{testUsageSummary(start, "resource-500", 1)},
  158. },
  159. },
  160. },
  161. }
  162. integration := &UsageApiIntegration{
  163. UsageApiConfiguration: UsageApiConfiguration{
  164. TenancyID: "tenancy-id",
  165. Region: "region",
  166. },
  167. }
  168. ccsr, err := integration.getCloudCost(context.Background(), client, start, start.AddDate(0, 0, 1))
  169. if err != nil {
  170. t.Fatalf("getCloudCost() error = %v", err)
  171. }
  172. if len(client.requests) != 2 {
  173. t.Fatalf("expected 2 OCI requests, got %d", len(client.requests))
  174. }
  175. if client.requests[0].Page != nil {
  176. t.Errorf("first request page = %q, want nil", *client.requests[0].Page)
  177. }
  178. if client.requests[1].Page == nil || *client.requests[1].Page != "next-page" {
  179. t.Errorf("second request page = %v, want next-page", client.requests[1].Page)
  180. }
  181. if client.requests[1].Limit == nil || *client.requests[1].Limit != 500 {
  182. t.Errorf("second request limit = %v, want 500", client.requests[1].Limit)
  183. }
  184. if got := len(ccsr.CloudCostSets[0].CloudCosts); got != 501 {
  185. t.Errorf("expected 501 CloudCosts from both pages, got %d", got)
  186. }
  187. }
  188. func TestUsageAPIIntegrationGetCloudCostSkipsUsageSummaryWithoutTimeWindow(t *testing.T) {
  189. start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
  190. item := testUsageSummary(start, "resource-1", 1)
  191. item.TimeUsageEnded = nil
  192. client := &fakeUsageAPIClient{
  193. responses: []usageapi.RequestSummarizedUsagesResponse{
  194. {
  195. UsageAggregation: usageapi.UsageAggregation{Items: []usageapi.UsageSummary{item}},
  196. },
  197. },
  198. }
  199. integration := &UsageApiIntegration{
  200. UsageApiConfiguration: UsageApiConfiguration{
  201. TenancyID: "tenancy-id",
  202. Region: "region",
  203. },
  204. }
  205. ccsr, err := integration.getCloudCost(context.Background(), client, start, start.AddDate(0, 0, 1))
  206. if err != nil {
  207. t.Fatalf("getCloudCost() error = %v", err)
  208. }
  209. if !ccsr.IsEmpty() {
  210. t.Error("expected usage summary without a time window to be skipped")
  211. }
  212. }
  213. func TestUsageAPIIntegrationGetCloudCostRejectsRepeatedPageToken(t *testing.T) {
  214. start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
  215. client := &fakeUsageAPIClient{
  216. responses: []usageapi.RequestSummarizedUsagesResponse{
  217. {
  218. UsageAggregation: usageapi.UsageAggregation{Items: []usageapi.UsageSummary{testUsageSummary(start, "resource-1", 1)}},
  219. OpcNextPage: common.String("page-token"),
  220. },
  221. {
  222. UsageAggregation: usageapi.UsageAggregation{Items: []usageapi.UsageSummary{testUsageSummary(start, "resource-2", 1)}},
  223. OpcNextPage: common.String("page-token"),
  224. },
  225. },
  226. }
  227. integration := &UsageApiIntegration{
  228. UsageApiConfiguration: UsageApiConfiguration{
  229. TenancyID: "tenancy-id",
  230. Region: "region",
  231. },
  232. }
  233. _, err := integration.getCloudCost(context.Background(), client, start, start.AddDate(0, 0, 1))
  234. if err == nil {
  235. t.Fatal("expected error for repeated OCI page token")
  236. }
  237. if got := len(client.requests); got != 2 {
  238. t.Errorf("expected 2 OCI requests before repeated token error, got %d", got)
  239. }
  240. if integration.ConnectionStatus != cloud.FailedConnection {
  241. t.Errorf("ConnectionStatus = %s, want %s", integration.ConnectionStatus, cloud.FailedConnection)
  242. }
  243. }
  244. func TestUsageSummaryToCloudCostUsesOCIUsageWindow(t *testing.T) {
  245. start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
  246. end := start.Add(12 * time.Hour)
  247. item := testUsageSummary(start, "resource-1", 1)
  248. item.TimeUsageEnded = &common.SDKTime{Time: end}
  249. integration := &UsageApiIntegration{
  250. UsageApiConfiguration: UsageApiConfiguration{
  251. TenancyID: "tenancy-id",
  252. Region: "region",
  253. },
  254. }
  255. cloudCost, err := integration.usageSummaryToCloudCost(item)
  256. if err != nil {
  257. t.Fatalf("usageSummaryToCloudCost() error = %v", err)
  258. }
  259. if got := cloudCost.Window.End(); !got.Equal(end) {
  260. t.Errorf("CloudCost window end = %s, want %s", got, end)
  261. }
  262. }
  263. func testUsageSummary(start time.Time, resourceID string, computedAmount float32) usageapi.UsageSummary {
  264. return usageapi.UsageSummary{
  265. TimeUsageStarted: &common.SDKTime{Time: start},
  266. TimeUsageEnded: &common.SDKTime{Time: start.AddDate(0, 0, 1)},
  267. ResourceId: common.String(resourceID),
  268. Service: common.String("Compute"),
  269. ComputedAmount: common.Float32(computedAmount),
  270. AttributedCost: common.String(fmt.Sprintf("%v", computedAmount)),
  271. }
  272. }