Skip to content

Commit ee6a892

Browse files
yroblataskbotclaude
authored
Improvements for the vmcp e2e test infrastructure (#5026)
test/e2e: fix polling loop cancellation, unify OIDC options, remove stale link - Replace time.Now()/time.Sleep polling in WaitForVMCPHealthReady with context.WithTimeout + time.NewTicker + select, matching the pattern already used in WaitForMCPServerReady - Unify NewOIDCMockServer and NewOIDCMockServerWithClientOptions into a single variadic constructor accepting OIDCMockOption, which covers both Fosite-config and client-registration axes; update WithClientAudience and WithAccessTokenLifespan to return OIDCMockOption - Remove the Tracked by issue link from the vmcp_infra_features_test.go package comment (belongs in the PR/commit, not in source) Closes #5022 Co-authored-by: taskbot <taskbot@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 39a3785 commit ee6a892

3 files changed

Lines changed: 58 additions & 44 deletions

File tree

test/e2e/mcp_client_helpers.go

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,28 +69,39 @@ func NewMCPClientForStreamableHTTPWithToken(config *TestConfig, serverURL, token
6969
// is configured (MCP Initialize would fail with 401 for unauthenticated probes).
7070
func WaitForVMCPHealthReady(healthURL string, timeout time.Duration) error {
7171
httpClient := &http.Client{Timeout: 5 * time.Second}
72-
deadline := time.Now().Add(timeout)
72+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
73+
defer cancel()
74+
ticker := time.NewTicker(2 * time.Second)
75+
defer ticker.Stop()
7376
var lastErr error
7477
var lastStatus int
7578
var lastBody string
76-
for time.Now().Before(deadline) {
77-
resp, err := httpClient.Get(healthURL) //nolint:gosec // URL is test-controlled
78-
if resp != nil {
79-
lastStatus = resp.StatusCode
80-
bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
81-
_ = resp.Body.Close()
82-
lastBody = string(bodyBytes)
83-
}
84-
lastErr = err
85-
if err == nil && resp.StatusCode == http.StatusOK {
86-
return nil
79+
for {
80+
select {
81+
case <-ctx.Done():
82+
if lastErr != nil {
83+
return fmt.Errorf("timeout waiting for vMCP health endpoint at %s: last error: %w", healthURL, lastErr)
84+
}
85+
return fmt.Errorf("timeout waiting for vMCP health endpoint at %s: last status: %d, body: %s", healthURL, lastStatus, lastBody)
86+
case <-ticker.C:
87+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil) //nolint:gosec // URL is test-controlled
88+
if err != nil {
89+
lastErr = err
90+
continue
91+
}
92+
resp, err := httpClient.Do(req)
93+
if resp != nil {
94+
lastStatus = resp.StatusCode
95+
bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
96+
_ = resp.Body.Close()
97+
lastBody = string(bodyBytes)
98+
}
99+
lastErr = err
100+
if err == nil && resp.StatusCode == http.StatusOK {
101+
return nil
102+
}
87103
}
88-
time.Sleep(2 * time.Second)
89-
}
90-
if lastErr != nil {
91-
return fmt.Errorf("timeout waiting for vMCP health endpoint at %s: last error: %w", healthURL, lastErr)
92104
}
93-
return fmt.Errorf("timeout waiting for vMCP health endpoint at %s: last status: %d, body: %s", healthURL, lastStatus, lastBody)
94105
}
95106

96107
// Initialize initializes the MCP connection

test/e2e/oidc_mock.go

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,35 @@ type AuthRequest struct {
5050
// pkg/auth's token validator requires the kid claim to look up the signing key.
5151
const jwtKeyID = "test-key-1"
5252

53-
// OIDCMockServerOption configures the OIDCMockServer client registration.
54-
type OIDCMockServerOption func(*fosite.DefaultClient)
53+
// OIDCMockOption is a unified option type for configuring the OIDC mock server.
54+
// Use WithClientAudience for client-registration settings and WithAccessTokenLifespan
55+
// (or other fosite-level helpers) for token-lifecycle settings. A single constructor
56+
// accepts both, so tests needing both a custom token lifetime and a specific audience
57+
// no longer require separate constructors.
58+
type OIDCMockOption struct {
59+
fositeOpt func(*fosite.Config)
60+
clientOpt func(*fosite.DefaultClient)
61+
}
5562

5663
// WithClientAudience sets the allowed audience(s) on the registered test client.
5764
// Use this when the vMCP OIDC config requires a specific audience claim in tokens.
58-
func WithClientAudience(audiences ...string) OIDCMockServerOption {
59-
return func(c *fosite.DefaultClient) {
65+
func WithClientAudience(audiences ...string) OIDCMockOption {
66+
return OIDCMockOption{clientOpt: func(c *fosite.DefaultClient) {
6067
c.Audience = audiences
61-
}
68+
}}
6269
}
6370

64-
// NewOIDCMockServer creates a new OIDC mock server using Ory Fosite
65-
func NewOIDCMockServer(port int, clientID, clientSecret string, opts ...func(*fosite.Config)) (*OIDCMockServer, error) {
71+
// NewOIDCMockServer creates a new OIDC mock server using Ory Fosite.
72+
// Use WithClientAudience to set client-level options and WithAccessTokenLifespan
73+
// for Fosite-level settings. Both option kinds may be mixed in a single call.
74+
func NewOIDCMockServer(port int, clientID, clientSecret string, opts ...OIDCMockOption) (*OIDCMockServer, error) {
6675
config := defaultFositeConfig(port)
6776
for _, opt := range opts {
68-
opt(config)
77+
if opt.fositeOpt != nil {
78+
opt.fositeOpt(config)
79+
}
6980
}
70-
return newOIDCMockServer(port, clientID, clientSecret, config)
71-
}
72-
73-
// NewOIDCMockServerWithClientOptions creates a new OIDC mock server, applying
74-
// clientOpts to the registered test client. Use this when you need to control
75-
// client-level settings such as Audience.
76-
func NewOIDCMockServerWithClientOptions(
77-
port int, clientID, clientSecret string, clientOpts ...OIDCMockServerOption,
78-
) (*OIDCMockServer, error) {
79-
return newOIDCMockServer(port, clientID, clientSecret, defaultFositeConfig(port), clientOpts...)
81+
return newOIDCMockServer(port, clientID, clientSecret, config, opts...)
8082
}
8183

8284
// defaultFositeConfig returns the standard Fosite config for the mock server.
@@ -93,9 +95,9 @@ func defaultFositeConfig(port int) *fosite.Config {
9395
}
9496
}
9597

96-
// newOIDCMockServer is the shared implementation for the public constructors.
98+
// newOIDCMockServer is the shared implementation for NewOIDCMockServer.
9799
func newOIDCMockServer(
98-
port int, clientID, clientSecret string, config *fosite.Config, clientOpts ...OIDCMockServerOption,
100+
port int, clientID, clientSecret string, config *fosite.Config, opts ...OIDCMockOption,
99101
) (*OIDCMockServer, error) {
100102
// Generate RSA key for JWT signing
101103
key, err := rsa.GenerateKey(rand.Reader, 2048)
@@ -121,8 +123,10 @@ func newOIDCMockServer(
121123
GrantTypes: []string{"authorization_code", "refresh_token", "client_credentials"},
122124
Scopes: []string{"openid", "profile", "email"},
123125
}
124-
for _, opt := range clientOpts {
125-
opt(client)
126+
for _, opt := range opts {
127+
if opt.clientOpt != nil {
128+
opt.clientOpt(client)
129+
}
126130
}
127131
store.Clients[clientID] = client
128132

@@ -497,8 +501,8 @@ func (*OIDCMockServer) CompleteAuthRequest(authReq *AuthRequest) error {
497501
}
498502

499503
// WithAccessTokenLifespan sets the lifespan of access tokens for the OIDC mock server.
500-
func WithAccessTokenLifespan(d time.Duration) func(*fosite.Config) {
501-
return func(c *fosite.Config) {
504+
func WithAccessTokenLifespan(d time.Duration) OIDCMockOption {
505+
return OIDCMockOption{fositeOpt: func(c *fosite.Config) {
502506
c.AccessTokenLifespan = d
503-
}
507+
}}
504508
}

test/e2e/vmcp_infra_features_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// Package e2e_test contains infrastructure-heavy vMCP CLI e2e tests that require
55
// external services (OIDC server, Redis) as test fixtures.
66
// These complement the basic feature tests in vmcp_cli_features_test.go.
7-
// Tracked by: https://github.com/stacklok/toolhive/issues/4944
87
package e2e_test
98

109
import (
@@ -107,7 +106,7 @@ var _ = Describe("vMCP infra features", Label("vmcp", "e2e", "infra"), func() {
107106

108107
oidcPort = allocateVMCPPort()
109108
var err error
110-
oidcServer, err = e2e.NewOIDCMockServerWithClientOptions(oidcPort, "test-client", "test-secret",
109+
oidcServer, err = e2e.NewOIDCMockServer(oidcPort, "test-client", "test-secret",
111110
e2e.WithClientAudience("vmcp-e2e-test"),
112111
)
113112
Expect(err).ToNot(HaveOccurred())

0 commit comments

Comments
 (0)