e04ed9c211
CF: Deploy Dev Docs / deploy (push) Waiting to run
Sync Labels / build (push) Waiting to run
tests / unit tests (macos-latest) (push) Waiting to run
tests / unit tests (ubuntu-latest) (push) Waiting to run
tests / unit tests (windows-latest) (push) Waiting to run
584 lines
18 KiB
Go
584 lines
18 KiB
Go
// Copyright 2026 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 cloudsqlpg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/googleapis/mcp-toolbox/internal/testutils"
|
|
"github.com/googleapis/mcp-toolbox/tests"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func createPostgresExtension(t *testing.T, ctx context.Context, pool *pgxpool.Pool, extensionName string) func() {
|
|
createExtensionCmd := fmt.Sprintf("CREATE EXTENSION IF NOT EXISTS %s", extensionName)
|
|
_, err := pool.Exec(ctx, createExtensionCmd)
|
|
if err != nil {
|
|
t.Fatalf("failed to create extension: %v", err)
|
|
}
|
|
return func() {
|
|
dropExtensionCmd := fmt.Sprintf("DROP EXTENSION IF EXISTS %s", extensionName)
|
|
_, err := pool.Exec(ctx, dropExtensionCmd)
|
|
if err != nil {
|
|
t.Fatalf("failed to drop extension: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// setupVectorAssistTable prepares the database extensions and test data needed
|
|
// to test the definespec, modifyspec, applyspec, and generatequery tools.
|
|
func setupVectorAssistTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool) (string, func(t *testing.T), func()) {
|
|
// Install necessary extensions for VectorAssist
|
|
dropExtensionFunc := createPostgresExtension(t, ctx, pool, "vector_assist")
|
|
|
|
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
|
uniqueID = uniqueID[:10]
|
|
tableName := "vector_assist_test_" + uniqueID
|
|
|
|
// Create a table with vector data for defining/modifying/applying specs
|
|
createStmt := fmt.Sprintf(`
|
|
CREATE TABLE %s (
|
|
name TEXT,
|
|
category TEXT,
|
|
content TEXT,
|
|
embedding vector(3)
|
|
);
|
|
`, tableName)
|
|
|
|
_, err := pool.Exec(ctx, createStmt)
|
|
if err != nil {
|
|
t.Fatalf("failed to create vector assist test table: %v", err)
|
|
}
|
|
|
|
// Insert sample data to generate queries against
|
|
insertDataStmt := fmt.Sprintf(`
|
|
INSERT INTO %s (name, category, content, embedding)
|
|
VALUES
|
|
('Item 1', 'Document', 'Sample text document about AI', array_fill(0.1, ARRAY[3])::vector),
|
|
('Item 2', 'Document', 'Sample text document about databases', array_fill(0.2, ARRAY[3])::vector);
|
|
`, tableName)
|
|
|
|
_, err = pool.Exec(ctx, insertDataStmt)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert data into vector assist table: %v", err)
|
|
}
|
|
|
|
// Return teardown function
|
|
teardown := func(t *testing.T) {
|
|
_, err := pool.Exec(context.Background(), fmt.Sprintf("DROP TABLE IF EXISTS %s;", tableName))
|
|
if err != nil {
|
|
t.Errorf("failed to drop vector assist table %s: %v", tableName, err)
|
|
}
|
|
}
|
|
|
|
return tableName, teardown, dropExtensionFunc
|
|
}
|
|
|
|
// TODO: Remove the test from this file and follow the existing test pattern
|
|
// by calling the tests from cloudsqlpg_integration_test.go
|
|
func TestVectorAssistIntegration(t *testing.T) {
|
|
// temporarily skip vector assist test since "Vector assist is temporarily
|
|
// disabled for all Cloud SQL for PostgreSQL instances"
|
|
// ref: https://docs.cloud.google.com/sql/docs/postgres/vector-assist-overview
|
|
t.Skip("Temporarily skipping vector assist test. Google had temporarily disabled vector assist for all CloudSQL for PostgreSQL instances.")
|
|
|
|
sourceConfig := getCloudSQLPgVars(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
|
|
args := []string{"--enable-api"}
|
|
|
|
pool, err := initCloudSQLPgConnectionPool(CloudSQLPostgresProject, CloudSQLPostgresRegion, CloudSQLPostgresInstance, "public", CloudSQLPostgresUser, CloudSQLPostgresPass, CloudSQLPostgresDatabase)
|
|
if err != nil {
|
|
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
|
|
}
|
|
|
|
// Generate a unique ID
|
|
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
|
|
|
// This will execute after all tool tests complete (success, fail, or t.Fatal)
|
|
t.Cleanup(func() {
|
|
tests.CleanupPostgresTables(t, context.Background(), pool, uniqueID)
|
|
})
|
|
|
|
//Create table names using the UUID
|
|
tableNameParam := "param_table_" + uniqueID
|
|
tableNameAuth := "auth_table_" + uniqueID
|
|
|
|
// set up data for param tool
|
|
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)
|
|
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createParamTableStmt, insertParamTableStmt, tableNameParam, paramTestParams)
|
|
defer teardownTable1(t)
|
|
|
|
// set up data for auth tool
|
|
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
|
|
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createAuthTableStmt, insertAuthTableStmt, tableNameAuth, authTestParams)
|
|
defer teardownTable2(t)
|
|
|
|
// // Set up data for vector assist tools
|
|
vectorAssistTableName, teardownVectorAssistTable, dropExtension := setupVectorAssistTable(t, ctx, pool)
|
|
defer teardownVectorAssistTable(t)
|
|
defer dropExtension()
|
|
|
|
// Write config into a file and pass it to command
|
|
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLPostgresToolType, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt)
|
|
toolsFile = tests.AddExecuteSqlConfig(t, toolsFile, "postgres-execute-sql")
|
|
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
|
|
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLPostgresToolType, tmplSelectCombined, tmplSelectFilterCombined, "")
|
|
|
|
// Add vector assist tools to the configuration
|
|
toolsFile = AddVectorAssistConfig(t, toolsFile, "my-instance")
|
|
|
|
toolsFile = tests.AddPostgresPrebuiltConfig(t, toolsFile)
|
|
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)
|
|
}
|
|
|
|
// Run vectorassist tool tests
|
|
specID := "va_spec_001"
|
|
RunVectorAssistDefineSpecToolInvokeTest(t, ctx, pool, vectorAssistTableName, specID)
|
|
RunVectorAssistModifySpecToolInvokeTest(t, ctx, pool, specID)
|
|
RunVectorAssistApplySpecToolInvokeTest(t, ctx, pool, specID)
|
|
RunVectorAssistGenerateQueryToolInvokeTest(t, ctx, pool, specID)
|
|
RunVectorAssistImproveQueryRecallToolInvokeTest(t, ctx, pool, vectorAssistTableName)
|
|
RunVectorAssistListSpecsToolInvokeTest(t, ctx, pool, vectorAssistTableName)
|
|
RunVectorAssistGetSpecToolInvokeTest(t, ctx, pool, specID)
|
|
RunVectorAssistDeleteSpecToolInvokeTest(t, ctx, pool, specID)
|
|
}
|
|
|
|
// AddVectorAssistConfig appends the vector assist tool configurations to the given tools file.
|
|
func AddVectorAssistConfig(t *testing.T, config map[string]any, sourceName string) map[string]any {
|
|
tools, ok := config["tools"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("unable to get tools from config")
|
|
}
|
|
|
|
tools["define_spec"] = map[string]any{
|
|
"type": "vector-assist-define-spec",
|
|
"source": sourceName,
|
|
}
|
|
tools["modify_spec"] = map[string]any{
|
|
"type": "vector-assist-modify-spec",
|
|
"source": sourceName,
|
|
}
|
|
tools["apply_spec"] = map[string]any{
|
|
"type": "vector-assist-apply-spec",
|
|
"source": sourceName,
|
|
}
|
|
tools["generate_query"] = map[string]any{
|
|
"type": "vector-assist-generate-query",
|
|
"source": sourceName,
|
|
}
|
|
tools["improve_query_recall"] = map[string]any{
|
|
"type": "vector-assist-improve-query-recall",
|
|
"source": sourceName,
|
|
}
|
|
tools["list_specs"] = map[string]any{
|
|
"type": "vector-assist-list-specs",
|
|
"source": sourceName,
|
|
}
|
|
tools["get_spec"] = map[string]any{
|
|
"type": "vector-assist-get-spec",
|
|
"source": sourceName,
|
|
}
|
|
tools["delete_spec"] = map[string]any{
|
|
"type": "vector-assist-delete-spec",
|
|
"source": sourceName,
|
|
}
|
|
config["tools"] = tools
|
|
return config
|
|
}
|
|
|
|
func RunVectorAssistImproveQueryRecallToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, tableName string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"schema_name": "public",
|
|
"table_name": "%s",
|
|
"vector_column_name": "embedding",
|
|
"index_name": "%s_embedding_idx",
|
|
"top_k": 10,
|
|
"target_recall": 0.95,
|
|
"distance_func": "cosine"
|
|
}`,
|
|
tableName, tableName)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "improve query recall valid parameters",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/improve_query_recall/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`ef_search`,
|
|
},
|
|
},
|
|
{
|
|
name: "improve query recall missing required table_name",
|
|
requestBody: bytes.NewBuffer([]byte(`{"schema_name": "public"}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/improve_query_recall/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistListSpecsToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, tableName string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"table_name": "%s",
|
|
"column_name": "content"
|
|
}`,
|
|
tableName)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "list specs valid parameters",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/list_specs/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`va_spec_001`,
|
|
},
|
|
},
|
|
{
|
|
name: "list specs missing required table_name",
|
|
requestBody: bytes.NewBuffer([]byte(`{"column_name": "content"}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/list_specs/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistGetSpecToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, specID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"spec_id": "%s"
|
|
}`,
|
|
specID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "get spec valid parameters",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/get_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
specID,
|
|
},
|
|
},
|
|
{
|
|
name: "get spec missing spec_id",
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/get_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistDeleteSpecToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, specID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"spec_id": "%s"
|
|
}`,
|
|
specID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "delete spec valid parameters",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/delete_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`delete_spec`,
|
|
},
|
|
},
|
|
{
|
|
name: "delete spec missing spec_id",
|
|
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/delete_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistDefineSpecToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, tableName string, specID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"table_name": "%s",
|
|
"schema_name": "public",
|
|
"spec_id": "%s",
|
|
"vector_column_name": "embedding",
|
|
"text_column_name": "content",
|
|
"vector_index_type": "hnsw",
|
|
"embeddings_available": true,
|
|
"num_vectors": 2,
|
|
"dimensionality": 3,
|
|
"embedding_model": "textembedding-gecko",
|
|
"prefilter_column_names": ["category"],
|
|
"distance_func": "cosine",
|
|
"quantization": "halfvec",
|
|
"memory_budget_kb": 1024,
|
|
"target_recall": 0.95,
|
|
"target_top_k": 10,
|
|
"tune_vector_index": true
|
|
}`, tableName, specID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "invoke define_spec with all valid parameters",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/define_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
// Check for key identifiers instead of the entire JSON string
|
|
wantContains: []string{
|
|
`"vector_spec_id":"va_spec_001"`,
|
|
fmt.Sprintf(`"table_name":"%s"`, tableName),
|
|
`"recommendation_id"`, // Ensure a recommendation was generated
|
|
},
|
|
},
|
|
{
|
|
name: "invoke define_spec with missing required table_name",
|
|
requestBody: bytes.NewBuffer([]byte(`{"schema_name": "public", "spec_id": "va_spec_002"}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/define_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistModifySpecToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, specID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"spec_id": "%s",
|
|
"memory_budget_kb": 2048,
|
|
"target_recall": 0.99
|
|
}`, specID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "modify existing spec with new constraints",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/modify_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"recommendation_id"`,
|
|
},
|
|
},
|
|
{
|
|
name: "modify existing spec without required spec id",
|
|
requestBody: bytes.NewBuffer([]byte(`{"target_recall": 0.99}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/modify_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistApplySpecToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, recommendationID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"spec_id": "%s"
|
|
}`, recommendationID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "apply recommendation to database",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/apply_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`{"apply_spec":true}`,
|
|
},
|
|
},
|
|
{
|
|
name: "apply recommendation to database without spec id",
|
|
requestBody: bytes.NewBuffer([]byte(`{"schema_name": "public"}`)),
|
|
api: "http://127.0.0.1:5000/api/tool/apply_spec/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"error"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func RunVectorAssistGenerateQueryToolInvokeTest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, specID string) {
|
|
validPayload := fmt.Sprintf(`{
|
|
"spec_id": "%s",
|
|
"search_text": "What is the capital of France?",
|
|
"top_k": 5
|
|
}`, specID)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}{
|
|
{
|
|
name: "generate SQL for vector search",
|
|
requestBody: bytes.NewBuffer([]byte(validPayload)),
|
|
api: "http://127.0.0.1:5000/api/tool/generate_query/invoke",
|
|
wantStatusCode: http.StatusOK,
|
|
wantContains: []string{
|
|
`"generate_query"`,
|
|
`LIMIT 5`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
runVectorAssistToolInvokeTest(t, tc)
|
|
}
|
|
}
|
|
|
|
func runVectorAssistToolInvokeTest(t *testing.T, tc struct {
|
|
name string
|
|
requestBody io.Reader
|
|
api string
|
|
wantStatusCode int
|
|
wantContains []string
|
|
}) {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp, body := tests.RunRequest(t, http.MethodPost, tc.api, tc.requestBody, nil)
|
|
|
|
if resp.StatusCode != tc.wantStatusCode {
|
|
t.Fatalf("tool %s: wrong status code: got %d, want %d, body: %s", tc.api, resp.StatusCode, tc.wantStatusCode, string(body))
|
|
}
|
|
|
|
if tc.wantStatusCode != http.StatusOK {
|
|
return
|
|
}
|
|
|
|
// Unmarshal the standard response wrapper
|
|
var bodyWrapper struct {
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(body, &bodyWrapper); err != nil {
|
|
t.Fatalf("error decoding response wrapper: %v", err)
|
|
}
|
|
|
|
// Handle the double-unmarshal logic for stringified results
|
|
var resultString string
|
|
if err := json.Unmarshal(bodyWrapper.Result, &resultString); err != nil {
|
|
resultString = string(bodyWrapper.Result)
|
|
}
|
|
|
|
// Verification loop
|
|
for _, expectedSubstr := range tc.wantContains {
|
|
if !strings.Contains(resultString, expectedSubstr) {
|
|
t.Errorf("Expected result to contain %q, but it did not.\nFull result: %s", expectedSubstr, resultString)
|
|
}
|
|
}
|
|
})
|
|
}
|