Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions test/integration/api/schema/allocation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package schema

import (
"fmt"
"testing"

"github.com/opencost/opencost-integration-tests/pkg/api"
)

// Description - Assert that /allocation responses retain their expected public
// JSON fields, nested objects, and important field types.
//
// Implementation Details
// - Raw JSON is inspected instead of typed API structs so removed or renamed
// fields are detected.
// - Additional response fields are allowed to preserve compatible API extensions.
// - The test is skipped when the endpoint returns no allocation items.
//
// Passing Criteria
// - The endpoint returns a successful response.
// - Every allocation item contains the required fields.
// - Required window, properties, name, and totalCost values have expected types.

var allocationRequiredFields = []string{
"name",
"cpuCost",
"ramCost",
"gpuCost",
"pvCost",
"networkCost",
"loadBalancerCost",
"sharedCost",
"externalCost",
"totalCost",
"minutes",
"window",
"properties",
}

func TestAllocationResponseSchemaStability(t *testing.T) {
apiClient := api.NewAPI()

resp := fetchRawEndpoint(t, apiClient, "/allocation", api.AutocompleteRequest{
Window: "1h",
})

requireSuccessfulResponse(t, "/allocation response", resp)

data := requireArray(t, "/allocation data", resp["data"])
if len(data) == 0 {
t.Skip("/allocation returned no data sets")
}

validatedItems := 0

for setIndex, rawSet := range data {
setContext := fmt.Sprintf("/allocation data[%d]", setIndex)
set := requireMap(t, setContext, rawSet)

for itemName, rawItem := range set {
validatedItems++

itemContext := fmt.Sprintf("/allocation item %q", itemName)
item := requireMap(t, itemContext, rawItem)

requireFields(t, itemContext, item, allocationRequiredFields)
requireString(t, itemContext+" name", item["name"])
requireNumber(t, itemContext+" totalCost", item["totalCost"])

windowContext := itemContext + " window"
window := requireMap(t, windowContext, item["window"])
requireFields(t, windowContext, window, windowRequiredFields)
requireString(t, windowContext+" start", window["start"])
requireString(t, windowContext+" end", window["end"])

requireMap(t, itemContext+" properties", item["properties"])
}
}

if validatedItems == 0 {
t.Skip("/allocation returned no allocation items")
}
}
91 changes: 91 additions & 0 deletions test/integration/api/schema/assets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package schema

import (
"fmt"
"testing"

"github.com/opencost/opencost-integration-tests/pkg/api"
)

// Description - Assert that /assets responses retain their expected public
// JSON fields, nested objects, and important field types.
//
// Implementation Details
// - Fields shared by every asset are validated separately from fields that are
// specific to Node assets.
// - Additional response fields and non-Node asset types are allowed.
// - The test is skipped when the endpoint returns no assets.
//
// Passing Criteria
// - The endpoint returns a successful response.
// - Every asset contains the common required fields.
// - Node assets contain their Node-specific fields and properties.

var assetCommonRequiredFields = []string{
"type",
"properties",
"window",
"start",
"end",
"minutes",
"totalCost",
}

var assetCommonPropertiesRequiredFields = []string{
"category",
"provider",
"service",
}

var assetNodeRequiredFields = []string{
"cpuCost",
"ramCost",
"gpuCost",
"nodeType",
"cpuCores",
"ramBytes",
}

var assetNodePropertiesRequiredFields = []string{
"name",
"providerID",
}

func TestAssetsResponseSchemaStability(t *testing.T) {
apiClient := api.NewAPI()

resp := fetchRawEndpoint(t, apiClient, "/assets", api.AutocompleteRequest{
Window: "1h",
})

requireSuccessfulResponse(t, "/assets response", resp)

data := requireMap(t, "/assets data", resp["data"])
if len(data) == 0 {
t.Skip("/assets returned no asset items")
}

for assetName, rawAsset := range data {
itemContext := fmt.Sprintf("/assets item %q", assetName)
asset := requireMap(t, itemContext, rawAsset)

requireFields(t, itemContext, asset, assetCommonRequiredFields)
requireNumber(t, itemContext+" totalCost", asset["totalCost"])

windowContext := itemContext + " window"
window := requireMap(t, windowContext, asset["window"])
requireFields(t, windowContext, window, windowRequiredFields)
requireString(t, windowContext+" start", window["start"])
requireString(t, windowContext+" end", window["end"])

propertiesContext := itemContext + " properties"
properties := requireMap(t, propertiesContext, asset["properties"])
requireFields(t, propertiesContext, properties, assetCommonPropertiesRequiredFields)

assetType := requireString(t, itemContext+" type", asset["type"])
if assetType == "Node" {
requireFields(t, itemContext, asset, assetNodeRequiredFields)
requireFields(t, propertiesContext, properties, assetNodePropertiesRequiredFields)
}
}
}
116 changes: 116 additions & 0 deletions test/integration/api/schema/cloudcost_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package schema

import (
"fmt"
"testing"

"github.com/opencost/opencost-integration-tests/pkg/api"
)

// Description - Assert that /cloudCost responses retain their expected public
// JSON fields, nested objects, and important field types.
//
// Implementation Details
// - Each CloudCost item, properties object, window, and nested cost object is
// validated using raw JSON.
// - Empty sets are ignored while other returned sets continue to be validated.
// - The test is skipped when no CloudCost items are returned.
//
// Passing Criteria
// - The endpoint returns a successful response.
// - Every CloudCost item contains the required fields and properties.
// - Each supported nested cost object contains numeric cost and
// kubernetesPercent fields.

var cloudCostPropertiesRequiredFields = []string{
"provider",
"accountID",
"accountName",
"invoiceEntityID",
"invoiceEntityName",
"service",
"category",
}

var cloudCostCostObjectRequiredFields = []string{
"cost",
"kubernetesPercent",
}

var cloudCostFields = []string{
"netCost",
"amortizedCost",
"amortizedNetCost",
"invoicedCost",
"listCost",
}
Comment thread
Noah-Philip marked this conversation as resolved.

var cloudCostItemRequiredFields = append(
[]string{
"properties",
"window",
},
cloudCostFields...,
)

func TestCloudCostResponseSchemaStability(t *testing.T) {
apiClient := api.NewAPI()

resp := fetchRawEndpoint(t, apiClient, "/cloudCost", api.AutocompleteRequest{
Window: "1d",
})

requireSuccessfulResponse(t, "/cloudCost response", resp)

data := requireMap(t, "/cloudCost data", resp["data"])
requireFields(t, "/cloudCost data", data, []string{"sets"})

sets := requireArray(t, "/cloudCost data.sets", data["sets"])
if len(sets) == 0 {
t.Skip("/cloudCost returned no sets")
}

validatedItems := 0

for setIndex, rawSet := range sets {
setContext := fmt.Sprintf("/cloudCost set[%d]", setIndex)
set := requireMap(t, setContext, rawSet)

requireFields(t, setContext, set, []string{"cloudCosts"})

cloudCostsContext := setContext + ".cloudCosts"
cloudCosts := requireMap(t, cloudCostsContext, set["cloudCosts"])

for itemName, rawItem := range cloudCosts {
validatedItems++

itemContext := fmt.Sprintf("/cloudCost item %q", itemName)
item := requireMap(t, itemContext, rawItem)

requireFields(t, itemContext, item, cloudCostItemRequiredFields)

windowContext := itemContext + " window"
window := requireMap(t, windowContext, item["window"])
requireFields(t, windowContext, window, windowRequiredFields)
requireString(t, windowContext+" start", window["start"])
requireString(t, windowContext+" end", window["end"])

propertiesContext := itemContext + " properties"
properties := requireMap(t, propertiesContext, item["properties"])
requireFields(t, propertiesContext, properties, cloudCostPropertiesRequiredFields)

for _, costField := range cloudCostFields {
costContext := itemContext + " " + costField
costObject := requireMap(t, costContext, item[costField])

requireFields(t, costContext, costObject, cloudCostCostObjectRequiredFields)
requireNumber(t, costContext+" cost", costObject["cost"])
requireNumber(t, costContext+" kubernetesPercent", costObject["kubernetesPercent"])
}
}
}

if validatedItems == 0 {
t.Skip("/cloudCost returned no cloud cost items")
}
}
Loading
Loading