Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions demo/pkg/subgraphs/employees/subgraph/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,6 @@ type WorkSetup @shareable {

input FindEmployeeCriteria @oneOf {
id: Int
department: Department
title: String
department: Department @cost(weight: 17)
title: String @cost(weight: -3)
}
2 changes: 1 addition & 1 deletion router-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ require (
github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e
github.com/wundergraph/cosmo/router v0.0.0-20260319123623-f186a0f724f6
github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267.0.20260331093123-e622a6e2da5a
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
Expand Down
4 changes: 2 additions & 2 deletions router-tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ github.com/wundergraph/astjson v1.1.0 h1:xORDosrZ87zQFJwNGe/HIHXqzpdHOFmqWgykCLV
github.com/wundergraph/astjson v1.1.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw=
github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc=
github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267 h1:qMkYR0oq0Cw61aDZs9VsCCVwNVSxRxT13ytz6WqCwJg=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267/go.mod h1:HjTAO/cuICpu31IfHY9qmSPygx6Gza7Wt9hTSReTI+A=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267.0.20260331093123-e622a6e2da5a h1:cDY603GUYLOKcXpLqkmGQkBflUGO+ib1tGHwlKfbQG0=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267.0.20260331093123-e622a6e2da5a/go.mod h1:HjTAO/cuICpu31IfHY9qmSPygx6Gza7Wt9hTSReTI+A=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
Expand Down
152 changes: 112 additions & 40 deletions router-tests/security/costs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package integration
import (
"context"
"net/http"
"strconv"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -53,7 +52,6 @@ func TestOperationCost(t *testing.T) {

// @listSize(assumedSize: 50) overrides EstimatedListSize; cost = 50 * 2 = 100
estimated := res.Response.Header.Get(core.CostEstimatedHeader)
require.NotEmpty(t, estimated, "estimated cost header should be present")
require.Equal(t, "100", estimated)

// the actual cost should not be calculated nor exposed
Expand Down Expand Up @@ -153,7 +151,6 @@ func TestOperationCost(t *testing.T) {

// @listSize(assumedSize: 50) on employees overrides EstimatedListSize(200)
estimated := res.Response.Header.Get(core.CostEstimatedHeader)
require.NotEmpty(t, estimated, "estimated cost header should be present")
require.Equal(t, "50", estimated)
})
})
Expand Down Expand Up @@ -277,11 +274,9 @@ func TestOperationCost(t *testing.T) {
// upc, repositoryURL, id: 0 (scalars)
// total: (10 + 13) × 10 = 230
estimated := res.Response.Header.Get(core.CostEstimatedHeader)
require.NotEmpty(t, estimated, "estimated cost header should be present")
require.Equal(t, "230", estimated)

actual := res.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, actual, "actual cost header should be present")
require.Equal(t, "45", actual)

// Query 2: only employees-subgraph fields — Cosmo @cost(weight: 5) from employees applies
Expand All @@ -294,11 +289,60 @@ func TestOperationCost(t *testing.T) {
require.Equal(t, "150", estimated2)

actual2 := res2.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, actual2, "actual cost header should be present")
require.Equal(t, "21", actual2)
})
})

t.Run("input object field cost weight on department", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) {
securityConfiguration.CostControl = &config.CostControl{
Enabled: true,
Mode: config.CostControlModeMeasure,
MaxEstimatedLimit: 10000,
EstimatedListSize: 10,
ExposeHeaders: true,
}
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { department: ENGINEERING }) { id } }`,
})
require.Contains(t, res.Body, `"data":`)

// 10*1 + 17
require.Equal(t, "27", res.Response.Header.Get(core.CostEstimatedHeader))
// 7*1 + 17
require.Equal(t, "24", res.Response.Header.Get(core.CostActualHeader))
})
})

t.Run("input object field cost weight on title", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) {
securityConfiguration.CostControl = &config.CostControl{
Enabled: true,
Mode: config.CostControlModeMeasure,
MaxEstimatedLimit: 10000,
EstimatedListSize: 10,
ExposeHeaders: true,
}
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { title: "Founder" }) { id } }`,
})
require.Contains(t, res.Body, `"data":`)

// 10 * 1 - 3
require.Equal(t, "7", res.Response.Header.Get(core.CostEstimatedHeader))
// 1 * 1 - 3
require.Equal(t, "0", res.Response.Header.Get(core.CostActualHeader))
})
})

t.Run("slicingArguments controls list size estimation", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
Expand Down Expand Up @@ -714,27 +758,16 @@ func TestOperationCost(t *testing.T) {
// 1st request – plan cache MISS
res1 := xEnv.MakeGraphQLRequestOK(query)
require.Contains(t, res1.Body, `"data":`)

estimated1 := res1.Response.Header.Get(core.CostEstimatedHeader)
actual1 := res1.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, estimated1, "first request should have estimated cost header")
require.NotEmpty(t, actual1, "first request should have actual cost header")
require.Equal(t, "MISS", res1.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "8", res1.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "8", res1.Response.Header.Get(core.CostActualHeader))

// 2nd request – plan cache HIT
res2 := xEnv.MakeGraphQLRequestOK(query)
require.Contains(t, res2.Body, `"data":`)

estimated2 := res2.Response.Header.Get(core.CostEstimatedHeader)
actual2 := res2.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, estimated2, "second request should have estimated cost header")
require.NotEmpty(t, actual2, "second request should have actual cost header")

require.Equal(t, estimated1, estimated2,
"estimated cost differs between cache miss (%s) and cache hit (%s) ",
estimated1, estimated2)
require.Equal(t, actual1, actual2,
"actual cost differs between cache miss (%s) and cache hit (%s) ",
actual1, actual2)
require.Equal(t, "HIT", res2.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "8", res2.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "8", res2.Response.Header.Get(core.CostActualHeader))
})
})

Expand All @@ -759,8 +792,8 @@ func TestOperationCost(t *testing.T) {

estimated1 := res1.Response.Header.Get(core.CostEstimatedHeader)
actual1 := res1.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, estimated1, "first request should have estimated cost header")
require.NotEmpty(t, actual1, "first request should have actual cost header")
require.Equal(t, "8", estimated1)
require.Equal(t, "8", actual1)

// 2nd request – plan cache HIT
query2 := testenv.GraphQLRequest{
Expand All @@ -771,15 +804,8 @@ func TestOperationCost(t *testing.T) {

estimated2 := res2.Response.Header.Get(core.CostEstimatedHeader)
actual2 := res2.Response.Header.Get(core.CostActualHeader)
require.NotEmpty(t, estimated2, "second request should have estimated cost header")
require.NotEmpty(t, actual2, "second request should have actual cost header")

require.Equal(t, estimated1, estimated2,
"estimated cost differs between cache miss (%s) and cache hit (%s) ",
estimated1, estimated2)
require.Equal(t, actual1, actual2,
"actual cost differs between cache miss (%s) and cache hit (%s) ",
actual1, actual2)
require.Equal(t, "8", estimated2)
require.Equal(t, "8", actual2)
})
})

Expand Down Expand Up @@ -885,6 +911,57 @@ func TestOperationCost(t *testing.T) {
require.Equal(t, int64(24), totalSum, "total estimated cost sum should be 3×8=24")
})
})

t.Run("input object field costs are consistent across cache hits for different queries", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) {
securityConfiguration.CostControl = &config.CostControl{
Enabled: true,
Mode: config.CostControlModeMeasure,
EstimatedListSize: 10,
ExposeHeaders: true,
}
},
}, func(t *testing.T, xEnv *testenv.Environment) {
// 1st request – plan cache MISS
resDept1 := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { department: ENGINEERING }) { id } }`,
})
require.Contains(t, resDept1.Body, `"data":`)
require.Equal(t, "MISS", resDept1.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "27", resDept1.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "24", resDept1.Response.Header.Get(core.CostActualHeader))

// 2nd request – plan cache HIT (same normalized query, different input field)
// Cost is recalculated per request based on actual input field values
resTitle1 := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { title: "Founder" }) { id } }`,
})
require.Contains(t, resTitle1.Body, `"data":`)
require.Equal(t, "HIT", resTitle1.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "7", resTitle1.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "0", resTitle1.Response.Header.Get(core.CostActualHeader))

// 3rd request – cache HIT, same input field as 1st, different value
resDept2 := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { department: MARKETING }) { id } }`,
})
require.Contains(t, resDept2.Body, `"data":`)
require.Equal(t, "HIT", resDept2.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "27", resDept2.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "20", resDept2.Response.Header.Get(core.CostActualHeader))

// 4th request – cache HIT, same input field as 2nd, different value
resTitle2 := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ findEmployeesBy(criteria: { title: "Director" }) { id } }`,
})
require.Contains(t, resTitle2.Body, `"data":`)
require.Equal(t, "HIT", resTitle2.Response.Header.Get(core.ExecutionPlanCacheHeader))
require.Equal(t, "7", resTitle2.Response.Header.Get(core.CostEstimatedHeader))
require.Equal(t, "0", resTitle2.Response.Header.Get(core.CostActualHeader))
})
})
})

t.Run("negative weights", func(t *testing.T) {
Expand Down Expand Up @@ -972,12 +1049,7 @@ func TestOperationCost(t *testing.T) {
})
require.Contains(t, res.Body, `"data":`)

estimated := res.Response.Header.Get(core.CostEstimatedHeader)
require.NotEmpty(t, estimated)
estimatedVal, err := strconv.Atoi(estimated)
require.NoError(t, err)
require.Equal(t, estimatedVal, 8, "estimated cost must not be negative")
require.Equal(t, estimatedVal, 8, "negative type weight should reduce cost below baseline of 18")
require.Equal(t, "8", res.Response.Header.Get(core.CostEstimatedHeader))
})
})

Expand Down
Loading
Loading