Skip to content

Commit 7c651a9

Browse files
committed
test(source/cloud-sql-mysql): create MCP integration tests
1 parent 69bb59b commit 7c651a9

File tree

4 files changed

+724
-2
lines changed

4 files changed

+724
-2
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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 cloudsqlmysql_test
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"net/http"
22+
"net/http/httptest"
23+
"net/url"
24+
"regexp"
25+
"strings"
26+
"testing"
27+
"time"
28+
29+
"github.com/google/go-cmp/cmp"
30+
"github.com/googleapis/genai-toolbox/internal/testutils"
31+
"github.com/googleapis/genai-toolbox/tests"
32+
"google.golang.org/api/sqladmin/v1"
33+
)
34+
35+
const createInstanceToolTypeMCP = "cloud-sql-mysql-create-instance"
36+
37+
type createInstanceTransportMCP struct {
38+
transport http.RoundTripper
39+
url *url.URL
40+
}
41+
42+
func (t *createInstanceTransportMCP) RoundTrip(req *http.Request) (*http.Response, error) {
43+
if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") {
44+
req.URL.Scheme = t.url.Scheme
45+
req.URL.Host = t.url.Host
46+
}
47+
return t.transport.RoundTrip(req)
48+
}
49+
50+
type masterHandlerMCP struct {
51+
t *testing.T
52+
}
53+
54+
func (h *masterHandlerMCP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
55+
if !strings.Contains(r.UserAgent(), "genai-toolbox/") {
56+
h.t.Errorf("User-Agent header not found")
57+
}
58+
59+
var body sqladmin.DatabaseInstance
60+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
61+
h.t.Fatalf("failed to decode request body: %v", err)
62+
}
63+
64+
instanceName := body.Name
65+
if instanceName == "" {
66+
http.Error(w, "missing instance name", http.StatusBadRequest)
67+
return
68+
}
69+
70+
var expectedBody sqladmin.DatabaseInstance
71+
var response any
72+
var statusCode int
73+
74+
switch instanceName {
75+
case "instance1":
76+
expectedBody = sqladmin.DatabaseInstance{
77+
Project: "p1",
78+
Name: "instance1",
79+
DatabaseVersion: "MYSQL_8_0",
80+
RootPassword: "password123",
81+
Settings: &sqladmin.Settings{
82+
AvailabilityType: "REGIONAL",
83+
Edition: "ENTERPRISE_PLUS",
84+
Tier: "db-perf-optimized-N-8",
85+
DataDiskSizeGb: 250,
86+
DataDiskType: "PD_SSD",
87+
},
88+
}
89+
response = map[string]any{"name": "op1", "status": "PENDING"}
90+
statusCode = http.StatusOK
91+
case "instance2":
92+
expectedBody = sqladmin.DatabaseInstance{
93+
Project: "p2",
94+
Name: "instance2",
95+
DatabaseVersion: "MYSQL_8_4",
96+
RootPassword: "password456",
97+
Settings: &sqladmin.Settings{
98+
AvailabilityType: "ZONAL",
99+
Edition: "ENTERPRISE_PLUS",
100+
Tier: "db-perf-optimized-N-2",
101+
DataDiskSizeGb: 100,
102+
DataDiskType: "PD_SSD",
103+
},
104+
}
105+
response = map[string]any{"name": "op2", "status": "RUNNING"}
106+
statusCode = http.StatusOK
107+
default:
108+
http.Error(w, fmt.Sprintf("unhandled instance name: %s", instanceName), http.StatusInternalServerError)
109+
return
110+
}
111+
112+
if expectedBody.Project != body.Project {
113+
h.t.Errorf("unexpected project: got %q, want %q", body.Project, expectedBody.Project)
114+
}
115+
if expectedBody.Name != body.Name {
116+
h.t.Errorf("unexpected name: got %q, want %q", body.Name, expectedBody.Name)
117+
}
118+
if expectedBody.DatabaseVersion != body.DatabaseVersion {
119+
h.t.Errorf("unexpected databaseVersion: got %q, want %q", body.DatabaseVersion, expectedBody.DatabaseVersion)
120+
}
121+
if expectedBody.RootPassword != body.RootPassword {
122+
h.t.Errorf("unexpected rootPassword: got %q, want %q", body.RootPassword, expectedBody.RootPassword)
123+
}
124+
if diff := cmp.Diff(expectedBody.Settings, body.Settings); diff != "" {
125+
h.t.Errorf("unexpected request body settings (-want +got):\n%s", diff)
126+
}
127+
128+
w.Header().Set("Content-Type", "application/json")
129+
w.WriteHeader(statusCode)
130+
if err := json.NewEncoder(w).Encode(response); err != nil {
131+
http.Error(w, err.Error(), http.StatusInternalServerError)
132+
}
133+
}
134+
135+
func TestCreateInstanceToolEndpointsMCP(t *testing.T) {
136+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
137+
defer cancel()
138+
139+
handler := &masterHandlerMCP{t: t}
140+
server := httptest.NewServer(handler)
141+
defer server.Close()
142+
143+
serverURL, err := url.Parse(server.URL)
144+
if err != nil {
145+
t.Fatalf("failed to parse server URL: %v", err)
146+
}
147+
148+
originalTransport := http.DefaultClient.Transport
149+
if originalTransport == nil {
150+
originalTransport = http.DefaultTransport
151+
}
152+
http.DefaultClient.Transport = &createInstanceTransportMCP{
153+
transport: originalTransport,
154+
url: serverURL,
155+
}
156+
t.Cleanup(func() {
157+
http.DefaultClient.Transport = originalTransport
158+
})
159+
160+
toolsFile := getCreateInstanceToolsConfigMCP()
161+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile)
162+
if err != nil {
163+
t.Fatalf("command initialization returned an error: %v", err)
164+
}
165+
defer cleanup()
166+
167+
waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second)
168+
defer cancelWait()
169+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
170+
if err != nil {
171+
t.Logf("toolbox command logs: \n%s", out)
172+
t.Fatalf("toolbox didn't start successfully: %v", err)
173+
}
174+
175+
tcs := []struct {
176+
name string
177+
toolName string
178+
body string
179+
want string
180+
expectError bool
181+
}{
182+
{
183+
name: "verify successful instance creation with production preset",
184+
toolName: "create-instance-prod",
185+
body: `{"project": "p1", "name": "instance1", "databaseVersion": "MYSQL_8_0", "rootPassword": "password123", "editionPreset": "Production"}`,
186+
want: `{"name":"op1","status":"PENDING"}`,
187+
expectError: false,
188+
},
189+
{
190+
name: "verify successful instance creation with development preset",
191+
toolName: "create-instance-dev",
192+
body: `{"project": "p2", "name": "instance2", "rootPassword": "password456", "editionPreset": "Development"}`,
193+
want: `{"name":"op2","status":"RUNNING"}`,
194+
expectError: false,
195+
},
196+
{
197+
name: "verify missing required parameter returns schema error",
198+
toolName: "create-instance-prod",
199+
body: `{"name": "instance1"}`,
200+
want: `parameter "project" is required`,
201+
expectError: true,
202+
},
203+
}
204+
205+
for _, tc := range tcs {
206+
t.Run(tc.name, func(t *testing.T) {
207+
var args map[string]any
208+
if err := json.Unmarshal([]byte(tc.body), &args); err != nil {
209+
t.Fatalf("failed to unmarshal body: %v", err)
210+
}
211+
212+
statusCode, mcpResp, err := tests.InvokeMCPTool(t, tc.toolName, args, nil)
213+
if err != nil {
214+
t.Fatalf("native error executing %s: %v", tc.toolName, err)
215+
}
216+
217+
if statusCode != http.StatusOK {
218+
t.Fatalf("expected status 200, got %d", statusCode)
219+
}
220+
221+
if tc.expectError {
222+
tests.AssertMCPError(t, mcpResp, tc.want)
223+
} else {
224+
if mcpResp.Result.IsError {
225+
t.Fatalf("expected success, got error result: %v", mcpResp.Result)
226+
}
227+
gotStr := mcpResp.Result.Content[0].Text
228+
var got, want map[string]any
229+
if err := json.Unmarshal([]byte(gotStr), &got); err != nil {
230+
t.Fatalf("failed to unmarshal result: %v", err)
231+
}
232+
if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
233+
t.Fatalf("failed to unmarshal want: %v", err)
234+
}
235+
if diff := cmp.Diff(want, got); diff != "" {
236+
t.Errorf("unexpected result (-want +got):\n%s", diff)
237+
}
238+
}
239+
})
240+
}
241+
}
242+
243+
func getCreateInstanceToolsConfigMCP() map[string]any {
244+
return map[string]any{
245+
"sources": map[string]any{
246+
"my-cloud-sql-source": map[string]any{
247+
"type": "cloud-sql-admin",
248+
},
249+
},
250+
"tools": map[string]any{
251+
"create-instance-prod": map[string]any{
252+
"type": createInstanceToolTypeMCP,
253+
"source": "my-cloud-sql-source",
254+
},
255+
"create-instance-dev": map[string]any{
256+
"type": createInstanceToolTypeMCP,
257+
"source": "my-cloud-sql-source",
258+
},
259+
},
260+
}
261+
}

0 commit comments

Comments
 (0)