package mcp import ( "context" "encoding/json" "testing" "github.com/google/uuid" "github.com/modelcontextprotocol/go-sdk/mcp" ) // mockSecretBackend implements secretBackend for testing. type mockSecretBackend struct { data map[string]string } func (m *mockSecretBackend) Get(ctx context.Context, key string) (string, error) { v, ok := m.data[key] if !ok { return "", &secretErr{msg: "secret not found: " + key} } return v, nil } func (m *mockSecretBackend) Set(ctx context.Context, key string, value string) error { m.data[key] = value return nil } func (m *mockSecretBackend) List(ctx context.Context) ([]string, error) { keys := make([]string, 0, len(m.data)) for k := range m.data { keys = append(keys, k) } return keys, nil } type secretErr struct{ msg string } func (e *secretErr) Error() string { return e.msg } // findToolHandler locates a tool's handler from allTools by name. func findToolHandler(t *testing.T, pool interface{}, name string, sec secretBackend) func(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t.Helper() for _, r := range allTools(nil, uuid.Nil, sec) { if r.tool.Name == name { return r.handler } } t.Fatalf("tool %q not found", name) return nil } func callToolJSON(t *testing.T, name string, sec secretBackend, args map[string]any) any { t.Helper() handler := findToolHandler(t, nil, name, sec) argBytes, _ := json.Marshal(args) req := &mcp.CallToolRequest{ Params: &mcp.CallToolParamsRaw{Arguments: argBytes}, } result, err := handler(context.Background(), req) if err != nil { t.Fatalf("tool %q error: %v", name, err) } if len(result.Content) == 0 { t.Fatalf("tool %q returned no content", name) } tc := result.Content[0].(*mcp.TextContent) var out any if err := json.Unmarshal([]byte(tc.Text), &out); err != nil { // Not JSON — return raw string return tc.Text } return out } func callToolText(t *testing.T, name string, sec secretBackend, args map[string]any) string { t.Helper() handler := findToolHandler(t, nil, name, sec) argBytes, _ := json.Marshal(args) req := &mcp.CallToolRequest{ Params: &mcp.CallToolParamsRaw{Arguments: argBytes}, } result, err := handler(context.Background(), req) if err != nil { t.Fatalf("tool %q error: %v", name, err) } if len(result.Content) == 0 { t.Fatalf("tool %q returned no content", name) } return result.Content[0].(*mcp.TextContent).Text } func TestGetSecret(t *testing.T) { sec := &mockSecretBackend{ data: map[string]string{ "matrix-token": "bot-token-123", "clients/host:hubris/age-key": "AGE-SECRET-KEY", }, } // Get existing key val := callToolText(t, "get_secret", sec, map[string]any{"key": "matrix-token"}) if val != "bot-token-123" { t.Errorf("get_secret = %q, want bot-token-123", val) } // Get missing key errText := callToolText(t, "get_secret", sec, map[string]any{"key": "nonexistent"}) if errText == "" { t.Error("expected error for missing key") } // Missing key arg errText = callToolText(t, "get_secret", sec, map[string]any{}) if errText != "error: key is required" { t.Errorf("missing key error = %q, want error: key is required", errText) } } func TestListSecrets(t *testing.T) { sec := &mockSecretBackend{ data: map[string]string{ "clients/host:hubris/age-key": "val1", "clients/host:strong/age-key": "val2", "shared/matrix-token": "val3", }, } // List all out := callToolJSON(t, "list_secrets", sec, map[string]any{}) keys, ok := out.([]any) if !ok { t.Fatalf("list_secrets returned non-array: %T", out) } if len(keys) != 3 { t.Errorf("list_secrets count = %d, want 3", len(keys)) } // List with prefix filter out = callToolJSON(t, "list_secrets", sec, map[string]any{"path_prefix": "clients/"}) keys, ok = out.([]any) if !ok { t.Fatalf("filtered list returned non-array: %T", out) } if len(keys) != 2 { t.Errorf("filtered list count = %d, want 2", len(keys)) } } func TestSetSecret(t *testing.T) { sec := &mockSecretBackend{ data: map[string]string{}, } // Set a key result := callToolText(t, "set_secret", sec, map[string]any{"key": "test-key", "value": "test-value"}) if result != "secret test-key stored" { t.Errorf("set_secret = %q, want 'secret test-key stored'", result) } // Verify it was stored val, err := sec.Get(context.Background(), "test-key") if err != nil { t.Fatalf("verify get: %v", err) } if val != "test-value" { t.Errorf("stored value = %q, want test-value", val) } // Missing key errText := callToolText(t, "set_secret", sec, map[string]any{}) if errText != "error: key is required" { t.Errorf("missing key error = %q", errText) } // Missing value errText = callToolText(t, "set_secret", sec, map[string]any{"key": "x"}) if errText != "error: value is required" { t.Errorf("missing value error = %q", errText) } } func TestSecretToolsNilBackend(t *testing.T) { // All tools should return a graceful error when no backend is configured errText := callToolText(t, "get_secret", nil, map[string]any{"key": "x"}) if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" { t.Errorf("nil backend get_secret = %q", errText) } errText = callToolText(t, "list_secrets", nil, map[string]any{}) if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" { t.Errorf("nil backend list_secrets = %q", errText) } errText = callToolText(t, "set_secret", nil, map[string]any{"key": "x", "value": "y"}) if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" { t.Errorf("nil backend set_secret = %q", errText) } }