e04ed9c211
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
908 lines
29 KiB
Go
908 lines
29 KiB
Go
// Copyright 2025 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cloudgda_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
bigqueryapi "cloud.google.com/go/bigquery"
|
|
geminidataanalytics "cloud.google.com/go/geminidataanalytics/apiv1beta"
|
|
"cloud.google.com/go/geminidataanalytics/apiv1beta/geminidataanalyticspb"
|
|
"github.com/google/uuid"
|
|
"github.com/googleapis/mcp-toolbox/internal/server/mcp/jsonrpc"
|
|
"github.com/googleapis/mcp-toolbox/internal/sources"
|
|
source "github.com/googleapis/mcp-toolbox/internal/sources/cloudgda"
|
|
"github.com/googleapis/mcp-toolbox/internal/testutils"
|
|
"github.com/googleapis/mcp-toolbox/internal/tools/cloudgda"
|
|
"github.com/googleapis/mcp-toolbox/internal/util"
|
|
"github.com/googleapis/mcp-toolbox/tests"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
"google.golang.org/api/googleapi"
|
|
"google.golang.org/api/iterator"
|
|
"google.golang.org/api/option"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
var (
|
|
cloudGdaToolType = "cloud-gemini-data-analytics-query"
|
|
CloudGDASourceType = "cloud-gemini-data-analytics"
|
|
CloudGdaProject = os.Getenv("CLOUD_GDA_PROJECT")
|
|
)
|
|
|
|
func getCloudGDAProject(t *testing.T) string {
|
|
if CloudGdaProject == "" {
|
|
t.Fatal("'CLOUD_GDA_PROJECT' not set")
|
|
}
|
|
return CloudGdaProject
|
|
}
|
|
|
|
type mockDataChatServer struct {
|
|
geminidataanalyticspb.UnimplementedDataChatServiceServer
|
|
t *testing.T
|
|
}
|
|
|
|
func (s *mockDataChatServer) QueryData(ctx context.Context, req *geminidataanalyticspb.QueryDataRequest) (*geminidataanalyticspb.QueryDataResponse, error) {
|
|
if req.Prompt == "" {
|
|
s.t.Errorf("missing prompt")
|
|
return nil, fmt.Errorf("missing prompt")
|
|
}
|
|
|
|
return &geminidataanalyticspb.QueryDataResponse{
|
|
GeneratedQuery: "SELECT * FROM table;",
|
|
NaturalLanguageAnswer: "Here is the answer.",
|
|
}, nil
|
|
}
|
|
|
|
func getCloudGdaToolsConfig() map[string]any {
|
|
return map[string]any{
|
|
"sources": map[string]any{
|
|
"my-gda-source": map[string]any{
|
|
"type": "cloud-gemini-data-analytics",
|
|
"projectId": "test-project",
|
|
},
|
|
},
|
|
"tools": map[string]any{
|
|
"cloud-gda-query": map[string]any{
|
|
"type": cloudGdaToolType,
|
|
"source": "my-gda-source",
|
|
"description": "Test GDA Tool",
|
|
"location": "us-central1",
|
|
"context": map[string]any{
|
|
"datasourceReferences": map[string]any{
|
|
"spannerReference": map[string]any{
|
|
"databaseReference": map[string]any{
|
|
"projectId": "test-project",
|
|
"instanceId": "test-instance",
|
|
"databaseId": "test-db",
|
|
"engine": "GOOGLE_SQL",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestCloudGdaToolEndpoints(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
|
|
// Start a gRPC server
|
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to listen: %v", err)
|
|
}
|
|
s := grpc.NewServer()
|
|
geminidataanalyticspb.RegisterDataChatServiceServer(s, &mockDataChatServer{t: t})
|
|
go func() {
|
|
if err := s.Serve(lis); err != nil {
|
|
// This might happen on strict shutdown, log if unexpected
|
|
t.Logf("server executed: %v", err)
|
|
}
|
|
}()
|
|
defer s.Stop()
|
|
|
|
// Configure toolbox to use the gRPC server
|
|
endpoint := lis.Addr().String()
|
|
|
|
// Override client creation
|
|
origFunc := source.NewDataChatClient
|
|
defer func() {
|
|
source.NewDataChatClient = origFunc
|
|
}()
|
|
|
|
source.NewDataChatClient = func(ctx context.Context, opts ...option.ClientOption) (*geminidataanalytics.DataChatClient, error) {
|
|
opts = append(opts,
|
|
option.WithEndpoint(endpoint),
|
|
option.WithoutAuthentication(),
|
|
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())))
|
|
return origFunc(ctx, opts...)
|
|
}
|
|
|
|
args := []string{"--enable-api"}
|
|
toolsFile := getCloudGdaToolsConfig()
|
|
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
|
if err != nil {
|
|
t.Fatalf("command initialization returned an error: %s", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
|
if err != nil {
|
|
t.Logf("toolbox command logs: \n%s", out)
|
|
t.Fatalf("toolbox didn't start successfully: %s", err)
|
|
}
|
|
|
|
toolName := "cloud-gda-query"
|
|
|
|
// 1. RunToolGetTestByName
|
|
expectedManifest := map[string]any{
|
|
toolName: map[string]any{
|
|
"description": "Test GDA Tool\n\n" + cloudgda.Guidance,
|
|
"parameters": []any{
|
|
map[string]any{
|
|
"name": "query",
|
|
"type": "string",
|
|
"description": "A natural language formulation of a database query.",
|
|
"required": true,
|
|
"authServices": []any{},
|
|
},
|
|
},
|
|
"authRequired": []any{},
|
|
},
|
|
}
|
|
tests.RunToolGetTestByName(t, toolName, expectedManifest)
|
|
|
|
// 2. RunToolInvokeParametersTest
|
|
params := []byte(`{"query": "test question"}`)
|
|
tests.RunToolInvokeParametersTest(t, toolName, params, "\"generated_query\":\"SELECT * FROM table;\"")
|
|
|
|
// 3. Manual MCP Tool Call Test
|
|
// Initialize MCP session
|
|
sessionId := tests.RunInitialize(t, "2024-11-05")
|
|
|
|
// Construct MCP Request
|
|
mcpReq := jsonrpc.JSONRPCRequest{
|
|
Jsonrpc: "2.0",
|
|
Id: "test-mcp-call",
|
|
Request: jsonrpc.Request{
|
|
Method: "tools/call",
|
|
},
|
|
Params: map[string]any{
|
|
"name": toolName,
|
|
"arguments": map[string]any{
|
|
"query": "test question",
|
|
},
|
|
},
|
|
}
|
|
reqBytes, _ := json.Marshal(mcpReq)
|
|
|
|
headers := map[string]string{}
|
|
if sessionId != "" {
|
|
headers["Mcp-Session-Id"] = sessionId
|
|
}
|
|
|
|
// Send Request
|
|
resp, respBody := tests.RunRequest(t, http.MethodPost, "http://127.0.0.1:5000/mcp", bytes.NewBuffer(reqBytes), headers)
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("MCP request failed with status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
// Check Response
|
|
respStr := string(respBody)
|
|
if !strings.Contains(respStr, "SELECT * FROM table;") {
|
|
t.Errorf("MCP response does not contain expected query result: %s", respStr)
|
|
}
|
|
}
|
|
|
|
// Copied over from bigquery_integration_test.go
|
|
func initBigQueryConnection(project string) (*bigqueryapi.Client, error) {
|
|
ctx := context.Background()
|
|
cred, err := google.FindDefaultCredentials(ctx, bigqueryapi.Scope)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", bigqueryapi.Scope, err)
|
|
}
|
|
|
|
client, err := bigqueryapi.NewClient(ctx, project, option.WithCredentials(cred))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create BigQuery client for project %q: %w", project, err)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.Client, createStatement, insertStatement, datasetName string, tableName string) func(*testing.T) {
|
|
// Create dataset
|
|
dataset := client.Dataset(datasetName)
|
|
_, err := dataset.Metadata(ctx)
|
|
|
|
if err != nil {
|
|
apiErr, ok := err.(*googleapi.Error)
|
|
if !ok || apiErr.Code != 404 {
|
|
t.Fatalf("Failed to check dataset %q existence: %v", datasetName, err)
|
|
}
|
|
metadataToCreate := &bigqueryapi.DatasetMetadata{Name: datasetName}
|
|
if err := dataset.Create(ctx, metadataToCreate); err != nil {
|
|
t.Fatalf("Failed to create dataset %q: %v", datasetName, err)
|
|
}
|
|
}
|
|
|
|
// Create table
|
|
createJob, err := client.Query(createStatement).Run(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to start create table job for %s: %v", tableName, err)
|
|
}
|
|
createStatus, err := createJob.Wait(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to wait for create table job for %s: %v", tableName, err)
|
|
}
|
|
if err := createStatus.Err(); err != nil {
|
|
t.Fatalf("Create table job for %s failed: %v", tableName, err)
|
|
}
|
|
|
|
if insertStatement != "" {
|
|
// Insert test data
|
|
insertQuery := client.Query(insertStatement)
|
|
insertJob, err := insertQuery.Run(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to start insert job for %s: %v", tableName, err)
|
|
}
|
|
insertStatus, err := insertJob.Wait(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to wait for insert job for %s: %v", tableName, err)
|
|
}
|
|
if err := insertStatus.Err(); err != nil {
|
|
t.Fatalf("Insert job for %s failed: %v", tableName, err)
|
|
}
|
|
}
|
|
|
|
return func(t *testing.T) {
|
|
// tear down table
|
|
dropSQL := fmt.Sprintf("drop table %s", tableName)
|
|
dropJob, err := client.Query(dropSQL).Run(ctx)
|
|
if err != nil {
|
|
t.Errorf("Failed to start drop table job for %s: %v", tableName, err)
|
|
return
|
|
}
|
|
dropStatus, err := dropJob.Wait(ctx)
|
|
if err != nil {
|
|
t.Errorf("Failed to wait for drop table job for %s: %v", tableName, err)
|
|
return
|
|
}
|
|
if err := dropStatus.Err(); err != nil {
|
|
t.Errorf("Error dropping table %s: %v", tableName, err)
|
|
}
|
|
|
|
// tear down dataset
|
|
datasetToTeardown := client.Dataset(datasetName)
|
|
tablesIterator := datasetToTeardown.Tables(ctx)
|
|
_, err = tablesIterator.Next()
|
|
|
|
if err == iterator.Done {
|
|
if err := datasetToTeardown.Delete(ctx); err != nil {
|
|
t.Errorf("Failed to delete dataset %s: %v", datasetName, err)
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Failed to list tables in dataset %s to check emptiness: %v.", datasetName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupDataAgent(t *testing.T, ctx context.Context, projectID, datasetID, tableID, dataAgentDisplayName string) (string, func(*testing.T)) {
|
|
t.Logf("Setting up data agent with ProjectID: %q, DatasetID: %q, TableID: %q, DisplayName: %q", projectID, datasetID, tableID, dataAgentDisplayName)
|
|
|
|
accessToken, err := sources.GetIAMAccessToken(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get access token: %v", err)
|
|
}
|
|
|
|
dataAgentId := "test" + strings.ReplaceAll(uuid.New().String(), "-", "")
|
|
parent := fmt.Sprintf("projects/%s/locations/global", projectID)
|
|
url := fmt.Sprintf("%s/v1beta/%s/dataAgents?dataAgentId=%s", util.GetGDAEndpoint(), parent, dataAgentId)
|
|
|
|
requestBody := map[string]any{
|
|
"displayName": dataAgentDisplayName,
|
|
"dataAnalyticsAgent": map[string]any{
|
|
"publishedContext": map[string]any{
|
|
"datasourceReferences": map[string]any{
|
|
"bq": map[string]any{
|
|
"tableReferences": []map[string]string{
|
|
{
|
|
"projectId": projectID,
|
|
"datasetId": datasetID,
|
|
"tableId": tableID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal create data agent request: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bodyBytes))
|
|
if err != nil {
|
|
t.Fatalf("failed to create request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken})
|
|
client, err := util.NewGDAClient(ctx, option.WithTokenSource(tokenSource))
|
|
if err != nil {
|
|
t.Fatalf("failed to create GDA client: %v", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("failed to create data agent: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response body: %v", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("failed to create data agent, status: %d, body: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var op map[string]any
|
|
if err := json.Unmarshal(respBody, &op); err != nil {
|
|
t.Fatalf("failed to unmarshal operation: %v", err)
|
|
}
|
|
|
|
opName, ok := op["name"].(string)
|
|
if !ok {
|
|
t.Fatalf("operation response missing name: %s", string(respBody))
|
|
}
|
|
|
|
// Poll for operation completion
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
timeout := time.After(60 * time.Second)
|
|
|
|
done := false
|
|
for !done {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatalf("context cancelled while waiting for data agent creation")
|
|
case <-timeout:
|
|
t.Fatalf("timed out waiting for data agent creation")
|
|
case <-ticker.C:
|
|
opUrl := fmt.Sprintf("%s/v1beta/%s", util.GetGDAEndpoint(), opName)
|
|
opReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, opUrl, nil)
|
|
opResp, err := client.Do(opReq)
|
|
if err != nil {
|
|
t.Logf("failed to poll operation: %v", err)
|
|
continue
|
|
}
|
|
opRespBody, _ := io.ReadAll(opResp.Body)
|
|
opResp.Body.Close()
|
|
|
|
var pollOp map[string]any
|
|
if err := json.Unmarshal(opRespBody, &pollOp); err != nil {
|
|
t.Logf("failed to unmarshal polling response: %v", err)
|
|
continue
|
|
}
|
|
|
|
if d, ok := pollOp["done"].(bool); ok && d {
|
|
if errVal, ok := pollOp["error"]; ok && errVal != nil {
|
|
t.Fatalf("data agent creation failed: %v", errVal)
|
|
}
|
|
done = true
|
|
}
|
|
}
|
|
}
|
|
|
|
teardown := func(t *testing.T) {
|
|
agentName := fmt.Sprintf("%s/dataAgents/%s", parent, dataAgentId)
|
|
deleteUrl := fmt.Sprintf("%s/v1beta/%s", util.GetGDAEndpoint(), agentName)
|
|
delReq, _ := http.NewRequest(http.MethodDelete, deleteUrl, nil)
|
|
delResp, err := client.Do(delReq)
|
|
if err != nil {
|
|
t.Errorf("failed to delete data agent %s: %v", agentName, err)
|
|
return
|
|
}
|
|
defer delResp.Body.Close()
|
|
if delResp.StatusCode != http.StatusOK {
|
|
t.Errorf("failed to delete data agent %s, status: %d", agentName, delResp.StatusCode)
|
|
}
|
|
}
|
|
|
|
return dataAgentId, teardown
|
|
}
|
|
|
|
func TestCloudGDAConservationalAnalyticsTools(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
|
|
projectID := getCloudGDAProject(t)
|
|
client, err := initBigQueryConnection(projectID)
|
|
if err != nil {
|
|
t.Fatalf("unable to create BigQuery client: %s", err)
|
|
}
|
|
|
|
// Setup dataset and table for Data Agent
|
|
datasetName := fmt.Sprintf("data_agent_test_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
|
|
tableName := "test_table"
|
|
tableNameParam := fmt.Sprintf("`%s.%s.%s`", projectID, datasetName, tableName)
|
|
|
|
createTableStmt := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING)", tableNameParam)
|
|
teardownTable := setupBigQueryTable(t, ctx, client, createTableStmt, "", datasetName, tableNameParam)
|
|
defer teardownTable(t)
|
|
|
|
// Create Data Agent
|
|
dataAgentDisplayName := fmt.Sprintf("test-agent-%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
|
|
dataAgentID, teardownDataAgent := setupDataAgent(t, ctx, projectID, datasetName, tableName, dataAgentDisplayName)
|
|
defer teardownDataAgent(t)
|
|
|
|
// Configure tools with cloud-gemini-data-analytics source
|
|
toolsFile := map[string]any{
|
|
"sources": map[string]any{
|
|
"my-instance": map[string]any{
|
|
"type": "cloud-gemini-data-analytics",
|
|
"projectId": projectID,
|
|
},
|
|
"my-client-auth-source": map[string]any{
|
|
"type": "cloud-gemini-data-analytics",
|
|
"projectId": projectID,
|
|
"useClientOAuth": true,
|
|
},
|
|
},
|
|
"authServices": map[string]any{
|
|
"my-google-auth": map[string]any{
|
|
"kind": "google",
|
|
"clientId": tests.ClientId,
|
|
},
|
|
},
|
|
"tools": map[string]any{
|
|
"my-list-accessible-data-agents-tool": map[string]any{
|
|
"type": "conversational-analytics-list-accessible-data-agents",
|
|
"source": "my-instance",
|
|
"description": "Tool to list data agents.",
|
|
},
|
|
"my-auth-list-accessible-data-agents-tool": map[string]any{
|
|
"type": "conversational-analytics-list-accessible-data-agents",
|
|
"source": "my-instance",
|
|
"description": "Tool to list data agents with auth.",
|
|
"authRequired": []string{"my-google-auth"},
|
|
},
|
|
"my-client-auth-list-accessible-data-agents-tool": map[string]any{
|
|
"type": "conversational-analytics-list-accessible-data-agents",
|
|
"source": "my-client-auth-source",
|
|
"description": "Tool to list data agents with client auth.",
|
|
},
|
|
"my-get-data-agent-info-tool": map[string]any{
|
|
"type": "conversational-analytics-get-data-agent-info",
|
|
"source": "my-instance",
|
|
"description": "Tool to get data agent info.",
|
|
},
|
|
"my-auth-get-data-agent-info-tool": map[string]any{
|
|
"type": "conversational-analytics-get-data-agent-info",
|
|
"source": "my-instance",
|
|
"description": "Tool to get data agent info with auth.",
|
|
"authRequired": []string{"my-google-auth"},
|
|
},
|
|
"my-client-auth-get-data-agent-info-tool": map[string]any{
|
|
"type": "conversational-analytics-get-data-agent-info",
|
|
"source": "my-client-auth-source",
|
|
"description": "Tool to get data agent info with client auth.",
|
|
},
|
|
"my-ask-data-agent-tool": map[string]any{
|
|
"type": "conversational-analytics-ask-data-agent",
|
|
"source": "my-instance",
|
|
"description": "Tool to ask data agent.",
|
|
},
|
|
"my-auth-ask-data-agent-tool": map[string]any{
|
|
"type": "conversational-analytics-ask-data-agent",
|
|
"source": "my-instance",
|
|
"description": "Tool to ask data agent with auth.",
|
|
"authRequired": []string{"my-google-auth"},
|
|
},
|
|
"my-client-auth-ask-data-agent-tool": map[string]any{
|
|
"type": "conversational-analytics-ask-data-agent",
|
|
"source": "my-client-auth-source",
|
|
"description": "Tool to ask data agent with client auth.",
|
|
},
|
|
},
|
|
}
|
|
|
|
args := []string{"--enable-api"}
|
|
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
|
if err != nil {
|
|
t.Fatalf("command initialization returned an error: %s", err)
|
|
}
|
|
defer cleanup()
|
|
|
|
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
|
if err != nil {
|
|
t.Logf("toolbox command logs: \n%s", out)
|
|
t.Fatalf("toolbox didn't start successfully: %s", err)
|
|
}
|
|
|
|
runListAccessibleDataAgentsInvokeTest(t, dataAgentDisplayName)
|
|
runGetDataAgentInfoInvokeTest(t, dataAgentID, dataAgentDisplayName)
|
|
runAskDataAgentInvokeTest(t, dataAgentID)
|
|
}
|
|
|
|
func runListAccessibleDataAgentsInvokeTest(t *testing.T, dataAgentDisplayName string) {
|
|
idToken, err := tests.GetGoogleIdToken(t)
|
|
if err != nil {
|
|
t.Fatalf("error getting Google ID token: %s", err)
|
|
}
|
|
|
|
accessToken, err := sources.GetIAMAccessToken(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("error getting access token from ADC: %s", err)
|
|
}
|
|
accessToken = "Bearer " + accessToken
|
|
|
|
invokeTcs := []struct {
|
|
name string
|
|
api string
|
|
requestHeader map[string]string
|
|
requestBody io.Reader
|
|
want string
|
|
isErr bool
|
|
}{
|
|
{
|
|
name: "invoke my-list-accessible-data-agents-tool",
|
|
api: "http://127.0.0.1:5000/api/tool/my-list-accessible-data-agents-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-list-accessible-data-agents-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-list-accessible-data-agents-tool/invoke",
|
|
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-list-accessible-data-agents-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-list-accessible-data-agents-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
isErr: true,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-list-accessible-data-agents-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-list-accessible-data-agents-tool/invoke",
|
|
requestHeader: map[string]string{"Authorization": accessToken},
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-list-accessible-data-agents-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-list-accessible-data-agents-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
isErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range invokeTcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
|
|
if err != nil {
|
|
t.Fatalf("unable to create request: %s", err)
|
|
}
|
|
req.Header.Add("Content-type", "application/json")
|
|
for k, v := range tc.requestHeader {
|
|
req.Header.Add(k, v)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("unable to send request: %s", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if tc.isErr {
|
|
return
|
|
}
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("error reading response body: %v", err)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
|
t.Fatalf("error parsing response body")
|
|
}
|
|
|
|
got, ok := body["result"].(string)
|
|
if !ok {
|
|
t.Fatalf("unable to find result in response body")
|
|
}
|
|
|
|
if !strings.Contains(got, tc.want) {
|
|
t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func runGetDataAgentInfoInvokeTest(t *testing.T, dataAgentName, dataAgentDisplayName string) {
|
|
idToken, err := tests.GetGoogleIdToken(t)
|
|
if err != nil {
|
|
t.Fatalf("error getting Google ID token: %s", err)
|
|
}
|
|
|
|
accessToken, err := sources.GetIAMAccessToken(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("error getting access token from ADC: %s", err)
|
|
}
|
|
accessToken = "Bearer " + accessToken
|
|
|
|
invokeTcs := []struct {
|
|
name string
|
|
api string
|
|
requestHeader map[string]string
|
|
requestBody io.Reader
|
|
want string
|
|
isErr bool
|
|
}{
|
|
{
|
|
name: "invoke my-get-data-agent-info-tool",
|
|
api: "http://127.0.0.1:5000/api/tool/my-get-data-agent-info-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"data_agent_id": "%s"}`, dataAgentName))),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-get-data-agent-info-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-get-data-agent-info-tool/invoke",
|
|
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
|
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"data_agent_id": "%s"}`, dataAgentName))),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-get-data-agent-info-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-get-data-agent-info-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"data_agent_id": "%s"}`, dataAgentName))),
|
|
isErr: true,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-get-data-agent-info-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-get-data-agent-info-tool/invoke",
|
|
requestHeader: map[string]string{"Authorization": accessToken},
|
|
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"data_agent_id": "%s"}`, dataAgentName))),
|
|
want: dataAgentDisplayName,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-get-data-agent-info-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-get-data-agent-info-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"data_agent_id": "%s"}`, dataAgentName))),
|
|
isErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range invokeTcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
|
|
if err != nil {
|
|
t.Fatalf("unable to create request: %s", err)
|
|
}
|
|
req.Header.Add("Content-type", "application/json")
|
|
for k, v := range tc.requestHeader {
|
|
req.Header.Add(k, v)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("unable to send request: %s", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if tc.isErr {
|
|
return
|
|
}
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("error parsing response body")
|
|
}
|
|
|
|
got, ok := body["result"].(string)
|
|
if !ok {
|
|
t.Fatalf("unable to find result in response body")
|
|
}
|
|
|
|
if !strings.Contains(got, tc.want) {
|
|
t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func runAskDataAgentInvokeTest(t *testing.T, dataAgentID string) {
|
|
const maxRetries = 3
|
|
const requestTimeout = 340 * time.Second
|
|
|
|
idToken, err := tests.GetGoogleIdToken(t)
|
|
if err != nil {
|
|
t.Fatalf("error getting Google ID token: %s", err)
|
|
}
|
|
|
|
accessToken, err := sources.GetIAMAccessToken(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("error getting access token from ADC: %s", err)
|
|
}
|
|
accessToken = "Bearer " + accessToken
|
|
|
|
dataAgentWant := `FINAL_RESPONSE`
|
|
|
|
invokeTcs := []struct {
|
|
name string
|
|
api string
|
|
requestHeader map[string]string
|
|
requestBody string
|
|
want string
|
|
isErr bool
|
|
}{
|
|
{
|
|
name: "invoke my-ask-data-agent-tool",
|
|
api: "http://127.0.0.1:5000/api/tool/my-ask-data-agent-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: fmt.Sprintf(`{"user_query_with_context": "What are the names in the table?", "data_agent_id": "%s"}`, dataAgentID),
|
|
want: dataAgentWant,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-ask-data-agent-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-ask-data-agent-tool/invoke",
|
|
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
|
requestBody: fmt.Sprintf(`{"user_query_with_context": "What are the names in the table?", "data_agent_id": "%s"}`, dataAgentID),
|
|
want: dataAgentWant,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-auth-ask-data-agent-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-auth-ask-data-agent-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: fmt.Sprintf(`{"user_query_with_context": "What are the names in the table?", "data_agent_id": "%s"}`, dataAgentID),
|
|
isErr: true,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-ask-data-agent-tool with auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-ask-data-agent-tool/invoke",
|
|
requestHeader: map[string]string{"Authorization": accessToken},
|
|
requestBody: fmt.Sprintf(`{"user_query_with_context": "What are the names in the table?", "data_agent_id": "%s"}`, dataAgentID),
|
|
want: dataAgentWant,
|
|
isErr: false,
|
|
},
|
|
{
|
|
name: "invoke my-client-auth-ask-data-agent-tool without auth token",
|
|
api: "http://127.0.0.1:5000/api/tool/my-client-auth-ask-data-agent-tool/invoke",
|
|
requestHeader: map[string]string{},
|
|
requestBody: fmt.Sprintf(`{"user_query_with_context": "What are the names in the table?", "data_agent_id": "%s"}`, dataAgentID),
|
|
isErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range invokeTcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var resp *http.Response
|
|
var err error
|
|
bodyBytes := []byte(tc.requestBody)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, tc.api, nil)
|
|
if err != nil {
|
|
t.Fatalf("unable to create request: %s", err)
|
|
}
|
|
req.Header.Set("Content-type", "application/json")
|
|
for k, v := range tc.requestHeader {
|
|
req.Header.Add(k, v)
|
|
}
|
|
|
|
for i := 0; i < maxRetries; i++ {
|
|
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
defer cancel()
|
|
|
|
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
req.GetBody = func() (io.ReadCloser, error) {
|
|
return io.NopCloser(bytes.NewReader(bodyBytes)), nil
|
|
}
|
|
reqWithCtx := req.WithContext(ctx)
|
|
|
|
resp, err = http.DefaultClient.Do(reqWithCtx)
|
|
if err != nil {
|
|
// Retry on time out.
|
|
if os.IsTimeout(err) {
|
|
t.Logf("Request timed out (attempt %d/%d), retrying...", i+1, maxRetries)
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
t.Fatalf("unable to send request: %s", err)
|
|
}
|
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
|
t.Logf("Received 503 Service Unavailable (attempt %d/%d), retrying...", i+1, maxRetries)
|
|
time.Sleep(15 * time.Second)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("Request failed after %d retries: %v", maxRetries, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
if tc.isErr {
|
|
return
|
|
}
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err = json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("error parsing response body: %v", err)
|
|
}
|
|
|
|
got, ok := body["result"].(string)
|
|
if !ok {
|
|
t.Fatalf("unable to find result in response body")
|
|
}
|
|
|
|
wantPattern := regexp.MustCompile(tc.want)
|
|
if !wantPattern.MatchString(got) {
|
|
t.Fatalf("response did not match the expected pattern.\nFull response:\n%s", got)
|
|
}
|
|
})
|
|
}
|
|
}
|