|
9 | 9 |
|
10 | 10 | mcpclient "github.com/mark3labs/mcp-go/client" |
11 | 11 | "github.com/mark3labs/mcp-go/mcp" |
| 12 | + |
| 13 | + "github.com/steipete/gogcli/internal/config" |
| 14 | + "github.com/steipete/gogcli/internal/googleapi" |
12 | 15 | ) |
13 | 16 |
|
14 | 17 | func TestMCPEnabledToolsDefaultReadOnly(t *testing.T) { |
@@ -39,6 +42,137 @@ func TestMCPEnabledToolsAllowWriteAndFilter(t *testing.T) { |
39 | 42 | } |
40 | 43 | } |
41 | 44 |
|
| 45 | +func TestMCPPolicyDefaultsToReadOnly(t *testing.T) { |
| 46 | + policy, err := selectMCPPolicy(config.MCPConfig{}, "") |
| 47 | + if err != nil { |
| 48 | + t.Fatalf("selectMCPPolicy: %v", err) |
| 49 | + } |
| 50 | + tools, err := mcpEnabledToolsWithPolicy(McpCmd{}, &RootFlags{}, policy) |
| 51 | + if err != nil { |
| 52 | + t.Fatalf("mcpEnabledToolsWithPolicy: %v", err) |
| 53 | + } |
| 54 | + if !hasMCPTool(tools, "gmail_search") || hasMCPTool(tools, "docs_write") { |
| 55 | + t.Fatalf("unexpected default policy tools: %#v", toolNames(tools)) |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +func TestMCPPolicyAccountReplacesGlobalAndEnablesNarrowWrites(t *testing.T) { |
| 60 | + cfg := config.MCPConfig{ |
| 61 | + MCPPolicy: config.MCPPolicy{AllowTools: []string{"read"}}, |
| 62 | + Accounts: map[string]config.MCPPolicy{ |
| 63 | + " [email protected] ": { AllowTools: [] string{ "docs.*"}, AllowWrite: true}, |
| 64 | + }, |
| 65 | + } |
| 66 | + policy, err := selectMCPPolicy( cfg, "[email protected]") |
| 67 | + if err != nil { |
| 68 | + t.Fatalf("selectMCPPolicy: %v", err) |
| 69 | + } |
| 70 | + tools, err := mcpEnabledToolsWithPolicy(McpCmd{}, &RootFlags{}, policy) |
| 71 | + if err != nil { |
| 72 | + t.Fatalf("mcpEnabledToolsWithPolicy: %v", err) |
| 73 | + } |
| 74 | + if !hasMCPTool(tools, "docs_get") || !hasMCPTool(tools, "docs_write") { |
| 75 | + t.Fatalf("expected configured Docs tools: %#v", toolNames(tools)) |
| 76 | + } |
| 77 | + if hasMCPTool(tools, "gmail_search") { |
| 78 | + t.Fatalf("global policy leaked into account replacement: %#v", toolNames(tools)) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +func TestMCPPolicyRuntimeCanOnlyNarrow(t *testing.T) { |
| 83 | + policy, err := normalizeMCPPolicy(config.MCPPolicy{AllowTools: []string{"docs.*"}, AllowWrite: true}) |
| 84 | + if err != nil { |
| 85 | + t.Fatalf("normalizeMCPPolicy: %v", err) |
| 86 | + } |
| 87 | + tools, err := mcpEnabledToolsWithPolicy(McpCmd{AllowTool: []string{"docs_get"}}, &RootFlags{}, policy) |
| 88 | + if err != nil { |
| 89 | + t.Fatalf("mcpEnabledToolsWithPolicy: %v", err) |
| 90 | + } |
| 91 | + if got := toolNames(tools); len(got) != 1 || got[0] != "docs_get" { |
| 92 | + t.Fatalf("runtime narrowed tools = %#v", got) |
| 93 | + } |
| 94 | + |
| 95 | + _, err = mcpEnabledToolsWithPolicy(McpCmd{AllowWrite: true}, &RootFlags{}, config.MCPPolicy{AllowTools: []string{"read"}}) |
| 96 | + if err == nil || !strings.Contains(err.Error(), "cannot widen") { |
| 97 | + t.Fatalf("allow-write widening error = %v", err) |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +func TestMCPPolicyReadOnlyRootHidesConfiguredWrites(t *testing.T) { |
| 102 | + policy, err := normalizeMCPPolicy(config.MCPPolicy{AllowTools: []string{"docs.*"}, AllowWrite: true}) |
| 103 | + if err != nil { |
| 104 | + t.Fatalf("normalizeMCPPolicy: %v", err) |
| 105 | + } |
| 106 | + tools, err := mcpEnabledToolsWithPolicy(McpCmd{}, &RootFlags{ReadOnly: true}, policy) |
| 107 | + if err != nil { |
| 108 | + t.Fatalf("mcpEnabledToolsWithPolicy: %v", err) |
| 109 | + } |
| 110 | + if !hasMCPTool(tools, "docs_get") || hasMCPTool(tools, "docs_write") { |
| 111 | + t.Fatalf("readonly tools = %#v", toolNames(tools)) |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +func TestMCPPolicyRejectsUnsafeOrUnknownConfig(t *testing.T) { |
| 116 | + for _, policy := range []config.MCPPolicy{ |
| 117 | + {AllowWrite: true}, |
| 118 | + {AllowTools: []string{}}, |
| 119 | + {AllowTools: []string{"not_a_tool"}}, |
| 120 | + } { |
| 121 | + if _, err := normalizeMCPPolicy(policy); err == nil { |
| 122 | + t.Fatalf("expected policy error for %#v", policy) |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + duplicateAccount := " [email protected] " |
| 127 | + _, err := selectMCPPolicy(config.MCPConfig{Accounts: map[string]config.MCPPolicy{ |
| 128 | + |
| 129 | + duplicateAccount: {}, |
| 130 | + |
| 131 | + if err == nil || !strings.Contains(err.Error(), "duplicate") { |
| 132 | + t.Fatalf("duplicate account error = %v", err) |
| 133 | + } |
| 134 | + |
| 135 | + _, err = selectMCPPolicy(config.MCPConfig{ |
| 136 | + Accounts: map[string]config.MCPPolicy{ |
| 137 | + "[email protected]": { AllowTools: [] string{ "read"}}, |
| 138 | + "[email protected]": { AllowTools: [] string{ "not_a_tool"}}, |
| 139 | + }, |
| 140 | + |
| 141 | + if err == nil || !strings. Contains( err. Error(), "[email protected]") || !strings. Contains( err. Error(), "matches no tool") { |
| 142 | + t.Fatalf("unselected account validation error = %v", err) |
| 143 | + } |
| 144 | +} |
| 145 | + |
| 146 | +func TestMCPPolicyAccountResolutionPinsAliasAndRejectsUnverifiableIdentity(t *testing.T) { |
| 147 | + store := config.NewConfigStore(config.Layout{ConfigDir: t.TempDir()}) |
| 148 | + if err := store. Write(config. File{ AccountAliases: map[ string] string{ "personal": "[email protected]"}}); err != nil { |
| 149 | + t.Fatalf("write config: %v", err) |
| 150 | + } |
| 151 | + flags := &RootFlags{ |
| 152 | + Account: "personal", |
| 153 | + configStoreResolver: func() (*config.ConfigStore, error) { |
| 154 | + return store, nil |
| 155 | + }, |
| 156 | + } |
| 157 | + account, err := resolveMCPPolicyAccount(flags) |
| 158 | + if err != nil { |
| 159 | + t.Fatalf("resolveMCPPolicyAccount: %v", err) |
| 160 | + } |
| 161 | + if account != "[email protected]" { |
| 162 | + t.Fatalf("resolved account = %q", account) |
| 163 | + } |
| 164 | + |
| 165 | + for _, unverifiable := range []*RootFlags{ |
| 166 | + { AccessToken: "token", Account: "[email protected]"}, |
| 167 | + { authMode: googleapi. AuthModeADC, Account: "[email protected]"}, |
| 168 | + } { |
| 169 | + account, err := resolveMCPPolicyAccount(unverifiable) |
| 170 | + if err != nil || account != "" { |
| 171 | + t.Fatalf("unverifiable identity resolution = %q, %v", account, err) |
| 172 | + } |
| 173 | + } |
| 174 | +} |
| 175 | + |
42 | 176 | func TestMCPListToolsUsesRuntimeStdout(t *testing.T) { |
43 | 177 | var output bytes.Buffer |
44 | 178 | err := (&McpCmd{ |
|
0 commit comments