Skip to content

Commit 19e1bb2

Browse files
committed
test(alloydb): add MCP integration tests
1 parent 1dd5ab3 commit 19e1bb2

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

tests/alloydb/alloydb_mcp_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package alloydb
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
"regexp"
22+
"testing"
23+
"time"
24+
25+
"github.com/googleapis/genai-toolbox/internal/testutils"
26+
"github.com/googleapis/genai-toolbox/tests"
27+
)
28+
29+
func TestAlloyDBListTools(t *testing.T) {
30+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
31+
defer cancel()
32+
33+
toolsFile := getAlloyDBToolsConfig()
34+
35+
// Start the toolbox server using our NEW helper!
36+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, "--enable-api=false")
37+
if err != nil {
38+
t.Fatalf("command initialization returned an error: %v", err)
39+
}
40+
defer cleanup()
41+
42+
waitCtx, cancelWait := context.WithTimeout(ctx, 20*time.Second)
43+
defer cancelWait()
44+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
45+
if err != nil {
46+
t.Logf("toolbox command logs: \n%s", out)
47+
t.Fatalf("toolbox didn't start successfully: %v", err)
48+
}
49+
50+
// Verify list of tools
51+
expectedTools := []tests.MCPToolManifest{
52+
{
53+
Name: "alloydb-list-clusters",
54+
Description: "Lists all AlloyDB clusters in a given project and location.",
55+
},
56+
{
57+
Name: "alloydb-list-users",
58+
Description: "Lists all AlloyDB users within a specific cluster.",
59+
},
60+
}
61+
62+
tests.RunMCPToolsListMethod(t, expectedTools)
63+
}
64+
65+
func TestAlloyDBCallTool(t *testing.T) {
66+
vars := getAlloyDBVars(t)
67+
68+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
69+
defer cancel()
70+
71+
toolsFile := getAlloyDBToolsConfig()
72+
73+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, "--enable-api=false")
74+
if err != nil {
75+
t.Fatalf("command initialization returned an error: %v", err)
76+
}
77+
defer cleanup()
78+
79+
waitCtx, cancelWait := context.WithTimeout(ctx, 20*time.Second)
80+
defer cancelWait()
81+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
82+
if err != nil {
83+
t.Logf("toolbox command logs: \n%s", out)
84+
t.Fatalf("toolbox didn't start successfully: %v", err)
85+
}
86+
87+
// Test calling "alloydb-list-clusters"
88+
args := map[string]any{
89+
"project": vars["project"],
90+
"location": vars["location"],
91+
}
92+
93+
wantContains := fmt.Sprintf(`"name":"projects/%s/locations/%s/clusters/%s"`, vars["project"], vars["location"], vars["cluster"])
94+
95+
tests.RunMCPCustomToolCallMethod(t, "alloydb-list-clusters", args, wantContains)
96+
97+
// Negative cases from legacy runAlloyDBMCPToolCallMethod
98+
t.Run("MCP Invoke my-fail-tool missing project", func(t *testing.T) {
99+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, "my-fail-tool", map[string]any{"location": vars["location"]}, nil)
100+
if err != nil {
101+
t.Fatalf("native error executing %s: %s", "my-fail-tool", err)
102+
}
103+
if statusCode != http.StatusOK {
104+
t.Fatalf("expected status 200, got %d", statusCode)
105+
}
106+
tests.AssertMCPError(t, mcpResp, `parameter \"project\" is required`)
107+
})
108+
109+
t.Run("MCP Invoke invalid tool", func(t *testing.T) {
110+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, "non-existent-tool", map[string]any{}, nil)
111+
if err != nil {
112+
t.Fatalf("native error executing %s: %s", "non-existent-tool", err)
113+
}
114+
if statusCode != http.StatusOK {
115+
t.Fatalf("expected status 200, got %d", statusCode)
116+
}
117+
tests.AssertMCPError(t, mcpResp, `tool with name \"non-existent-tool\" does not exist`)
118+
})
119+
120+
t.Run("MCP Invoke tool without required parameters", func(t *testing.T) {
121+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, "alloydb-list-clusters", map[string]any{"location": vars["location"]}, nil)
122+
if err != nil {
123+
t.Fatalf("native error executing %s: %s", "alloydb-list-clusters", err)
124+
}
125+
if statusCode != http.StatusOK {
126+
t.Fatalf("expected status 200, got %d", statusCode)
127+
}
128+
tests.AssertMCPError(t, mcpResp, `parameter \"project\" is required`)
129+
})
130+
}
131+
132+
func TestAlloyDBListClusters(t *testing.T) {
133+
vars := getAlloyDBVars(t)
134+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
135+
defer cancel()
136+
137+
toolsFile := getAlloyDBToolsConfig()
138+
139+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, "--enable-api=false")
140+
if err != nil {
141+
t.Fatalf("command initialization returned an error: %v", err)
142+
}
143+
defer cleanup()
144+
145+
waitCtx, cancelWait := context.WithTimeout(ctx, 20*time.Second)
146+
defer cancelWait()
147+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
148+
if err != nil {
149+
t.Logf("toolbox command logs: \n%s", out)
150+
t.Fatalf("toolbox didn't start successfully: %v", err)
151+
}
152+
153+
wantForAllLocations := []string{
154+
fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-ai-nl-testing", vars["project"]),
155+
fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-pg-testing", vars["project"]),
156+
}
157+
158+
t.Run("list clusters for all locations", func(t *testing.T) {
159+
args := map[string]any{"project": vars["project"], "location": "-"}
160+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, "alloydb-list-clusters", args, nil)
161+
if err != nil {
162+
t.Fatalf("native error executing: %s", err)
163+
}
164+
if statusCode != http.StatusOK {
165+
t.Fatalf("expected status 200, got %d", statusCode)
166+
}
167+
if mcpResp.Result.IsError {
168+
t.Fatalf("returned error result: %v", mcpResp.Result)
169+
}
170+
171+
got := mcpResp.Result.Content[0].Text
172+
for _, want := range wantForAllLocations {
173+
if !regexp.MustCompile(want).MatchString(got) {
174+
t.Errorf("Expected substring not found: %q", want)
175+
}
176+
}
177+
})
178+
179+
t.Run("list clusters missing project", func(t *testing.T) {
180+
args := map[string]any{"location": vars["location"]}
181+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, "alloydb-list-clusters", args, nil)
182+
if err != nil {
183+
t.Fatalf("native error executing: %s", err)
184+
}
185+
if statusCode != http.StatusOK {
186+
t.Fatalf("expected status 200, got %d", statusCode)
187+
}
188+
tests.AssertMCPError(t, mcpResp, `parameter \"project\" is required`)
189+
})
190+
}

0 commit comments

Comments
 (0)