Skip to content

Commit ab5bd1c

Browse files
committed
Merge remote-tracking branch 'origin/main' into sergiy/eng-8285-improve-query-planning-time
2 parents 60a72b3 + 83e0ed6 commit ab5bd1c

31 files changed

+1947
-196
lines changed

router-tests/authentication_test.go

Lines changed: 995 additions & 83 deletions
Large diffs are not rendered by default.

router-tests/batch_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,18 @@ func TestBatch(t *testing.T) {
333333
t.Parallel()
334334

335335
authenticators, authServer := ConfigureAuth(t)
336+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
337+
Authenticators: authenticators,
338+
AuthenticationRequired: false,
339+
SkipIntrospectionQueries: false,
340+
IntrospectionSkipSecret: "",
341+
})
342+
require.NoError(t, err)
336343

337344
testenv.Run(t,
338345
&testenv.Config{
339346
RouterOptions: []core.Option{
340-
core.WithAccessController(core.NewAccessController(authenticators, false)),
347+
core.WithAccessController(accessController),
341348
},
342349
BatchingConfig: config.BatchingConfig{
343350
Enabled: true,
@@ -692,14 +699,22 @@ func TestBatch(t *testing.T) {
692699
t.Parallel()
693700

694701
authenticators, authServer := ConfigureAuth(t)
702+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
703+
Authenticators: authenticators,
704+
AuthenticationRequired: false,
705+
SkipIntrospectionQueries: false,
706+
IntrospectionSkipSecret: "",
707+
})
708+
require.NoError(t, err)
709+
695710
testenv.Run(t, &testenv.Config{
696711
BatchingConfig: config.BatchingConfig{
697712
Enabled: true,
698713
MaxConcurrency: 10,
699714
MaxEntriesPerBatch: 100,
700715
},
701716
RouterOptions: []core.Option{
702-
core.WithAccessController(core.NewAccessController(authenticators, false)),
717+
core.WithAccessController(accessController),
703718
core.WithRouterTrafficConfig(&config.RouterTrafficConfiguration{
704719
MaxRequestBodyBytes: 5 << 20, // 5MiB
705720
DecompressionEnabled: true,

router-tests/block_operations_test.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,17 @@ func TestBlockOperations(t *testing.T) {
150150
t.Parallel()
151151

152152
authenticators, authServer := ConfigureAuth(t)
153+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
154+
Authenticators: authenticators,
155+
AuthenticationRequired: false,
156+
SkipIntrospectionQueries: false,
157+
IntrospectionSkipSecret: "",
158+
})
159+
require.NoError(t, err)
160+
153161
testenv.Run(t, &testenv.Config{
154162
RouterOptions: []core.Option{
155-
core.WithAccessController(core.NewAccessController(authenticators, false)),
163+
core.WithAccessController(accessController),
156164
},
157165
ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) {
158166
securityConfiguration.BlockMutations = config.BlockOperationConfiguration{
@@ -303,10 +311,17 @@ func TestBlockOperations(t *testing.T) {
303311
t.Parallel()
304312

305313
authenticators, authServer := ConfigureAuth(t)
314+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
315+
Authenticators: authenticators,
316+
AuthenticationRequired: false,
317+
SkipIntrospectionQueries: false,
318+
IntrospectionSkipSecret: "",
319+
})
320+
require.NoError(t, err)
306321

307322
testenv.Run(t, &testenv.Config{
308323
RouterOptions: []core.Option{
309-
core.WithAccessController(core.NewAccessController(authenticators, false)),
324+
core.WithAccessController(accessController),
310325
core.WithAuthorizationConfig(&config.AuthorizationConfiguration{
311326
RejectOperationIfUnauthorized: false,
312327
}),
@@ -395,14 +410,21 @@ func TestBlockOperations(t *testing.T) {
395410
t.Parallel()
396411

397412
authenticators, authServer := ConfigureAuth(t)
413+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
414+
Authenticators: authenticators,
415+
AuthenticationRequired: false,
416+
SkipIntrospectionQueries: false,
417+
IntrospectionSkipSecret: "",
418+
})
419+
require.NoError(t, err)
398420

399421
testenv.Run(t, &testenv.Config{
400422
ModifyWebsocketConfiguration: func(cfg *config.WebSocketConfiguration) {
401423
cfg.Authentication.FromInitialPayload.Enabled = true
402424
cfg.Enabled = true
403425
},
404426
RouterOptions: []core.Option{
405-
core.WithAccessController(core.NewAccessController(authenticators, false)),
427+
core.WithAccessController(accessController),
406428
core.WithAuthorizationConfig(&config.AuthorizationConfiguration{
407429
RejectOperationIfUnauthorized: false,
408430
}),

router-tests/header_set_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,21 @@ func TestHeaderSetWithExpression(t *testing.T) {
275275
authenticator, err := authentication.NewHttpHeaderAuthenticator(authOptions)
276276
require.NoError(t, err)
277277

278+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
279+
Authenticators: []authentication.Authenticator{authenticator},
280+
AuthenticationRequired: true,
281+
SkipIntrospectionQueries: false,
282+
IntrospectionSkipSecret: "",
283+
})
284+
require.NoError(t, err)
285+
278286
token, err := authServer.TokenForKID(rsa1.KID(), map[string]any{"user_id": "TestId"}, false)
279287
require.NoError(t, err)
280288

281289
testenv.Run(t, &testenv.Config{
282290
RouterOptions: append(
283291
global(customHeader, `request.auth.claims.user_id`),
284-
core.WithAccessController(core.NewAccessController([]authentication.Authenticator{authenticator}, true)),
292+
core.WithAccessController(accessController),
285293
),
286294
}, func(t *testing.T, xEnv *testenv.Environment) {
287295
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{

router-tests/mcp_test.go

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"fmt"
66
"net/http"
77
"strings"
8+
"sync"
89
"testing"
910

1011
"github.com/mark3labs/mcp-go/mcp"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314
"github.com/wundergraph/cosmo/router-tests/testenv"
15+
"github.com/wundergraph/cosmo/router/core"
1416
"github.com/wundergraph/cosmo/router/pkg/config"
1517
)
1618

@@ -212,7 +214,6 @@ func TestMCP(t *testing.T) {
212214
t.Run("Execute Query", func(t *testing.T) {
213215
t.Run("Execute operation of type query with valid input", func(t *testing.T) {
214216
testenv.Run(t, &testenv.Config{
215-
EnableNats: true,
216217
MCP: config.MCPConfiguration{
217218
Enabled: true,
218219
},
@@ -553,4 +554,124 @@ func TestMCP(t *testing.T) {
553554
})
554555
})
555556
})
557+
558+
t.Run("Header Forwarding", func(t *testing.T) {
559+
t.Run("All request headers are forwarded from MCP client through to subgraphs", func(t *testing.T) {
560+
// This test validates that ALL headers sent by MCP clients are forwarded
561+
// through the complete chain: MCP Client -> MCP Server -> Router -> Subgraphs
562+
//
563+
// The router's header forwarding rules (configured with wildcard `.*`) determine
564+
// what gets propagated to subgraphs. The MCP server acts as a transparent proxy,
565+
// forwarding all headers without filtering.
566+
//
567+
// Note: We use direct HTTP POST requests instead of the mcp-go client library
568+
// because transport.WithHTTPHeaders() in mcp-go sets headers at the SSE connection
569+
// level, not on individual tool execution requests. Direct HTTP requests allow us
570+
// to test per-request headers, which is what real MCP clients (like Claude Desktop) send.
571+
572+
var capturedSubgraphRequest *http.Request
573+
var subgraphMutex sync.Mutex
574+
575+
testenv.Run(t, &testenv.Config{
576+
MCP: config.MCPConfiguration{
577+
Enabled: true,
578+
Session: config.MCPSessionConfig{
579+
Stateless: true, // Enable stateless mode so we don't need session IDs
580+
},
581+
},
582+
RouterOptions: []core.Option{
583+
// Forward all headers including custom ones
584+
core.WithHeaderRules(config.HeaderRules{
585+
All: &config.GlobalHeaderRule{
586+
Request: []*config.RequestHeaderRule{
587+
{
588+
Operation: config.HeaderRuleOperationPropagate,
589+
Matching: ".*", // Forward all headers
590+
},
591+
},
592+
},
593+
}),
594+
},
595+
Subgraphs: testenv.SubgraphsConfig{
596+
GlobalMiddleware: func(handler http.Handler) http.Handler {
597+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
598+
subgraphMutex.Lock()
599+
capturedSubgraphRequest = r.Clone(r.Context())
600+
subgraphMutex.Unlock()
601+
handler.ServeHTTP(w, r)
602+
})
603+
},
604+
},
605+
}, func(t *testing.T, xEnv *testenv.Environment) {
606+
// With stateless mode enabled, we can make direct HTTP POST requests
607+
// without needing to establish a session first
608+
mcpAddr := xEnv.GetMCPServerAddr()
609+
610+
// Make a direct HTTP POST request with custom headers
611+
// This simulates a real MCP client sending custom headers on tool calls
612+
mcpRequest := map[string]interface{}{
613+
"jsonrpc": "2.0",
614+
"id": 1,
615+
"method": "tools/call",
616+
"params": map[string]interface{}{
617+
"name": "execute_operation_my_employees",
618+
"arguments": map[string]interface{}{
619+
"criteria": map[string]interface{}{},
620+
},
621+
},
622+
}
623+
624+
requestBody, err := json.Marshal(mcpRequest)
625+
require.NoError(t, err)
626+
627+
req, err := http.NewRequest("POST", mcpAddr, strings.NewReader(string(requestBody)))
628+
require.NoError(t, err)
629+
630+
// Add various headers to test forwarding
631+
req.Header.Set("Content-Type", "application/json")
632+
req.Header.Set("foo", "bar") // Non-standard header
633+
req.Header.Set("X-Custom-Header", "custom-value") // Custom X- header
634+
req.Header.Set("X-Trace-Id", "trace-123") // Tracing header
635+
req.Header.Set("Authorization", "Bearer test-token") // Auth header
636+
637+
// Make the request
638+
resp, err := xEnv.RouterClient.Do(req)
639+
require.NoError(t, err)
640+
defer resp.Body.Close()
641+
642+
// With stateless mode, the request should succeed
643+
t.Logf("Response Status: %d", resp.StatusCode)
644+
require.Equal(t, http.StatusOK, resp.StatusCode, "Request should succeed in stateless mode")
645+
646+
// Verify headers reached subgraph
647+
subgraphMutex.Lock()
648+
defer subgraphMutex.Unlock()
649+
650+
require.NotNil(t, capturedSubgraphRequest, "Subgraph should have received a request")
651+
652+
// Log all headers that the subgraph received
653+
t.Logf("Headers received by subgraph:")
654+
for key, values := range capturedSubgraphRequest.Header {
655+
for _, value := range values {
656+
t.Logf(" %s: %s", key, value)
657+
}
658+
}
659+
660+
// Verify that all headers were forwarded through the entire chain:
661+
// MCP Client -> MCP Server -> Router -> Subgraph
662+
assert.Equal(t, "bar", capturedSubgraphRequest.Header.Get("Foo"),
663+
"'foo' header should be forwarded to subgraph")
664+
assert.Equal(t, "custom-value", capturedSubgraphRequest.Header.Get("X-Custom-Header"),
665+
"X-Custom-Header should be forwarded to subgraph")
666+
assert.Equal(t, "trace-123", capturedSubgraphRequest.Header.Get("X-Trace-Id"),
667+
"X-Trace-Id should be forwarded to subgraph")
668+
assert.Equal(t, "Bearer test-token", capturedSubgraphRequest.Header.Get("Authorization"),
669+
"Authorization header should be forwarded to subgraph")
670+
671+
// This test proves that ALL headers sent by MCP clients are forwarded
672+
// through the complete chain. The router's header rules determine what
673+
// ultimately reaches the subgraphs.
674+
})
675+
})
676+
})
556677
}

router-tests/modules/router_on_request_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package module_test
22

33
import (
44
"encoding/json"
5-
"github.com/wundergraph/cosmo/router-tests/modules/router-on-request"
6-
"go.uber.org/zap/zapcore"
75
"net/http"
86
"testing"
97

8+
router_on_request "github.com/wundergraph/cosmo/router-tests/modules/router-on-request"
9+
"go.uber.org/zap/zapcore"
10+
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
"github.com/wundergraph/cosmo/router-tests/testenv"
@@ -69,9 +70,17 @@ func TestRouterOnRequestHook(t *testing.T) {
6970
},
7071
}
7172

73+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
74+
Authenticators: authenticators,
75+
AuthenticationRequired: true,
76+
SkipIntrospectionQueries: false,
77+
IntrospectionSkipSecret: "",
78+
})
79+
require.NoError(t, err)
80+
7281
testenv.Run(t, &testenv.Config{
7382
RouterOptions: []core.Option{
74-
core.WithAccessController(core.NewAccessController(authenticators, true)),
83+
core.WithAccessController(accessController),
7584
core.WithModulesConfig(cfg.Modules),
7685
core.WithCustomModules(&router_on_request.RouterOnRequestModule{}),
7786
},

router-tests/modules/set_authentication_scopes_test.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,17 @@ func TestCustomModuleSetAuthenticationScopes(t *testing.T) {
3232
},
3333
}
3434
authenticators, authServer := configureAuth(t)
35+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
36+
Authenticators: authenticators,
37+
AuthenticationRequired: false,
38+
SkipIntrospectionQueries: false,
39+
IntrospectionSkipSecret: "",
40+
})
41+
require.NoError(t, err)
42+
3543
testenv.Run(t, &testenv.Config{
3644
RouterOptions: []core.Option{
37-
core.WithAccessController(core.NewAccessController(authenticators, false)),
45+
core.WithAccessController(accessController),
3846
core.WithModulesConfig(cfg.Modules),
3947
core.WithCustomModules(&setScopesModule.SetAuthenticationScopesModule{}, &verifyScopes.VerifyScopesModule{}),
4048
},
@@ -73,9 +81,17 @@ func TestCustomModuleSetAuthenticationScopes(t *testing.T) {
7381
},
7482
}
7583
authenticators, authServer := configureAuth(t)
84+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
85+
Authenticators: authenticators,
86+
AuthenticationRequired: false,
87+
SkipIntrospectionQueries: false,
88+
IntrospectionSkipSecret: "",
89+
})
90+
require.NoError(t, err)
91+
7692
testenv.Run(t, &testenv.Config{
7793
RouterOptions: []core.Option{
78-
core.WithAccessController(core.NewAccessController(authenticators, false)),
94+
core.WithAccessController(accessController),
7995
core.WithModulesConfig(cfg.Modules),
8096
core.WithCustomModules(&setScopesModule.SetAuthenticationScopesModule{}, &verifyScopes.VerifyScopesModule{}),
8197
},
@@ -116,9 +132,17 @@ func TestCustomModuleSetAuthenticationScopes(t *testing.T) {
116132
},
117133
}
118134
authenticators, authServer := configureAuth(t)
135+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
136+
Authenticators: authenticators,
137+
AuthenticationRequired: false,
138+
SkipIntrospectionQueries: false,
139+
IntrospectionSkipSecret: "",
140+
})
141+
require.NoError(t, err)
142+
119143
testenv.Run(t, &testenv.Config{
120144
RouterOptions: []core.Option{
121-
core.WithAccessController(core.NewAccessController(authenticators, false)),
145+
core.WithAccessController(accessController),
122146
core.WithModulesConfig(cfg.Modules),
123147
core.WithCustomModules(&setScopesModule.SetAuthenticationScopesModule{}, &verifyScopes.VerifyScopesModule{}),
124148
},

0 commit comments

Comments
 (0)