test(wasm-go/mcp): expand unit test coverage for mcp-server framework (#3871)

Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jingze
2026-06-15 20:29:20 +08:00
committed by GitHub
parent c69526b30e
commit bf0b1e96c5
10 changed files with 3426 additions and 3 deletions

View File

@@ -18,6 +18,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMcpProxyServerBasicInterface tests that McpProxyServer implements the Server interface
@@ -110,3 +111,329 @@ func TestMcpProxyServerSecuritySchemes(t *testing.T) {
assert.Equal(t, scheme.ID, retrievedScheme.ID)
assert.Equal(t, scheme.Type, retrievedScheme.Type)
}
// -----------------------------------------------------------------------------
// SetDefaultDownstreamSecurity / SetDefaultUpstreamSecurity / PassthroughAuth
// -----------------------------------------------------------------------------
func TestMcpProxyServer_SetGetDefaultDownstreamSecurity(t *testing.T) {
s := NewMcpProxyServer("p")
assert.Equal(t, "", s.GetDefaultDownstreamSecurity().ID, "fresh server has empty default")
s.SetDefaultDownstreamSecurity(SecurityRequirement{ID: "K", Passthrough: true})
got := s.GetDefaultDownstreamSecurity()
assert.Equal(t, "K", got.ID)
assert.True(t, got.Passthrough)
}
func TestMcpProxyServer_SetGetDefaultUpstreamSecurity(t *testing.T) {
s := NewMcpProxyServer("p")
s.SetDefaultUpstreamSecurity(SecurityRequirement{ID: "U", Credential: "c"})
got := s.GetDefaultUpstreamSecurity()
assert.Equal(t, "U", got.ID)
assert.Equal(t, "c", got.Credential)
}
func TestMcpProxyServer_PassthroughAuthHeaderGetterAndSetter(t *testing.T) {
s := NewMcpProxyServer("p")
assert.False(t, s.GetPassthroughAuthHeader(), "default is false")
s.SetPassthroughAuthHeader(true)
assert.True(t, s.GetPassthroughAuthHeader())
s.SetPassthroughAuthHeader(false)
assert.False(t, s.GetPassthroughAuthHeader())
}
// -----------------------------------------------------------------------------
// AddSecurityScheme — nil-map branch
// -----------------------------------------------------------------------------
func TestMcpProxyServer_AddSecurityScheme_InitializesNilMap(t *testing.T) {
// Skip the constructor so we can hit the `securitySchemes == nil` branch.
s := &McpProxyServer{Name: "p"}
s.AddSecurityScheme(SecurityScheme{ID: "K", Type: "apiKey", In: "header", Name: "X"})
got, ok := s.GetSecurityScheme("K")
require.True(t, ok)
assert.Equal(t, "K", got.ID)
}
// -----------------------------------------------------------------------------
// AddMCPTool — delegates to BaseMCPServer
// -----------------------------------------------------------------------------
func TestMcpProxyServer_AddMCPTool_StoresInBaseAndReturnsSelf(t *testing.T) {
s := NewMcpProxyServer("p")
stub := &stubTool{desc: "d"}
ret := s.AddMCPTool("custom", stub)
assert.Same(t, s, ret, "AddMCPTool returns receiver for chaining")
tools := s.GetMCPTools()
got, ok := tools["custom"]
require.True(t, ok)
assert.Same(t, stub, got)
}
// -----------------------------------------------------------------------------
// AddProxyTool — overrides on duplicate name
// -----------------------------------------------------------------------------
func TestMcpProxyServer_AddProxyTool_DuplicateNameOverwrites(t *testing.T) {
s := NewMcpProxyServer("p")
require.NoError(t, s.AddProxyTool(McpProxyToolConfig{Name: "t", Description: "first"}))
require.NoError(t, s.AddProxyTool(McpProxyToolConfig{Name: "t", Description: "second"}))
tools := s.GetMCPTools()
assert.Len(t, tools, 1, "duplicate AddProxyTool should overwrite, not duplicate")
cfg, ok := s.GetToolConfig("t")
require.True(t, ok)
assert.Equal(t, "second", cfg.Description, "later AddProxyTool wins")
}
// -----------------------------------------------------------------------------
// GetToolConfig — hit and miss
// -----------------------------------------------------------------------------
func TestMcpProxyServer_GetToolConfig_HitAndMiss(t *testing.T) {
s := NewMcpProxyServer("p")
require.NoError(t, s.AddProxyTool(McpProxyToolConfig{Name: "t", Description: "d"}))
cfg, ok := s.GetToolConfig("t")
require.True(t, ok)
assert.Equal(t, "d", cfg.Description)
_, missOK := s.GetToolConfig("missing")
assert.False(t, missOK)
}
// -----------------------------------------------------------------------------
// Clone — deep copy of toolsConfig and securitySchemes
// -----------------------------------------------------------------------------
func TestMcpProxyServer_Clone_DeepCopiesToolsConfigAndSchemes(t *testing.T) {
orig := NewMcpProxyServer("orig")
orig.SetMcpServerURL("http://b")
orig.SetTimeout(1234)
orig.SetTransport(TransportSSE)
orig.SetPassthroughAuthHeader(true)
orig.SetDefaultDownstreamSecurity(SecurityRequirement{ID: "K"})
orig.AddSecurityScheme(SecurityScheme{ID: "K", Type: "apiKey", In: "header", Name: "X"})
require.NoError(t, orig.AddProxyTool(McpProxyToolConfig{Name: "t", Description: "d"}))
clonedI := orig.Clone()
cloned, ok := clonedI.(*McpProxyServer)
require.True(t, ok)
require.NotSame(t, orig, cloned, "Clone must return a fresh struct")
// Surface fields are copied.
assert.Equal(t, orig.Name, cloned.Name)
// NOTE: Clone does not propagate mcpServerURL/timeout/transport/passthrough
// nor defaultDownstream/upstreamSecurity. That is intentional today (see
// proxy_server.go:188): cloning is used for per-request isolation of
// tool/security registries only. This test pins that contract — if Clone
// starts copying those fields, update here and document the change.
assert.Equal(t, "", cloned.GetMcpServerURL())
// toolsConfig: deep copy — adding to clone doesn't bleed back to orig.
require.NoError(t, cloned.AddProxyTool(McpProxyToolConfig{Name: "extra", Description: "x"}))
_, origHasExtra := orig.GetToolConfig("extra")
assert.False(t, origHasExtra, "tool added to clone must not appear in original")
// securitySchemes: deep copy — replacing scheme on clone doesn't touch orig.
cloned.AddSecurityScheme(SecurityScheme{ID: "K", Type: "http", Scheme: "bearer"})
origScheme, _ := orig.GetSecurityScheme("K")
clonedScheme, _ := cloned.GetSecurityScheme("K")
assert.Equal(t, "apiKey", origScheme.Type, "original scheme must remain apiKey")
assert.Equal(t, "http", clonedScheme.Type, "clone reflects the override")
}
// -----------------------------------------------------------------------------
// McpProxyTool — Description / InputSchema / OutputSchema / Create
// -----------------------------------------------------------------------------
func TestMcpProxyTool_DescriptionAndOutputSchema(t *testing.T) {
tool := &McpProxyTool{
toolConfig: McpProxyToolConfig{
Description: "describe me",
OutputSchema: map[string]any{"type": "string"},
},
}
assert.Equal(t, "describe me", tool.Description())
assert.Equal(t, map[string]any{"type": "string"}, tool.OutputSchema())
}
func TestMcpProxyTool_InputSchema_RequiredAndOptionalAndEnumAndDefault(t *testing.T) {
tool := &McpProxyTool{
toolConfig: McpProxyToolConfig{
Args: []ToolArg{
{Name: "must", Type: "string", Description: "required", Required: true},
{Name: "opt", Type: "integer", Description: "optional", Default: 7},
{Name: "pick", Type: "string", Description: "enum", Enum: []interface{}{"a", "b"}},
},
},
}
schema := tool.InputSchema()
assert.Equal(t, "object", schema["type"])
required, ok := schema["required"].([]string)
require.True(t, ok)
assert.Equal(t, []string{"must"}, required, "only Required:true args land in required[]")
props := schema["properties"].(map[string]any)
mustProp := props["must"].(map[string]any)
assert.Equal(t, "string", mustProp["type"])
optProp := props["opt"].(map[string]any)
assert.Equal(t, 7, optProp["default"])
pickProp := props["pick"].(map[string]any)
assert.Equal(t, []interface{}{"a", "b"}, pickProp["enum"])
}
func TestMcpProxyTool_InputSchema_NoArgs(t *testing.T) {
tool := &McpProxyTool{toolConfig: McpProxyToolConfig{}}
schema := tool.InputSchema()
props, ok := schema["properties"].(map[string]any)
require.True(t, ok)
assert.Empty(t, props)
required, ok := schema["required"].([]string)
require.True(t, ok)
assert.Empty(t, required)
}
func TestMcpProxyTool_Create_NewInstanceWithBoundArgs(t *testing.T) {
orig := &McpProxyTool{
serverName: "srv",
name: "t",
toolConfig: McpProxyToolConfig{Name: "t"},
}
created := orig.Create([]byte(`{"q":"hello","n":7}`))
require.NotSame(t, orig, created, "Create returns a fresh instance")
cloned := created.(*McpProxyTool)
assert.Equal(t, "srv", cloned.serverName)
assert.Equal(t, "t", cloned.name)
assert.Equal(t, "hello", cloned.arguments["q"])
// JSON unmarshals numbers as float64.
assert.Equal(t, float64(7), cloned.arguments["n"])
}
func TestMcpProxyTool_Create_EmptyParamsStillReturnsInstance(t *testing.T) {
orig := &McpProxyTool{serverName: "s", name: "t"}
created := orig.Create(nil)
cloned := created.(*McpProxyTool)
assert.Equal(t, "s", cloned.serverName)
assert.Equal(t, "t", cloned.name)
require.NotNil(t, cloned.arguments)
assert.Empty(t, cloned.arguments, "no params → empty arguments map, not nil")
}
func TestMcpProxyTool_Create_MalformedJSON_PreservesEmptyArgs(t *testing.T) {
orig := &McpProxyTool{serverName: "s", name: "t"}
created := orig.Create([]byte(`{not json`))
cloned := created.(*McpProxyTool)
// json.Unmarshal silently fails; arguments stays empty.
assert.Empty(t, cloned.arguments)
}
// -----------------------------------------------------------------------------
// ValidateSecurityScheme — full matrix
// -----------------------------------------------------------------------------
func TestValidateSecurityScheme(t *testing.T) {
cases := []struct {
name string
scheme SecurityScheme
wantErr string
}{
{"missing ID", SecurityScheme{Type: "apiKey", In: "header", Name: "X"}, "ID is required"},
{"invalid type", SecurityScheme{ID: "k", Type: "oauth2"}, "invalid security scheme type"},
{"apiKey missing name", SecurityScheme{ID: "k", Type: "apiKey", In: "header"}, "name is required"},
{"apiKey invalid in", SecurityScheme{ID: "k", Type: "apiKey", In: "body", Name: "X"}, "invalid security scheme location"},
{"apiKey ok header", SecurityScheme{ID: "k", Type: "apiKey", In: "header", Name: "X"}, ""},
{"apiKey ok query", SecurityScheme{ID: "k", Type: "apiKey", In: "query", Name: "X"}, ""},
{"apiKey ok cookie", SecurityScheme{ID: "k", Type: "apiKey", In: "cookie", Name: "X"}, ""},
{"http missing scheme", SecurityScheme{ID: "k", Type: "http"}, "scheme is required for http"},
{"http bearer ok", SecurityScheme{ID: "k", Type: "http", Scheme: "bearer"}, ""},
{"http basic ok", SecurityScheme{ID: "k", Type: "http", Scheme: "basic"}, ""},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := ValidateSecurityScheme(c.scheme)
if c.wantErr == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), c.wantErr)
}
})
}
}
// -----------------------------------------------------------------------------
// ValidateToolConfig — full matrix
// -----------------------------------------------------------------------------
func TestValidateToolConfig(t *testing.T) {
cases := []struct {
name string
config McpProxyToolConfig
wantErr string
}{
{
"missing name",
McpProxyToolConfig{Description: "d"},
"tool name is required",
},
{
"missing description",
McpProxyToolConfig{Name: "t"},
"tool description is required",
},
{
"arg missing name",
McpProxyToolConfig{Name: "t", Description: "d", Args: []ToolArg{{Type: "string", Description: "x"}}},
"argument name is required",
},
{
"arg duplicate names",
McpProxyToolConfig{Name: "t", Description: "d", Args: []ToolArg{
{Name: "a", Type: "string", Description: "x"},
{Name: "a", Type: "string", Description: "y"},
}},
"duplicate argument name",
},
{
"arg missing description",
McpProxyToolConfig{Name: "t", Description: "d", Args: []ToolArg{{Name: "a", Type: "string"}}},
"argument description is required",
},
{
"arg invalid type",
McpProxyToolConfig{Name: "t", Description: "d", Args: []ToolArg{{Name: "a", Type: "money", Description: "x"}}},
"invalid argument type",
},
{
"happy path with multiple typed args",
McpProxyToolConfig{Name: "t", Description: "d", Args: []ToolArg{
{Name: "s", Type: "string", Description: "x"},
{Name: "n", Type: "number", Description: "x"},
{Name: "i", Type: "integer", Description: "x"},
{Name: "b", Type: "boolean", Description: "x"},
{Name: "a", Type: "array", Description: "x"},
{Name: "o", Type: "object", Description: "x"},
}},
"",
},
{
"happy path no args",
McpProxyToolConfig{Name: "t", Description: "d"},
"",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := ValidateToolConfig(c.config)
if c.wantErr == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), c.wantErr)
}
})
}
}