This is page 30 of 33. Use http://codebase.md/googleapis/genai-toolbox?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ └── looker-run-look.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ └── lookerrunlook │ │ │ ├── lookerrunlook_test.go │ │ │ └── lookerrunlook.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /tests/alloydb/alloydb_integration_test.go: -------------------------------------------------------------------------------- ```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 alloydb import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "reflect" "regexp" "sort" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/tests" ) var ( AlloyDBProject = os.Getenv("ALLOYDB_PROJECT") AlloyDBLocation = os.Getenv("ALLOYDB_REGION") AlloyDBCluster = os.Getenv("ALLOYDB_CLUSTER") AlloyDBInstance = os.Getenv("ALLOYDB_INSTANCE") AlloyDBUser = os.Getenv("ALLOYDB_POSTGRES_USER") ) func getAlloyDBVars(t *testing.T) map[string]string { if AlloyDBProject == "" { t.Fatal("'ALLOYDB_PROJECT' not set") } if AlloyDBLocation == "" { t.Fatal("'ALLOYDB_REGION' not set") } if AlloyDBCluster == "" { t.Fatal("'ALLOYDB_CLUSTER' not set") } if AlloyDBInstance == "" { t.Fatal("'ALLOYDB_INSTANCE' not set") } if AlloyDBUser == "" { t.Fatal("'ALLOYDB_USER' not set") } return map[string]string{ "project": AlloyDBProject, "location": AlloyDBLocation, "cluster": AlloyDBCluster, "instance": AlloyDBInstance, "user": AlloyDBUser, } } func getAlloyDBToolsConfig() map[string]any { return map[string]any{ "sources": map[string]any{ "alloydb-admin-source": map[string]any{ "kind": "alloydb-admin", }, }, "tools": map[string]any{ // Tool for RunAlloyDBToolGetTest "my-simple-tool": map[string]any{ "kind": "alloydb-list-clusters", "source": "alloydb-admin-source", "description": "Simple tool to test end to end functionality.", }, // Tool for MCP test "my-param-tool": map[string]any{ "kind": "alloydb-list-clusters", "source": "alloydb-admin-source", "description": "Tool to list clusters", }, // Tool for MCP test that fails "my-fail-tool": map[string]any{ "kind": "alloydb-list-clusters", "source": "alloydb-admin-source", "description": "Tool that will fail", }, // AlloyDB specific tools "alloydb-list-clusters": map[string]any{ "kind": "alloydb-list-clusters", "source": "alloydb-admin-source", "description": "Lists all AlloyDB clusters in a given project and location.", }, "alloydb-list-users": map[string]any{ "kind": "alloydb-list-users", "source": "alloydb-admin-source", "description": "Lists all AlloyDB users within a specific cluster.", }, "alloydb-list-instances": map[string]any{ "kind": "alloydb-list-instances", "source": "alloydb-admin-source", "description": "Lists all AlloyDB instances within a specific cluster.", }, "alloydb-get-cluster": map[string]any{ "kind": "alloydb-get-cluster", "source": "alloydb-admin-source", "description": "Retrieves details of a specific AlloyDB cluster.", }, "alloydb-get-instance": map[string]any{ "kind": "alloydb-get-instance", "source": "alloydb-admin-source", "description": "Retrieves details of a specific AlloyDB instance.", }, "alloydb-get-user": map[string]any{ "kind": "alloydb-get-user", "source": "alloydb-admin-source", "description": "Retrieves details of a specific AlloyDB user.", }, "alloydb-create-cluster": map[string]any{ "kind": "alloydb-create-cluster", "description": "create cluster", "source": "alloydb-admin-source", }, "alloydb-create-instance": map[string]any{ "kind": "alloydb-create-instance", "description": "create instance", "source": "alloydb-admin-source", }, "alloydb-create-user": map[string]any{ "kind": "alloydb-create-user", "description": "create user", "source": "alloydb-admin-source", }, }, } } func TestAlloyDBToolEndpoints(t *testing.T) { vars := getAlloyDBVars(t) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() var args []string toolsFile := getAlloyDBToolsConfig() cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } defer cleanup() waitCtx, cancelWait := context.WithTimeout(ctx, 20*time.Second) defer cancelWait() 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: %v", err) } runAlloyDBToolGetTest(t) runAlloyDBMCPToolCallMethod(t, vars) // Run tool-specific invoke tests runAlloyDBListClustersTest(t, vars) runAlloyDBListInstancesTest(t, vars) runAlloyDBListUsersTest(t, vars) runAlloyDBGetClusterTest(t, vars) runAlloyDBGetInstanceTest(t, vars) runAlloyDBGetUserTest(t, vars) } func runAlloyDBToolGetTest(t *testing.T) { tcs := []struct { name string api string want map[string]any }{ { name: "get my-simple-tool", api: "http://127.0.0.1:5000/api/tool/my-simple-tool/", want: map[string]any{ "my-simple-tool": map[string]any{ "description": "Simple tool to test end to end functionality.", "parameters": []any{ map[string]any{"name": "project", "type": "string", "description": "The GCP project ID to list clusters for.", "required": true, "authSources": []any{}}, map[string]any{"name": "location", "type": "string", "description": "Optional: The location to list clusters in (e.g., 'us-central1'). Use '-' to list clusters across all locations.(Default: '-')", "required": false, "authSources": []any{}}, }, "authRequired": []any{}, }, }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { resp, err := http.Get(tc.api) if err != nil { t.Fatalf("error when sending a request: %s", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("response status code is not 200") } 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["tools"] if !ok { t.Fatalf("unable to find 'tools' in response body") } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("response mismatch (-want +got):\n%s", diff) } }) } } func runAlloyDBMCPToolCallMethod(t *testing.T, vars map[string]string) { sessionId := tests.RunInitialize(t, "2024-11-05") header := map[string]string{} if sessionId != "" { header["Mcp-Session-Id"] = sessionId } invokeTcs := []struct { name string requestBody jsonrpc.JSONRPCRequest wantContains string isErr bool }{ { name: "MCP Invoke my-param-tool", requestBody: jsonrpc.JSONRPCRequest{ Jsonrpc: "2.0", Id: "my-param-tool-mcp", Request: jsonrpc.Request{Method: "tools/call"}, Params: map[string]any{ "name": "my-param-tool", "arguments": map[string]any{ "project": vars["project"], "location": vars["location"], }, }, }, wantContains: fmt.Sprintf(`"name\":\"projects/%s/locations/%s/clusters/%s\"`, vars["project"], vars["location"], vars["cluster"]), isErr: false, }, { name: "MCP Invoke my-fail-tool", requestBody: jsonrpc.JSONRPCRequest{ Jsonrpc: "2.0", Id: "invoke-fail-tool", Request: jsonrpc.Request{Method: "tools/call"}, Params: map[string]any{ "name": "my-fail-tool", "arguments": map[string]any{ "location": vars["location"], }, }, }, wantContains: `parameter \"project\" is required`, isErr: true, }, { name: "MCP Invoke invalid tool", requestBody: jsonrpc.JSONRPCRequest{ Jsonrpc: "2.0", Id: "invalid-tool-mcp", Request: jsonrpc.Request{Method: "tools/call"}, Params: map[string]any{ "name": "non-existent-tool", "arguments": map[string]any{}, }, }, wantContains: `tool with name \"non-existent-tool\" does not exist`, isErr: true, }, { name: "MCP Invoke tool without required parameters", requestBody: jsonrpc.JSONRPCRequest{ Jsonrpc: "2.0", Id: "invoke-without-params-mcp", Request: jsonrpc.Request{Method: "tools/call"}, Params: map[string]any{ "name": "my-param-tool", "arguments": map[string]any{"location": vars["location"]}, }, }, wantContains: `parameter \"project\" is required`, isErr: true, }, { name: "MCP Invoke my-auth-required-tool", requestBody: jsonrpc.JSONRPCRequest{ Jsonrpc: "2.0", Id: "invoke my-auth-required-tool", Request: jsonrpc.Request{Method: "tools/call"}, Params: map[string]any{ "name": "my-auth-required-tool", "arguments": map[string]any{}, }, }, wantContains: `tool with name \"my-auth-required-tool\" does not exist`, isErr: true, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/mcp" reqMarshal, err := json.Marshal(tc.requestBody) if err != nil { t.Fatalf("unexpected error during marshaling of request body: %v", err) } req, err := http.NewRequest(http.MethodPost, api, bytes.NewBuffer(reqMarshal)) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("unable to read request body: %s", err) } got := string(bytes.TrimSpace(respBody)) if !strings.Contains(got, tc.wantContains) { t.Fatalf("Expected substring not found:\ngot: %q\nwant: %q (to be contained within got)", got, tc.wantContains) } }) } } func runAlloyDBListClustersTest(t *testing.T, vars map[string]string) { type ListClustersResponse struct { Clusters []struct { Name string `json:"name"` } `json:"clusters"` } type ToolResponse struct { Result string `json:"result"` } // NOTE: If clusters are added, removed or changed in the test project, // this list must be updated for the "list clusters specific locations" test to pass wantForSpecificLocation := []string{ fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-ai-nl-testing", vars["project"]), fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-pg-testing", vars["project"]), } // NOTE: If clusters are added, removed, or changed in the test project, // this list must be updated for the "list clusters all locations" test to pass wantForAllLocations := []string{ fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-ai-nl-testing", vars["project"]), fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-pg-testing", vars["project"]), fmt.Sprintf("projects/%s/locations/us-east4/clusters/alloydb-private-pg-testing", vars["project"]), fmt.Sprintf("projects/%s/locations/us-east4/clusters/colab-testing", vars["project"]), } invokeTcs := []struct { name string requestBody io.Reader want []string wantStatusCode int }{ { name: "list clusters for all locations", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "-"}`, vars["project"])), want: wantForAllLocations, wantStatusCode: http.StatusOK, }, { name: "list clusters specific location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "us-central1"}`, vars["project"])), want: wantForSpecificLocation, wantStatusCode: http.StatusOK, }, { name: "list clusters missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s"}`, vars["location"])), wantStatusCode: http.StatusBadRequest, }, { name: "list clusters non-existent location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "abcd"}`, vars["project"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list clusters non-existent project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "non-existent-project", "location": "%s"}`, vars["location"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list clusters empty project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "", "location": "%s"}`, vars["location"])), wantStatusCode: http.StatusBadRequest, }, { name: "list clusters empty location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": ""}`, vars["project"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-list-clusters/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing outer response body: %v", err) } var clustersData ListClustersResponse if err := json.Unmarshal([]byte(body.Result), &clustersData); err != nil { t.Fatalf("error parsing nested result JSON: %v", err) } var got []string for _, cluster := range clustersData.Clusters { got = append(got, cluster.Name) } sort.Strings(got) sort.Strings(tc.want) if !reflect.DeepEqual(got, tc.want) { t.Errorf("cluster list mismatch:\n got: %v\nwant: %v", got, tc.want) } } }) } } func runAlloyDBListUsersTest(t *testing.T, vars map[string]string) { type UsersResponse struct { Users []struct { Name string `json:"name"` } `json:"users"` } type ToolResponse struct { Result string `json:"result"` } invokeTcs := []struct { name string requestBody io.Reader wantContains string wantCount int wantStatusCode int }{ { name: "list users success", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s"}`, vars["project"], vars["location"], vars["cluster"])), wantContains: fmt.Sprintf("projects/%s/locations/%s/clusters/%s/users/%s", vars["project"], vars["location"], vars["cluster"], AlloyDBUser), wantCount: 3, // NOTE: If users are added or removed in the test project, update the number of users here must be updated for this test to pass wantStatusCode: http.StatusOK, }, { name: "list users missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s", "cluster": "%s"}`, vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "list users missing location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "cluster": "%s"}`, vars["project"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "list users missing cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s"}`, vars["project"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "list users non-existent project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "non-existent-project", "location": "%s", "cluster": "%s"}`, vars["location"], vars["cluster"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list users non-existent location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "non-existent-location", "cluster": "%s"}`, vars["project"], vars["cluster"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list users non-existent cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "non-existent-cluster"}`, vars["project"], vars["location"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-list-users/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing outer response body: %v", err) } var usersData UsersResponse if err := json.Unmarshal([]byte(body.Result), &usersData); err != nil { t.Fatalf("error parsing nested result JSON: %v", err) } var got []string for _, user := range usersData.Users { got = append(got, user.Name) } sort.Strings(got) if len(got) != tc.wantCount { t.Errorf("user count mismatch:\n got: %v\nwant: %v", len(got), tc.wantCount) } found := false for _, g := range got { if g == tc.wantContains { found = true break } } if !found { t.Errorf("wantContains not found in response:\n got: %v\nwant: %v", got, tc.wantContains) } } }) } } func runAlloyDBListInstancesTest(t *testing.T, vars map[string]string) { type ListInstancesResponse struct { Instances []struct { Name string `json:"name"` } `json:"instances"` } type ToolResponse struct { Result string `json:"result"` } wantForSpecificClusterAndLocation := []string{ fmt.Sprintf("projects/%s/locations/%s/clusters/%s/instances/%s", vars["project"], vars["location"], vars["cluster"], vars["instance"]), } // NOTE: If clusters or instances are added, removed or changed in the test project, // the below lists must be updated for the tests to pass. wantForAllClustersSpecificLocation := []string{ fmt.Sprintf("projects/%s/locations/%s/clusters/alloydb-ai-nl-testing/instances/alloydb-ai-nl-testing-instance", vars["project"], vars["location"]), fmt.Sprintf("projects/%s/locations/%s/clusters/alloydb-pg-testing/instances/alloydb-pg-testing-instance", vars["project"], vars["location"]), } wantForAllClustersAllLocations := []string{ fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-ai-nl-testing/instances/alloydb-ai-nl-testing-instance", vars["project"]), fmt.Sprintf("projects/%s/locations/us-central1/clusters/alloydb-pg-testing/instances/alloydb-pg-testing-instance", vars["project"]), fmt.Sprintf("projects/%s/locations/us-east4/clusters/alloydb-private-pg-testing/instances/alloydb-private-pg-testing-instance", vars["project"]), fmt.Sprintf("projects/%s/locations/us-east4/clusters/colab-testing/instances/colab-testing-primary", vars["project"]), } invokeTcs := []struct { name string requestBody io.Reader want []string wantStatusCode int }{ { name: "list instances for a specific cluster and location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s"}`, vars["project"], vars["location"], vars["cluster"])), want: wantForSpecificClusterAndLocation, wantStatusCode: http.StatusOK, }, { name: "list instances for all clusters and specific location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "-"}`, vars["project"], vars["location"])), want: wantForAllClustersSpecificLocation, wantStatusCode: http.StatusOK, }, { name: "list instances for all clusters and all locations", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "-", "cluster": "-"}`, vars["project"])), want: wantForAllClustersAllLocations, wantStatusCode: http.StatusOK, }, { name: "list instances missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s", "cluster": "%s"}`, vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "list instances non-existent project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "non-existent-project", "location": "%s", "cluster": "%s"}`, vars["location"], vars["cluster"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list instances non-existent location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "non-existent-location", "cluster": "%s"}`, vars["project"], vars["cluster"])), wantStatusCode: http.StatusInternalServerError, }, { name: "list instances non-existent cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "non-existent-cluster"}`, vars["project"], vars["location"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-list-instances/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing outer response body: %v", err) } var instancesData ListInstancesResponse if err := json.Unmarshal([]byte(body.Result), &instancesData); err != nil { t.Fatalf("error parsing nested result JSON: %v", err) } var got []string for _, instance := range instancesData.Instances { got = append(got, instance.Name) } sort.Strings(got) sort.Strings(tc.want) if !reflect.DeepEqual(got, tc.want) { t.Errorf("instance list mismatch:\n got: %v\nwant: %v", got, tc.want) } } }) } } func runAlloyDBGetClusterTest(t *testing.T, vars map[string]string) { type ToolResponse struct { Result string `json:"result"` } invokeTcs := []struct { name string requestBody io.Reader want map[string]any wantStatusCode int }{ { name: "get cluster success", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s"}`, vars["project"], vars["location"], vars["cluster"])), want: map[string]any{ "clusterType": "PRIMARY", "name": fmt.Sprintf("projects/%s/locations/%s/clusters/%s", vars["project"], vars["location"], vars["cluster"]), }, wantStatusCode: http.StatusOK, }, { name: "get cluster missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s", "cluster": "%s"}`, vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "get cluster missing location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "cluster": "%s"}`, vars["project"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "get cluster missing cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s"}`, vars["project"], vars["location"])), wantStatusCode: http.StatusBadRequest, }, { name: "get cluster non-existent cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "non-existent-cluster"}`, vars["project"], vars["location"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-get-cluster/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing response body: %v", err) } if tc.want != nil { var gotMap map[string]any if err := json.Unmarshal([]byte(body.Result), &gotMap); err != nil { t.Fatalf("failed to unmarshal JSON result into map: %v", err) } got := make(map[string]any) for key := range tc.want { if value, ok := gotMap[key]; ok { got[key] = value } } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want) } } } }) } } func runAlloyDBGetInstanceTest(t *testing.T, vars map[string]string) { type ToolResponse struct { Result string `json:"result"` } invokeTcs := []struct { name string requestBody io.Reader want map[string]any wantStatusCode int }{ { name: "get instance success", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s", "instance": "%s"}`, vars["project"], vars["location"], vars["cluster"], vars["instance"])), want: map[string]any{ "instanceType": "PRIMARY", "name": fmt.Sprintf("projects/%s/locations/%s/clusters/%s/instances/%s", vars["project"], vars["location"], vars["cluster"], vars["instance"]), }, wantStatusCode: http.StatusOK, }, { name: "get instance missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s", "cluster": "%s", "instance": "%s"}`, vars["location"], vars["cluster"], vars["instance"])), wantStatusCode: http.StatusBadRequest, }, { name: "get instance missing location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "cluster": "%s", "instance": "%s"}`, vars["project"], vars["cluster"], vars["instance"])), wantStatusCode: http.StatusBadRequest, }, { name: "get instance missing cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "instance": "%s"}`, vars["project"], vars["location"], vars["instance"])), wantStatusCode: http.StatusBadRequest, }, { name: "get instance missing instance", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s"}`, vars["project"], vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "get instance non-existent instance", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s", "instance": "non-existent-instance"}`, vars["project"], vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-get-instance/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing response body: %v", err) } if tc.want != nil { var gotMap map[string]any if err := json.Unmarshal([]byte(body.Result), &gotMap); err != nil { t.Fatalf("failed to unmarshal JSON result into map: %v", err) } got := make(map[string]any) for key := range tc.want { if value, ok := gotMap[key]; ok { got[key] = value } } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want) } } } }) } } func runAlloyDBGetUserTest(t *testing.T, vars map[string]string) { type ToolResponse struct { Result string `json:"result"` } invokeTcs := []struct { name string requestBody io.Reader want map[string]any wantStatusCode int }{ { name: "get user success", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s", "user": "%s"}`, vars["project"], vars["location"], vars["cluster"], vars["user"])), want: map[string]any{ "name": fmt.Sprintf("projects/%s/locations/%s/clusters/%s/users/%s", vars["project"], vars["location"], vars["cluster"], vars["user"]), "userType": "ALLOYDB_BUILT_IN", }, wantStatusCode: http.StatusOK, }, { name: "get user missing project", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"location": "%s", "cluster": "%s", "user": "%s"}`, vars["location"], vars["cluster"], vars["user"])), wantStatusCode: http.StatusBadRequest, }, { name: "get user missing location", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "cluster": "%s", "user": "%s"}`, vars["project"], vars["cluster"], vars["user"])), wantStatusCode: http.StatusBadRequest, }, { name: "get user missing cluster", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "user": "%s"}`, vars["project"], vars["location"], vars["user"])), wantStatusCode: http.StatusBadRequest, }, { name: "get user missing user", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s"}`, vars["project"], vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, { name: "get non-existent user", requestBody: bytes.NewBufferString(fmt.Sprintf(`{"project": "%s", "location": "%s", "cluster": "%s", "user": "non-existent-user"}`, vars["project"], vars["location"], vars["cluster"])), wantStatusCode: http.StatusBadRequest, }, } for _, tc := range invokeTcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-get-user/invoke" req, err := http.NewRequest(http.MethodPost, api, tc.requestBody) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() if resp.StatusCode != tc.wantStatusCode { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("response status code is not %d, got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode == http.StatusOK { var body ToolResponse if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("error parsing response body: %v", err) } if tc.want != nil { var gotMap map[string]any if err := json.Unmarshal([]byte(body.Result), &gotMap); err != nil { t.Fatalf("failed to unmarshal JSON result into map: %v", err) } got := make(map[string]any) for key := range tc.want { if value, ok := gotMap[key]; ok { got[key] = value } } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("Unexpected result: got %#v, want: %#v", got, tc.want) } } } }) } } type mockAlloyDBTransport struct { transport http.RoundTripper url *url.URL } func (t *mockAlloyDBTransport) RoundTrip(req *http.Request) (*http.Response, error) { if strings.HasPrefix(req.URL.String(), "https://alloydb.googleapis.com") { req.URL.Scheme = t.url.Scheme req.URL.Host = t.url.Host } return t.transport.RoundTrip(req) } type mockAlloyDBHandler struct { t *testing.T idParam string } func (h *mockAlloyDBHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.UserAgent(), "genai-toolbox/") { h.t.Errorf("User-Agent header not found") } id := r.URL.Query().Get(h.idParam) var response string var statusCode int switch id { case "c1-success": response = `{ "name": "projects/p1/locations/l1/operations/mock-operation-success", "metadata": { "verb": "create", "target": "projects/p1/locations/l1/clusters/c1-success" } }` statusCode = http.StatusOK case "c2-api-failure": response = `{"error":{"message":"internal api error"}}` statusCode = http.StatusInternalServerError case "i1-success": response = `{ "metadata": { "@type": "type.googleapis.com/google.cloud.alloydb.v1.OperationMetadata", "target": "projects/p1/locations/l1/clusters/c1/instances/i1-success", "verb": "create", "requestedCancellation": false, "apiVersion": "v1" }, "name": "projects/p1/locations/l1/operations/mock-operation-success" }` statusCode = http.StatusOK case "i2-api-failure": response = `{"error":{"message":"internal api error"}}` statusCode = http.StatusInternalServerError case "u1-iam-success": response = `{ "databaseRoles": ["alloydbiamuser"], "name": "projects/p1/locations/l1/clusters/c1/users/u1-iam-success", "userType": "ALLOYDB_IAM_USER" }` statusCode = http.StatusOK case "u2-builtin-success": response = `{ "databaseRoles": ["alloydbsuperuser"], "name": "projects/p1/locations/l1/clusters/c1/users/u2-builtin-success", "userType": "ALLOYDB_BUILT_IN" }` statusCode = http.StatusOK case "u3-api-failure": response = `{"error":{"message":"user internal api error"}}` statusCode = http.StatusInternalServerError default: http.Error(w, fmt.Sprintf("unhandled %s in mock server: %s", h.idParam, id), http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) if _, err := w.Write([]byte(response)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func setupTestServer(t *testing.T, idParam string) func() { handler := &mockAlloyDBHandler{t: t, idParam: idParam} server := httptest.NewServer(handler) serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("failed to parse server URL: %v", err) } originalTransport := http.DefaultClient.Transport if originalTransport == nil { originalTransport = http.DefaultTransport } http.DefaultClient.Transport = &mockAlloyDBTransport{ transport: originalTransport, url: serverURL, } return func() { server.Close() http.DefaultClient.Transport = originalTransport } } func TestAlloyDBCreateCluster(t *testing.T) { cleanup := setupTestServer(t, "clusterId") defer cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string toolsFile := getAlloyDBToolsConfig() cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } defer cleanupCmd() waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) defer cancelWait() 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) } tcs := []struct { name string body string want string wantStatusCode int }{ { name: "successful creation", body: `{"project": "p1", "location": "l1", "cluster": "c1-success", "password": "p1"}`, want: `{"name":"projects/p1/locations/l1/operations/mock-operation-success", "metadata": {"verb": "create", "target": "projects/p1/locations/l1/clusters/c1-success"}}`, wantStatusCode: http.StatusOK, }, { name: "api failure", body: `{"project": "p1", "location": "l1", "cluster": "c2-api-failure", "password": "p1"}`, want: "internal api error", wantStatusCode: http.StatusBadRequest, }, { name: "missing project", body: `{"location": "l1", "cluster": "c1", "password": "p1"}`, want: `parameter \"project\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing cluster", body: `{"project": "p1", "location": "l1", "password": "p1"}`, want: `parameter \"cluster\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing password", body: `{"project": "p1", "location": "l1", "cluster": "c1"}`, want: `parameter \"password\" is required`, wantStatusCode: http.StatusBadRequest, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-create-cluster/invoke" req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() bodyBytes, _ := io.ReadAll(resp.Body) if tc.wantStatusCode != http.StatusOK { if tc.want != "" && !bytes.Contains(bodyBytes, []byte(tc.want)) { t.Fatalf("expected error response to contain %q, but got: %s", tc.want, string(bodyBytes)) } return } if resp.StatusCode != http.StatusOK { t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } var result struct { Result string `json:"result"` } if err := json.Unmarshal(bodyBytes, &result); err != nil { t.Fatalf("failed to decode response: %v", err) } var got, want map[string]any if err := json.Unmarshal([]byte(result.Result), &got); err != nil { t.Fatalf("failed to unmarshal result: %v", err) } if err := json.Unmarshal([]byte(tc.want), &want); err != nil { t.Fatalf("failed to unmarshal want: %v", err) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("unexpected result (-want +got):\n%s", diff) } }) } } func TestAlloyDBCreateInstance(t *testing.T) { cleanup := setupTestServer(t, "instanceId") defer cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string toolsFile := getAlloyDBToolsConfig() cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } defer cleanupCmd() waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) defer cancelWait() 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) } tcs := []struct { name string body string want string wantStatusCode int }{ { name: "successful creation", body: `{"project": "p1", "location": "l1", "cluster": "c1", "instance": "i1-success", "instanceType": "PRIMARY", "displayName": "i1-success"}`, want: `{"metadata":{"@type":"type.googleapis.com/google.cloud.alloydb.v1.OperationMetadata","target":"projects/p1/locations/l1/clusters/c1/instances/i1-success","verb":"create","requestedCancellation":false,"apiVersion":"v1"},"name":"projects/p1/locations/l1/operations/mock-operation-success"}`, wantStatusCode: http.StatusOK, }, { name: "api failure", body: `{"project": "p1", "location": "l1", "cluster": "c1", "instance": "i2-api-failure", "instanceType": "PRIMARY", "displayName": "i1-success"}`, want: "internal api error", wantStatusCode: http.StatusBadRequest, }, { name: "missing project", body: `{"location": "l1", "cluster": "c1", "instance": "i1", "instanceType": "PRIMARY"}`, want: `parameter \"project\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing cluster", body: `{"project": "p1", "location": "l1", "instance": "i1", "instanceType": "PRIMARY"}`, want: `parameter \"cluster\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing location", body: `{"project": "p1", "cluster": "c1", "instance": "i1", "instanceType": "PRIMARY"}`, want: `parameter \"location\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing instance", body: `{"project": "p1", "location": "l1", "cluster": "c1", "instanceType": "PRIMARY"}`, want: `parameter \"instance\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "invalid instanceType", body: `{"project": "p1", "location": "l1", "cluster": "c1", "instance": "i1", "instanceType": "INVALID", "displayName": "invalid"}`, want: `invalid 'instanceType' parameter; expected 'PRIMARY' or 'READ_POOL'`, wantStatusCode: http.StatusBadRequest, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-create-instance/invoke" req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() bodyBytes, _ := io.ReadAll(resp.Body) if resp.StatusCode != tc.wantStatusCode { t.Fatalf("expected status %d but got %d: %s", tc.wantStatusCode, resp.StatusCode, string(bodyBytes)) } if tc.wantStatusCode != http.StatusOK { if tc.want != "" && !bytes.Contains(bodyBytes, []byte(tc.want)) { t.Fatalf("expected error response to contain %q, but got: %s", tc.want, string(bodyBytes)) } return } if resp.StatusCode != http.StatusOK { t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } var result struct { Result string `json:"result"` } if err := json.Unmarshal(bodyBytes, &result); err != nil { t.Fatalf("failed to decode response: %v", err) } var got, want map[string]any if err := json.Unmarshal([]byte(result.Result), &got); err != nil { t.Fatalf("failed to unmarshal result: %v", err) } if err := json.Unmarshal([]byte(tc.want), &want); err != nil { t.Fatalf("failed to unmarshal want: %v", err) } if !reflect.DeepEqual(want, got) { t.Errorf("unexpected result:\n- want: %+v\n- got: %+v", want, got) } }) } } func TestAlloyDBCreateUser(t *testing.T) { cleanup := setupTestServer(t, "userId") defer cleanup() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() var args []string toolsFile := getAlloyDBToolsConfig() cmd, cleanupCmd, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %v", err) } defer cleanupCmd() waitCtx, cancelWait := context.WithTimeout(ctx, 10*time.Second) defer cancelWait() 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) } tcs := []struct { name string body string want string wantStatusCode int }{ { name: "successful creation IAM user", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u1-iam-success", "userType": "ALLOYDB_IAM_USER"}`, want: `{"databaseRoles": ["alloydbiamuser"], "name": "projects/p1/locations/l1/clusters/c1/users/u1-iam-success", "userType": "ALLOYDB_IAM_USER"}`, wantStatusCode: http.StatusOK, }, { name: "successful creation builtin user", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u2-builtin-success", "userType": "ALLOYDB_BUILT_IN", "password": "pass123", "databaseRoles": ["alloydbsuperuser"]}`, want: `{"databaseRoles": ["alloydbsuperuser"], "name": "projects/p1/locations/l1/clusters/c1/users/u2-builtin-success", "userType": "ALLOYDB_BUILT_IN"}`, wantStatusCode: http.StatusOK, }, { name: "api failure", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u3-api-failure", "userType": "ALLOYDB_IAM_USER"}`, want: "user internal api error", wantStatusCode: http.StatusBadRequest, }, { name: "missing project", body: `{"location": "l1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`, want: `parameter \"project\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing cluster", body: `{"project": "p1", "location": "l1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`, want: `parameter \"cluster\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing location", body: `{"project": "p1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_IAM_USER"}`, want: `parameter \"location\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing user", body: `{"project": "p1", "location": "l1", "cluster": "c1", "userType": "ALLOYDB_IAM_USER"}`, want: `parameter \"user\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing userType", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail"}`, want: `parameter \"userType\" is required`, wantStatusCode: http.StatusBadRequest, }, { name: "missing password for builtin user", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail", "userType": "ALLOYDB_BUILT_IN"}`, want: `password is required when userType is ALLOYDB_BUILT_IN`, wantStatusCode: http.StatusBadRequest, }, { name: "invalid userType", body: `{"project": "p1", "location": "l1", "cluster": "c1", "user": "u-fail", "userType": "invalid"}`, want: `invalid or missing 'userType' parameter; expected 'ALLOYDB_BUILT_IN' or 'ALLOYDB_IAM_USER'`, wantStatusCode: http.StatusBadRequest, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { api := "http://127.0.0.1:5000/api/tool/alloydb-create-user/invoke" req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) if err != nil { t.Fatalf("unable to create request: %s", err) } req.Header.Add("Content-type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("unable to send request: %s", err) } defer resp.Body.Close() bodyBytes, _ := io.ReadAll(resp.Body) if tc.wantStatusCode != http.StatusOK { if tc.want != "" && !bytes.Contains(bodyBytes, []byte(tc.want)) { t.Fatalf("expected error response to contain %q, but got: %s", tc.want, string(bodyBytes)) } return } if resp.StatusCode != http.StatusOK { t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } var result struct { Result string `json:"result"` } if err := json.Unmarshal(bodyBytes, &result); err != nil { t.Fatalf("failed to decode response: %v", err) } var got, want map[string]any if err := json.Unmarshal([]byte(result.Result), &got); err != nil { t.Fatalf("failed to unmarshal result string: %v. Result: %s", err, result.Result) } if err := json.Unmarshal([]byte(tc.want), &want); err != nil { t.Fatalf("failed to unmarshal want string: %v. Want: %s", err, tc.want) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("unexpected result map (-want +got):\n%s", diff) } }) } } ``` -------------------------------------------------------------------------------- /internal/tools/parameters_test.go: -------------------------------------------------------------------------------- ```go // Copyright 2024 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 tools_test import ( "bytes" "encoding/json" "math" "reflect" "slices" "strings" "testing" "github.com/goccy/go-yaml" "github.com/google/go-cmp/cmp" "github.com/googleapis/genai-toolbox/internal/testutils" "github.com/googleapis/genai-toolbox/internal/tools" ) func TestParametersMarshal(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { name string in []map[string]any want tools.Parameters }{ { name: "string", in: []map[string]any{ { "name": "my_string", "type": "string", "description": "this param is a string", }, }, want: tools.Parameters{ tools.NewStringParameter("my_string", "this param is a string"), }, }, { name: "string not required", in: []map[string]any{ { "name": "my_string", "type": "string", "description": "this param is a string", "required": false, }, }, want: tools.Parameters{ tools.NewStringParameterWithRequired("my_string", "this param is a string", false), }, }, { name: "int", in: []map[string]any{ { "name": "my_integer", "type": "integer", "description": "this param is an int", }, }, want: tools.Parameters{ tools.NewIntParameter("my_integer", "this param is an int"), }, }, { name: "int not required", in: []map[string]any{ { "name": "my_integer", "type": "integer", "description": "this param is an int", "required": false, }, }, want: tools.Parameters{ tools.NewIntParameterWithRequired("my_integer", "this param is an int", false), }, }, { name: "float", in: []map[string]any{ { "name": "my_float", "type": "float", "description": "my param is a float", }, }, want: tools.Parameters{ tools.NewFloatParameter("my_float", "my param is a float"), }, }, { name: "float not required", in: []map[string]any{ { "name": "my_float", "type": "float", "description": "my param is a float", "required": false, }, }, want: tools.Parameters{ tools.NewFloatParameterWithRequired("my_float", "my param is a float", false), }, }, { name: "bool", in: []map[string]any{ { "name": "my_bool", "type": "boolean", "description": "this param is a boolean", }, }, want: tools.Parameters{ tools.NewBooleanParameter("my_bool", "this param is a boolean"), }, }, { name: "bool not required", in: []map[string]any{ { "name": "my_bool", "type": "boolean", "description": "this param is a boolean", "required": false, }, }, want: tools.Parameters{ tools.NewBooleanParameterWithRequired("my_bool", "this param is a boolean", false), }, }, { name: "string array", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", "items": map[string]string{ "name": "my_string", "type": "string", "description": "string item", }, }, }, want: tools.Parameters{ tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")), }, }, { name: "string array not required", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", "required": false, "items": map[string]string{ "name": "my_string", "type": "string", "description": "string item", }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithRequired("my_array", "this param is an array of strings", false, tools.NewStringParameter("my_string", "string item")), }, }, { name: "float array", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of floats", "items": map[string]string{ "name": "my_float", "type": "float", "description": "float item", }, }, }, want: tools.Parameters{ tools.NewArrayParameter("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")), }, }, { name: "string default", in: []map[string]any{ { "name": "my_string", "type": "string", "default": "foo", "description": "this param is a string", }, }, want: tools.Parameters{ tools.NewStringParameterWithDefault("my_string", "foo", "this param is a string"), }, }, { name: "int default", in: []map[string]any{ { "name": "my_integer", "type": "integer", "default": 5, "description": "this param is an int", }, }, want: tools.Parameters{ tools.NewIntParameterWithDefault("my_integer", 5, "this param is an int"), }, }, { name: "float default", in: []map[string]any{ { "name": "my_float", "type": "float", "default": 1.1, "description": "my param is a float", }, }, want: tools.Parameters{ tools.NewFloatParameterWithDefault("my_float", 1.1, "my param is a float"), }, }, { name: "bool default", in: []map[string]any{ { "name": "my_bool", "type": "boolean", "default": true, "description": "this param is a boolean", }, }, want: tools.Parameters{ tools.NewBooleanParameterWithDefault("my_bool", true, "this param is a boolean"), }, }, { name: "string array default", in: []map[string]any{ { "name": "my_array", "type": "array", "default": []any{"foo", "bar"}, "description": "this param is an array of strings", "items": map[string]string{ "name": "my_string", "type": "string", "description": "string item", }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithDefault("my_array", []any{"foo", "bar"}, "this param is an array of strings", tools.NewStringParameter("my_string", "string item")), }, }, { name: "float array default", in: []map[string]any{ { "name": "my_array", "type": "array", "default": []any{1.0, 1.1}, "description": "this param is an array of floats", "items": map[string]string{ "name": "my_float", "type": "float", "description": "float item", }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithDefault("my_array", []any{1.0, 1.1}, "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")), }, }, { name: "map with string values", in: []map[string]any{ { "name": "my_map", "type": "map", "description": "this param is a map of strings", "valueType": "string", }, }, want: tools.Parameters{ tools.NewMapParameter("my_map", "this param is a map of strings", "string"), }, }, { name: "map not required", in: []map[string]any{ { "name": "my_map", "type": "map", "description": "this param is a map of strings", "required": false, "valueType": "string", }, }, want: tools.Parameters{ tools.NewMapParameterWithRequired("my_map", "this param is a map of strings", false, "string"), }, }, { name: "map with default", in: []map[string]any{ { "name": "my_map", "type": "map", "description": "this param is a map of strings", "default": map[string]any{"key1": "val1"}, "valueType": "string", }, }, want: tools.Parameters{ tools.NewMapParameterWithDefault("my_map", map[string]any{"key1": "val1"}, "this param is a map of strings", "string"), }, }, { name: "generic map (no valueType)", in: []map[string]any{ { "name": "my_generic_map", "type": "map", "description": "this param is a generic map", }, }, want: tools.Parameters{ tools.NewMapParameter("my_generic_map", "this param is a generic map", ""), }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { var got tools.Parameters // parse map to bytes data, err := yaml.Marshal(tc.in) if err != nil { t.Fatalf("unable to marshal input to yaml: %s", err) } // parse bytes to object err = yaml.UnmarshalContext(ctx, data, &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("incorrect parse: diff %v", diff) } }) } } func TestAuthParametersMarshal(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } authServices := []tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"}, {Name: "other-auth-service", Field: "user_id"}} tcs := []struct { name string in []map[string]any want tools.Parameters }{ { name: "string", in: []map[string]any{ { "name": "my_string", "type": "string", "description": "this param is a string", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewStringParameterWithAuth("my_string", "this param is a string", authServices), }, }, { name: "string with authServices", in: []map[string]any{ { "name": "my_string", "type": "string", "description": "this param is a string", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewStringParameterWithAuth("my_string", "this param is a string", authServices), }, }, { name: "int", in: []map[string]any{ { "name": "my_integer", "type": "integer", "description": "this param is an int", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewIntParameterWithAuth("my_integer", "this param is an int", authServices), }, }, { name: "int with authServices", in: []map[string]any{ { "name": "my_integer", "type": "integer", "description": "this param is an int", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewIntParameterWithAuth("my_integer", "this param is an int", authServices), }, }, { name: "float", in: []map[string]any{ { "name": "my_float", "type": "float", "description": "my param is a float", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewFloatParameterWithAuth("my_float", "my param is a float", authServices), }, }, { name: "float with authServices", in: []map[string]any{ { "name": "my_float", "type": "float", "description": "my param is a float", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewFloatParameterWithAuth("my_float", "my param is a float", authServices), }, }, { name: "bool", in: []map[string]any{ { "name": "my_bool", "type": "boolean", "description": "this param is a boolean", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewBooleanParameterWithAuth("my_bool", "this param is a boolean", authServices), }, }, { name: "bool with authServices", in: []map[string]any{ { "name": "my_bool", "type": "boolean", "description": "this param is a boolean", "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewBooleanParameterWithAuth("my_bool", "this param is a boolean", authServices), }, }, { name: "string array", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", "items": map[string]string{ "name": "my_string", "type": "string", "description": "string item", }, "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithAuth("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item"), authServices), }, }, { name: "string array with authServices", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", "items": map[string]string{ "name": "my_string", "type": "string", "description": "string item", }, "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithAuth("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item"), authServices), }, }, { name: "float array", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of floats", "items": map[string]string{ "name": "my_float", "type": "float", "description": "float item", }, "authServices": []map[string]string{ { "name": "my-google-auth-service", "field": "user_id", }, { "name": "other-auth-service", "field": "user_id", }, }, }, }, want: tools.Parameters{ tools.NewArrayParameterWithAuth("my_array", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item"), authServices), }, }, { name: "map", in: []map[string]any{ { "name": "my_map", "type": "map", "description": "this param is a map of strings", "valueType": "string", "authServices": []map[string]string{ {"name": "my-google-auth-service", "field": "user_id"}, {"name": "other-auth-service", "field": "user_id"}, }, }, }, want: tools.Parameters{ tools.NewMapParameterWithAuth("my_map", "this param is a map of strings", "string", authServices), }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { var got tools.Parameters // parse map to bytes data, err := yaml.Marshal(tc.in) if err != nil { t.Fatalf("unable to marshal input to yaml: %s", err) } // parse bytes to object err = yaml.UnmarshalContext(ctx, data, &got) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("incorrect parse: diff %v", diff) } }) } } func TestParametersParse(t *testing.T) { tcs := []struct { name string params tools.Parameters in map[string]any want tools.ParamValues }{ // ... (primitive type tests are unchanged) { name: "string", params: tools.Parameters{ tools.NewStringParameter("my_string", "this param is a string"), }, in: map[string]any{ "my_string": "hello world", }, want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "hello world"}}, }, { name: "not string", params: tools.Parameters{ tools.NewStringParameter("my_string", "this param is a string"), }, in: map[string]any{ "my_string": 4, }, }, { name: "int", params: tools.Parameters{ tools.NewIntParameter("my_int", "this param is an int"), }, in: map[string]any{ "my_int": 100, }, want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 100}}, }, { name: "not int", params: tools.Parameters{ tools.NewIntParameter("my_int", "this param is an int"), }, in: map[string]any{ "my_int": 14.5, }, }, { name: "not int (big)", params: tools.Parameters{ tools.NewIntParameter("my_int", "this param is an int"), }, in: map[string]any{ "my_int": math.MaxInt64, }, want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: math.MaxInt64}}, }, { name: "float", params: tools.Parameters{ tools.NewFloatParameter("my_float", "this param is a float"), }, in: map[string]any{ "my_float": 1.5, }, want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.5}}, }, { name: "not float", params: tools.Parameters{ tools.NewFloatParameter("my_float", "this param is a float"), }, in: map[string]any{ "my_float": true, }, }, { name: "bool", params: tools.Parameters{ tools.NewBooleanParameter("my_bool", "this param is a bool"), }, in: map[string]any{ "my_bool": true, }, want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}}, }, { name: "not bool", params: tools.Parameters{ tools.NewBooleanParameter("my_bool", "this param is a bool"), }, in: map[string]any{ "my_bool": 1.5, }, }, { name: "string default", params: tools.Parameters{ tools.NewStringParameterWithDefault("my_string", "foo", "this param is a string"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "foo"}}, }, { name: "int default", params: tools.Parameters{ tools.NewIntParameterWithDefault("my_int", 100, "this param is an int"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 100}}, }, { name: "int (big)", params: tools.Parameters{ tools.NewIntParameterWithDefault("my_big_int", math.MaxInt64, "this param is an int"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_big_int", Value: math.MaxInt64}}, }, { name: "float default", params: tools.Parameters{ tools.NewFloatParameterWithDefault("my_float", 1.1, "this param is a float"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.1}}, }, { name: "bool default", params: tools.Parameters{ tools.NewBooleanParameterWithDefault("my_bool", true, "this param is a bool"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}}, }, { name: "string not required", params: tools.Parameters{ tools.NewStringParameterWithRequired("my_string", "this param is a string", false), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: nil}}, }, { name: "int not required", params: tools.Parameters{ tools.NewIntParameterWithRequired("my_int", "this param is an int", false), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: nil}}, }, { name: "float not required", params: tools.Parameters{ tools.NewFloatParameterWithRequired("my_float", "this param is a float", false), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: nil}}, }, { name: "bool not required", params: tools.Parameters{ tools.NewBooleanParameterWithRequired("my_bool", "this param is a bool", false), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}}, }, { name: "map", params: tools.Parameters{ tools.NewMapParameter("my_map", "a map", "string"), }, in: map[string]any{ "my_map": map[string]any{"key1": "val1", "key2": "val2"}, }, want: tools.ParamValues{tools.ParamValue{Name: "my_map", Value: map[string]any{"key1": "val1", "key2": "val2"}}}, }, { name: "generic map", params: tools.Parameters{ tools.NewMapParameter("my_map_generic_type", "a generic map", ""), }, in: map[string]any{ "my_map_generic_type": map[string]any{"key1": "val1", "key2": 123, "key3": true}, }, want: tools.ParamValues{tools.ParamValue{Name: "my_map_generic_type", Value: map[string]any{"key1": "val1", "key2": int64(123), "key3": true}}}, }, { name: "not map (value type mismatch)", params: tools.Parameters{ tools.NewMapParameter("my_map", "a map", "string"), }, in: map[string]any{ "my_map": map[string]any{"key1": 123}, }, }, { name: "map default", params: tools.Parameters{ tools.NewMapParameterWithDefault("my_map_default", map[string]any{"default_key": "default_val"}, "a map", "string"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_map_default", Value: map[string]any{"default_key": "default_val"}}}, }, { name: "map not required", params: tools.Parameters{ tools.NewMapParameterWithRequired("my_map_not_required", "a map", false, "string"), }, in: map[string]any{}, want: tools.ParamValues{tools.ParamValue{Name: "my_map_not_required", Value: nil}}, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { // parse map to bytes data, err := json.Marshal(tc.in) if err != nil { t.Fatalf("unable to marshal input to yaml: %s", err) } // parse bytes to object var m map[string]any d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() err = d.Decode(&m) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } wantErr := len(tc.want) == 0 // error is expected if no items in want gotAll, err := tools.ParseParams(tc.params, m, make(map[string]map[string]any)) if err != nil { if wantErr { return } t.Fatalf("unexpected error from ParseParams: %s", err) } if wantErr { t.Fatalf("expected error but Param parsed successfully: %s", gotAll) } // Use cmp.Diff for robust comparison if diff := cmp.Diff(tc.want, gotAll); diff != "" { t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff) } }) } } func TestAuthParametersParse(t *testing.T) { authServices := []tools.ParamAuthService{ { Name: "my-google-auth-service", Field: "auth_field", }, { Name: "other-auth-service", Field: "other_auth_field", }} tcs := []struct { name string params tools.Parameters in map[string]any claimsMap map[string]map[string]any want tools.ParamValues }{ { name: "string", params: tools.Parameters{ tools.NewStringParameterWithAuth("my_string", "this param is a string", authServices), }, in: map[string]any{ "my_string": "hello world", }, claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": "hello"}}, want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "hello"}}, }, { name: "not string", params: tools.Parameters{ tools.NewStringParameterWithAuth("my_string", "this param is a string", authServices), }, in: map[string]any{ "my_string": 4, }, claimsMap: map[string]map[string]any{}, }, { name: "int", params: tools.Parameters{ tools.NewIntParameterWithAuth("my_int", "this param is an int", authServices), }, in: map[string]any{ "my_int": 100, }, claimsMap: map[string]map[string]any{"other-auth-service": {"other_auth_field": 120}}, want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 120}}, }, { name: "not int", params: tools.Parameters{ tools.NewIntParameterWithAuth("my_int", "this param is an int", authServices), }, in: map[string]any{ "my_int": 14.5, }, claimsMap: map[string]map[string]any{}, }, { name: "float", params: tools.Parameters{ tools.NewFloatParameterWithAuth("my_float", "this param is a float", authServices), }, in: map[string]any{ "my_float": 1.5, }, claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": 2.1}}, want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 2.1}}, }, { name: "not float", params: tools.Parameters{ tools.NewFloatParameterWithAuth("my_float", "this param is a float", authServices), }, in: map[string]any{ "my_float": true, }, claimsMap: map[string]map[string]any{}, }, { name: "bool", params: tools.Parameters{ tools.NewBooleanParameterWithAuth("my_bool", "this param is a bool", authServices), }, in: map[string]any{ "my_bool": true, }, claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": false}}, want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: false}}, }, { name: "not bool", params: tools.Parameters{ tools.NewBooleanParameterWithAuth("my_bool", "this param is a bool", authServices), }, in: map[string]any{ "my_bool": 1.5, }, claimsMap: map[string]map[string]any{}, }, { name: "username", params: tools.Parameters{ tools.NewStringParameterWithAuth("username", "username string", authServices), }, in: map[string]any{ "username": "Violet", }, claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": "Alice"}}, want: tools.ParamValues{tools.ParamValue{Name: "username", Value: "Alice"}}, }, { name: "expect claim error", params: tools.Parameters{ tools.NewStringParameterWithAuth("username", "username string", authServices), }, in: map[string]any{ "username": "Violet", }, claimsMap: map[string]map[string]any{"my-google-auth-service": {"not_an_auth_field": "Alice"}}, }, { name: "map", params: tools.Parameters{ tools.NewMapParameterWithAuth("my_map", "a map", "string", authServices), }, in: map[string]any{"my_map": map[string]any{"key1": "val1"}}, claimsMap: map[string]map[string]any{"my-google-auth-service": {"auth_field": map[string]any{"authed_key": "authed_val"}}}, want: tools.ParamValues{tools.ParamValue{Name: "my_map", Value: map[string]any{"authed_key": "authed_val"}}}, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { // parse map to bytes data, err := json.Marshal(tc.in) if err != nil { t.Fatalf("unable to marshal input to yaml: %s", err) } // parse bytes to object var m map[string]any d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() err = d.Decode(&m) if err != nil { t.Fatalf("unable to unmarshal: %s", err) } gotAll, err := tools.ParseParams(tc.params, m, tc.claimsMap) if err != nil { if len(tc.want) == 0 { // error is expected if no items in want return } t.Fatalf("unexpected error from ParseParams: %s", err) } if diff := cmp.Diff(tc.want, gotAll); diff != "" { t.Fatalf("ParseParams() mismatch (-want +got):\n%s", diff) } }) } } func TestParamValues(t *testing.T) { tcs := []struct { name string in tools.ParamValues wantSlice []any wantMap map[string]interface{} wantMapOrdered map[string]interface{} wantMapWithDollar map[string]interface{} }{ { name: "string", in: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}, tools.ParamValue{Name: "my_string", Value: "hello world"}}, wantSlice: []any{true, "hello world"}, wantMap: map[string]interface{}{"my_bool": true, "my_string": "hello world"}, wantMapOrdered: map[string]interface{}{"p1": true, "p2": "hello world"}, wantMapWithDollar: map[string]interface{}{ "$my_bool": true, "$my_string": "hello world", }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { gotSlice := tc.in.AsSlice() gotMap := tc.in.AsMap() gotMapOrdered := tc.in.AsMapByOrderedKeys() gotMapWithDollar := tc.in.AsMapWithDollarPrefix() for i, got := range gotSlice { want := tc.wantSlice[i] if got != want { t.Fatalf("unexpected value: got %q, want %q", got, want) } } for i, got := range gotMap { want := tc.wantMap[i] if got != want { t.Fatalf("unexpected value: got %q, want %q", got, want) } } for i, got := range gotMapOrdered { want := tc.wantMapOrdered[i] if got != want { t.Fatalf("unexpected value: got %q, want %q", got, want) } } for key, got := range gotMapWithDollar { want := tc.wantMapWithDollar[key] if got != want { t.Fatalf("unexpected value in AsMapWithDollarPrefix: got %q, want %q", got, want) } } }) } } func TestParamManifest(t *testing.T) { tcs := []struct { name string in tools.Parameter want tools.ParameterManifest }{ { name: "string", in: tools.NewStringParameter("foo-string", "bar"), want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}}, }, { name: "int", in: tools.NewIntParameter("foo-int", "bar"), want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: true, Description: "bar", AuthServices: []string{}}, }, { name: "float", in: tools.NewFloatParameter("foo-float", "bar"), want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: true, Description: "bar", AuthServices: []string{}}, }, { name: "boolean", in: tools.NewBooleanParameter("foo-bool", "bar"), want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: true, Description: "bar", AuthServices: []string{}}, }, { name: "array", in: tools.NewArrayParameter("foo-array", "bar", tools.NewStringParameter("foo-string", "bar")), want: tools.ParameterManifest{ Name: "foo-array", Type: "array", Required: true, Description: "bar", AuthServices: []string{}, Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}}, }, }, { name: "string default", in: tools.NewStringParameterWithDefault("foo-string", "foo", "bar"), want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "int default", in: tools.NewIntParameterWithDefault("foo-int", 1, "bar"), want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "float default", in: tools.NewFloatParameterWithDefault("foo-float", 1.1, "bar"), want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "boolean default", in: tools.NewBooleanParameterWithDefault("foo-bool", true, "bar"), want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "array default", in: tools.NewArrayParameterWithDefault("foo-array", []any{"foo", "bar"}, "bar", tools.NewStringParameter("foo-string", "bar")), want: tools.ParameterManifest{ Name: "foo-array", Type: "array", Required: false, Description: "bar", AuthServices: []string{}, Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}}, }, }, { name: "string not required", in: tools.NewStringParameterWithRequired("foo-string", "bar", false), want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "int not required", in: tools.NewIntParameterWithRequired("foo-int", "bar", false), want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "float not required", in: tools.NewFloatParameterWithRequired("foo-float", "bar", false), want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "boolean not required", in: tools.NewBooleanParameterWithRequired("foo-bool", "bar", false), want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}}, }, { name: "array not required", in: tools.NewArrayParameterWithRequired("foo-array", "bar", false, tools.NewStringParameter("foo-string", "bar")), want: tools.ParameterManifest{ Name: "foo-array", Type: "array", Required: false, Description: "bar", AuthServices: []string{}, Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}}, }, }, { name: "map with string values", in: tools.NewMapParameter("foo-map", "bar", "string"), want: tools.ParameterManifest{ Name: "foo-map", Type: "object", Required: true, Description: "bar", AuthServices: []string{}, AdditionalProperties: map[string]any{"type": "string"}, }, }, { name: "map not required", in: tools.NewMapParameterWithRequired("foo-map", "bar", false, "string"), want: tools.ParameterManifest{ Name: "foo-map", Type: "object", Required: false, Description: "bar", AuthServices: []string{}, AdditionalProperties: map[string]any{"type": "string"}, }, }, { name: "generic map (additionalProperties true)", in: tools.NewMapParameter("foo-map", "bar", ""), want: tools.ParameterManifest{ Name: "foo-map", Type: "object", Required: true, Description: "bar", AuthServices: []string{}, AdditionalProperties: true, }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got := tc.in.Manifest() if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("unexpected manifest (-want +got):\n%s", diff) } }) } } func TestParamMcpManifest(t *testing.T) { tcs := []struct { name string in tools.Parameter want tools.ParameterMcpManifest wantAuthParam []string }{ { name: "string", in: tools.NewStringParameter("foo-string", "bar"), want: tools.ParameterMcpManifest{Type: "string", Description: "bar"}, wantAuthParam: []string{}, }, { name: "int", in: tools.NewIntParameter("foo-int", "bar"), want: tools.ParameterMcpManifest{Type: "integer", Description: "bar"}, wantAuthParam: []string{}, }, { name: "float", in: tools.NewFloatParameter("foo-float", "bar"), want: tools.ParameterMcpManifest{Type: "number", Description: "bar"}, wantAuthParam: []string{}, }, { name: "boolean", in: tools.NewBooleanParameter("foo-bool", "bar"), want: tools.ParameterMcpManifest{Type: "boolean", Description: "bar"}, wantAuthParam: []string{}, }, { name: "array", in: tools.NewArrayParameter("foo-array", "bar", tools.NewStringParameter("foo-string", "bar")), want: tools.ParameterMcpManifest{ Type: "array", Description: "bar", Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"}, }, wantAuthParam: []string{}, }, { name: "map with string values", in: tools.NewMapParameter("foo-map", "bar", "string"), want: tools.ParameterMcpManifest{ Type: "object", Description: "bar", AdditionalProperties: map[string]any{"type": "string"}, }, wantAuthParam: []string{}, }, { name: "generic map (additionalProperties true)", in: tools.NewMapParameter("foo-map", "bar", ""), want: tools.ParameterMcpManifest{ Type: "object", Description: "bar", AdditionalProperties: true, }, wantAuthParam: []string{}, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got, gotAuthParam := tc.in.McpManifest() if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("unexpected manifest (-want +got):\n%s", diff) } slices.Sort(gotAuthParam) if !reflect.DeepEqual(gotAuthParam, tc.wantAuthParam) { t.Fatalf("unexpected auth param list: got %s, want %s", gotAuthParam, tc.wantAuthParam) } }) } } func TestMcpManifest(t *testing.T) { authServices := []tools.ParamAuthService{ { Name: "my-google-auth-service", Field: "auth_field", }, { Name: "other-auth-service", Field: "other_auth_field", }} tcs := []struct { name string in tools.Parameters wantSchema tools.McpToolsSchema wantAuthParam map[string][]string }{ { name: "all types", in: tools.Parameters{ tools.NewStringParameterWithDefault("foo-string", "foo", "bar"), tools.NewStringParameter("foo-string2", "bar"), tools.NewStringParameterWithAuth("foo-string3-auth", "bar", authServices), tools.NewIntParameter("foo-int2", "bar"), tools.NewFloatParameter("foo-float", "bar"), tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")), tools.NewMapParameter("foo-map-int", "a map of ints", "integer"), tools.NewMapParameter("foo-map-any", "a map of any", ""), }, wantSchema: tools.McpToolsSchema{ Type: "object", Properties: map[string]tools.ParameterMcpManifest{ "foo-string": {Type: "string", Description: "bar"}, "foo-string2": {Type: "string", Description: "bar"}, "foo-string3-auth": {Type: "string", Description: "bar"}, "foo-int2": {Type: "integer", Description: "bar"}, "foo-float": {Type: "number", Description: "bar"}, "foo-array2": { Type: "array", Description: "bar", Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"}, }, "foo-map-int": { Type: "object", Description: "a map of ints", AdditionalProperties: map[string]any{"type": "integer"}, }, "foo-map-any": { Type: "object", Description: "a map of any", AdditionalProperties: true, }, }, Required: []string{"foo-string2", "foo-string3-auth", "foo-int2", "foo-float", "foo-array2", "foo-map-int", "foo-map-any"}, }, wantAuthParam: map[string][]string{ "foo-string3-auth": []string{"my-google-auth-service", "other-auth-service"}, }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { gotSchema, gotAuthParam := tc.in.McpManifest() if diff := cmp.Diff(tc.wantSchema, gotSchema); diff != "" { t.Fatalf("unexpected manifest (-want +got):\n%s", diff) } if len(gotAuthParam) != len(tc.wantAuthParam) { t.Fatalf("got %d items in auth param map, want %d", len(gotAuthParam), len(tc.wantAuthParam)) } for k, want := range tc.wantAuthParam { got, ok := gotAuthParam[k] if !ok { t.Fatalf("missing auth param: %s", k) } slices.Sort(got) if !reflect.DeepEqual(got, want) { t.Fatalf("unexpected auth param, got %s, want %s", got, want) } } }) } } func TestFailParametersUnmarshal(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { t.Fatalf("unexpected error: %s", err) } tcs := []struct { name string in []map[string]any err string }{ { name: "common parameter missing name", in: []map[string]any{ { "type": "string", "description": "this is a param for string", }, }, err: "unable to parse as \"string\": Key: 'CommonParameter.Name' Error:Field validation for 'Name' failed on the 'required' tag", }, { name: "common parameter missing type", in: []map[string]any{ { "name": "string", "description": "this is a param for string", }, }, err: "parameter is missing 'type' field: %!w(<nil>)", }, { name: "common parameter missing description", in: []map[string]any{ { "name": "my_string", "type": "string", }, }, err: "unable to parse as \"string\": Key: 'CommonParameter.Desc' Error:Field validation for 'Desc' failed on the 'required' tag", }, { name: "array parameter missing items", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", }, }, err: "unable to parse as \"array\": unable to parse 'items' field: error parsing parameters: nothing to unmarshal", }, { name: "array parameter missing items' name", in: []map[string]any{ { "name": "my_array", "type": "array", "description": "this param is an array of strings", "items": map[string]string{ "type": "string", "description": "string item", }, }, }, err: "unable to parse as \"array\": unable to parse 'items' field: unable to parse as \"string\": Key: 'CommonParameter.Name' Error:Field validation for 'Name' failed on the 'required' tag", }, // --- MODIFIED MAP PARAMETER TEST --- { name: "map with invalid valueType", in: []map[string]any{ { "name": "my_map", "type": "map", "description": "this param is a map", "valueType": "not-a-real-type", }, }, err: "unsupported valueType \"not-a-real-type\" for map parameter", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { var got tools.Parameters // parse map to bytes data, err := yaml.Marshal(tc.in) if err != nil { t.Fatalf("unable to marshal input to yaml: %s", err) } // parse bytes to object err = yaml.UnmarshalContext(ctx, data, &got) if err == nil { t.Fatalf("expect parsing to fail") } errStr := err.Error() if !strings.Contains(errStr, tc.err) { t.Fatalf("unexpected error: got %q, want to contain %q", errStr, tc.err) } }) } } // ... (Remaining test functions do not involve parameter definitions and need no changes) func TestConvertArrayParamToString(t *testing.T) { tcs := []struct { name string in []any want string }{ { in: []any{ "id", "name", "location", }, want: "id, name, location", }, { in: []any{ "id", }, want: "id", }, { in: []any{ "id", "5", "false", }, want: "id, 5, false", }, { in: []any{}, want: "", }, { in: []any{}, want: "", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got, _ := tools.ConvertArrayParamToString(tc.in) if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("incorrect array param conversion: diff %v", diff) } }) } } func TestFailConvertArrayParamToString(t *testing.T) { tcs := []struct { name string in []any err string }{ { in: []any{5, 10, 15}, err: "templateParameter only supports string arrays", }, { in: []any{"id", "name", 15}, err: "templateParameter only supports string arrays", }, { in: []any{false}, err: "templateParameter only supports string arrays", }, { in: []any{10, true}, err: "templateParameter only supports string arrays", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { _, err := tools.ConvertArrayParamToString(tc.in) errStr := err.Error() if errStr != tc.err { t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) } }) } } func TestGetParams(t *testing.T) { tcs := []struct { name string in map[string]any params tools.Parameters want tools.ParamValues }{ { name: "parameters to include and exclude", params: tools.Parameters{ tools.NewStringParameter("my_string_inc", "this should be included"), tools.NewStringParameter("my_string_inc2", "this should be included"), }, in: map[string]any{ "my_string_inc": "hello world A", "my_string_inc2": "hello world B", "my_string_exc": "hello world C", }, want: tools.ParamValues{ tools.ParamValue{Name: "my_string_inc", Value: "hello world A"}, tools.ParamValue{Name: "my_string_inc2", Value: "hello world B"}, }, }, { name: "include all", params: tools.Parameters{ tools.NewStringParameter("my_string_inc", "this should be included"), }, in: map[string]any{ "my_string_inc": "hello world A", }, want: tools.ParamValues{ tools.ParamValue{Name: "my_string_inc", Value: "hello world A"}, }, }, { name: "exclude all", params: tools.Parameters{}, in: map[string]any{ "my_string_exc": "hello world A", "my_string_exc2": "hello world B", }, want: tools.ParamValues{}, }, { name: "empty", params: tools.Parameters{}, in: map[string]any{}, want: tools.ParamValues{}, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got, _ := tools.GetParams(tc.params, tc.in) if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("incorrect get params: diff %v", diff) } }) } } func TestFailGetParams(t *testing.T) { tcs := []struct { name string params tools.Parameters in map[string]any err string }{ { name: "missing the only parameter", params: tools.Parameters{tools.NewStringParameter("my_string", "this was missing")}, in: map[string]any{}, err: "missing parameter my_string", }, { name: "missing one parameter of multiple", params: tools.Parameters{ tools.NewStringParameter("my_string_inc", "this should be included"), tools.NewStringParameter("my_string_exc", "this was missing"), }, in: map[string]any{ "my_string_inc": "hello world A", }, err: "missing parameter my_string_exc", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { _, err := tools.GetParams(tc.params, tc.in) errStr := err.Error() if errStr != tc.err { t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) } }) } } func TestResolveTemplateParameters(t *testing.T) { tcs := []struct { name string templateParams tools.Parameters statement string in map[string]any want string }{ { name: "single template parameter", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), }, statement: "SELECT * FROM {{.tableName}}", in: map[string]any{ "tableName": "hotels", }, want: "SELECT * FROM hotels", }, { name: "multiple template parameters", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), tools.NewStringParameter("columnName", "this is a string template parameter"), }, statement: "SELECT * FROM {{.tableName}} WHERE {{.columnName}} = 'Hilton'", in: map[string]any{ "tableName": "hotels", "columnName": "name", }, want: "SELECT * FROM hotels WHERE name = 'Hilton'", }, { name: "standard and template parameter", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), tools.NewStringParameter("hotelName", "this is a string parameter"), }, statement: "SELECT * FROM {{.tableName}} WHERE name = $1", in: map[string]any{ "tableName": "hotels", "hotelName": "name", }, want: "SELECT * FROM hotels WHERE name = $1", }, { name: "standard parameter", templateParams: tools.Parameters{ tools.NewStringParameter("hotelName", "this is a string parameter"), }, statement: "SELECT * FROM hotels WHERE name = $1", in: map[string]any{ "hotelName": "hotels", }, want: "SELECT * FROM hotels WHERE name = $1", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got, _ := tools.ResolveTemplateParams(tc.templateParams, tc.statement, tc.in) if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatalf("incorrect resolved template params: diff %v", diff) } }) } } func TestFailResolveTemplateParameters(t *testing.T) { tcs := []struct { name string templateParams tools.Parameters statement string in map[string]any err string }{ { name: "wrong param name", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), }, statement: "SELECT * FROM {{.missingParam}}", in: map[string]any{}, err: "error getting template params missing parameter tableName", }, { name: "incomplete param template", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), }, statement: "SELECT * FROM {{.tableName", in: map[string]any{ "tableName": "hotels", }, err: "error creating go template template: statement:1: unclosed action", }, { name: "undefined function", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), }, statement: "SELECT * FROM {{json .tableName}}", in: map[string]any{ "tableName": "hotels", }, err: "error creating go template template: statement:1: function \"json\" not defined", }, { name: "undefined method", templateParams: tools.Parameters{ tools.NewStringParameter("tableName", "this is a string template parameter"), }, statement: "SELECT * FROM {{.tableName .wrong}}", in: map[string]any{ "tableName": "hotels", }, err: "error executing go template template: statement:1:16: executing \"statement\" at <.tableName>: tableName is not a method but has arguments", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { _, err := tools.ResolveTemplateParams(tc.templateParams, tc.statement, tc.in) errStr := err.Error() if errStr != tc.err { t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err) } }) } } func TestCheckParamRequired(t *testing.T) { tcs := []struct { name string required bool defaultV any want bool }{ { name: "required and no default", required: true, defaultV: nil, want: true, }, { name: "required and default", required: true, defaultV: "foo", want: false, }, { name: "not required and no default", required: false, defaultV: nil, want: false, }, { name: "not required and default", required: false, defaultV: "foo", want: false, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got := tools.CheckParamRequired(tc.required, tc.defaultV) if got != tc.want { t.Fatalf("got %v, want %v", got, tc.want) } }) } } ```