This is page 54 of 59. Use http://codebase.md/googleapis/genai-toolbox?lines=true&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
│ │ │ ├── adkgo
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── 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-healthcare.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
│ │ │ ├── elasticsearch.md
│ │ │ ├── firebird.md
│ │ │ ├── firestore.md
│ │ │ ├── http.md
│ │ │ ├── looker.md
│ │ │ ├── mindsdb.md
│ │ │ ├── mongodb.md
│ │ │ ├── mssql.md
│ │ │ ├── mysql.md
│ │ │ ├── neo4j.md
│ │ │ ├── oceanbase.md
│ │ │ ├── oracle.md
│ │ │ ├── postgres.md
│ │ │ ├── redis.md
│ │ │ ├── serverless-spark.md
│ │ │ ├── singlestore.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
│ │ ├── cloudhealthcare
│ │ │ ├── _index.md
│ │ │ ├── cloud-healthcare-fhir-fetch-page.md
│ │ │ ├── cloud-healthcare-fhir-patient-everything.md
│ │ │ ├── cloud-healthcare-fhir-patient-search.md
│ │ │ ├── cloud-healthcare-get-dataset.md
│ │ │ ├── cloud-healthcare-get-dicom-store-metrics.md
│ │ │ ├── cloud-healthcare-get-dicom-store.md
│ │ │ ├── cloud-healthcare-get-fhir-resource.md
│ │ │ ├── cloud-healthcare-get-fhir-store-metrics.md
│ │ │ ├── cloud-healthcare-get-fhir-store.md
│ │ │ ├── cloud-healthcare-list-dicom-stores.md
│ │ │ ├── cloud-healthcare-list-fhir-stores.md
│ │ │ ├── cloud-healthcare-retrieve-rendered-dicom-instance.md
│ │ │ ├── cloud-healthcare-search-dicom-instances.md
│ │ │ ├── cloud-healthcare-search-dicom-series.md
│ │ │ └── cloud-healthcare-search-dicom-studies.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
│ │ ├── elasticsearch
│ │ │ ├── _index.md
│ │ │ └── elasticsearch-esql.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-create-project-file.md
│ │ │ ├── looker-delete-project-file.md
│ │ │ ├── looker-dev-mode.md
│ │ │ ├── looker-get-connection-databases.md
│ │ │ ├── looker-get-connection-schemas.md
│ │ │ ├── looker-get-connection-table-columns.md
│ │ │ ├── looker-get-connection-tables.md
│ │ │ ├── looker-get-connections.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-get-project-file.md
│ │ │ ├── looker-get-project-files.md
│ │ │ ├── looker-get-projects.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-dashboard.md
│ │ │ ├── looker-run-look.md
│ │ │ └── looker-update-project-file.md
│ │ ├── mindsdb
│ │ │ ├── _index.md
│ │ │ ├── mindsdb-execute-sql.md
│ │ │ └── mindsdb-sql.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-schemas.md
│ │ │ ├── postgres-list-tables.md
│ │ │ ├── postgres-list-views.md
│ │ │ └── postgres-sql.md
│ │ ├── redis
│ │ │ ├── _index.md
│ │ │ └── redis.md
│ │ ├── serverless-spark
│ │ │ ├── _index.md
│ │ │ ├── serverless-spark-cancel-batch.md
│ │ │ ├── serverless-spark-get-batch.md
│ │ │ └── serverless-spark-list-batches.md
│ │ ├── singlestore
│ │ │ ├── _index.md
│ │ │ ├── singlestore-execute-sql.md
│ │ │ └── singlestore-sql.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-healthcare.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
│ │ ├── elasticsearch.yaml
│ │ ├── firestore.yaml
│ │ ├── looker-conversational-analytics.yaml
│ │ ├── looker.yaml
│ │ ├── mindsdb.yaml
│ │ ├── mssql.yaml
│ │ ├── mysql.yaml
│ │ ├── neo4j.yaml
│ │ ├── oceanbase.yaml
│ │ ├── postgres.yaml
│ │ ├── serverless-spark.yaml
│ │ ├── singlestore.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
│ │ │ └── cache.go
│ │ ├── bigtable
│ │ │ ├── bigtable_test.go
│ │ │ └── bigtable.go
│ │ ├── cassandra
│ │ │ ├── cassandra_test.go
│ │ │ └── cassandra.go
│ │ ├── clickhouse
│ │ │ ├── clickhouse_test.go
│ │ │ └── clickhouse.go
│ │ ├── cloudhealthcare
│ │ │ ├── cloud_healthcare_test.go
│ │ │ └── cloud_healthcare.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
│ │ ├── elasticsearch
│ │ │ ├── elasticsearch_test.go
│ │ │ └── elasticsearch.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
│ │ ├── mindsdb
│ │ │ ├── mindsdb_test.go
│ │ │ └── mindsdb.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
│ │ ├── serverlessspark
│ │ │ ├── serverlessspark_test.go
│ │ │ └── serverlessspark.go
│ │ ├── singlestore
│ │ │ ├── singlestore_test.go
│ │ │ └── singlestore.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
│ │ ├── cloudhealthcare
│ │ │ ├── cloudhealthcarefhirfetchpage
│ │ │ │ ├── cloudhealthcarefhirfetchpage_test.go
│ │ │ │ └── cloudhealthcarefhirfetchpage.go
│ │ │ ├── cloudhealthcarefhirpatienteverything
│ │ │ │ ├── cloudhealthcarefhirpatienteverything_test.go
│ │ │ │ └── cloudhealthcarefhirpatienteverything.go
│ │ │ ├── cloudhealthcarefhirpatientsearch
│ │ │ │ ├── cloudhealthcarefhirpatientsearch_test.go
│ │ │ │ └── cloudhealthcarefhirpatientsearch.go
│ │ │ ├── cloudhealthcaregetdataset
│ │ │ │ ├── cloudhealthcaregetdataset_test.go
│ │ │ │ └── cloudhealthcaregetdataset.go
│ │ │ ├── cloudhealthcaregetdicomstore
│ │ │ │ ├── cloudhealthcaregetdicomstore_test.go
│ │ │ │ └── cloudhealthcaregetdicomstore.go
│ │ │ ├── cloudhealthcaregetdicomstoremetrics
│ │ │ │ ├── cloudhealthcaregetdicomstoremetrics_test.go
│ │ │ │ └── cloudhealthcaregetdicomstoremetrics.go
│ │ │ ├── cloudhealthcaregetfhirresource
│ │ │ │ ├── cloudhealthcaregetfhirresource_test.go
│ │ │ │ └── cloudhealthcaregetfhirresource.go
│ │ │ ├── cloudhealthcaregetfhirstore
│ │ │ │ ├── cloudhealthcaregetfhirstore_test.go
│ │ │ │ └── cloudhealthcaregetfhirstore.go
│ │ │ ├── cloudhealthcaregetfhirstoremetrics
│ │ │ │ ├── cloudhealthcaregetfhirstoremetrics_test.go
│ │ │ │ └── cloudhealthcaregetfhirstoremetrics.go
│ │ │ ├── cloudhealthcarelistdicomstores
│ │ │ │ ├── cloudhealthcarelistdicomstores_test.go
│ │ │ │ └── cloudhealthcarelistdicomstores.go
│ │ │ ├── cloudhealthcarelistfhirstores
│ │ │ │ ├── cloudhealthcarelistfhirstores_test.go
│ │ │ │ └── cloudhealthcarelistfhirstores.go
│ │ │ ├── cloudhealthcareretrieverendereddicominstance
│ │ │ │ ├── cloudhealthcareretrieverendereddicominstance_test.go
│ │ │ │ └── cloudhealthcareretrieverendereddicominstance.go
│ │ │ ├── cloudhealthcaresearchdicominstances
│ │ │ │ ├── cloudhealthcaresearchdicominstances_test.go
│ │ │ │ └── cloudhealthcaresearchdicominstances.go
│ │ │ ├── cloudhealthcaresearchdicomseries
│ │ │ │ ├── cloudhealthcaresearchdicomseries_test.go
│ │ │ │ └── cloudhealthcaresearchdicomseries.go
│ │ │ ├── cloudhealthcaresearchdicomstudies
│ │ │ │ ├── cloudhealthcaresearchdicomstudies_test.go
│ │ │ │ └── cloudhealthcaresearchdicomstudies.go
│ │ │ └── common
│ │ │ └── util.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
│ │ ├── elasticsearch
│ │ │ └── elasticsearchesql
│ │ │ ├── elasticsearchesql_test.go
│ │ │ └── elasticsearchesql.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
│ │ │ ├── lookercreateprojectfile
│ │ │ │ ├── lookercreateprojectfile_test.go
│ │ │ │ └── lookercreateprojectfile.go
│ │ │ ├── lookerdeleteprojectfile
│ │ │ │ ├── lookerdeleteprojectfile_test.go
│ │ │ │ └── lookerdeleteprojectfile.go
│ │ │ ├── lookerdevmode
│ │ │ │ ├── lookerdevmode_test.go
│ │ │ │ └── lookerdevmode.go
│ │ │ ├── lookergetconnectiondatabases
│ │ │ │ ├── lookergetconnectiondatabases_test.go
│ │ │ │ └── lookergetconnectiondatabases.go
│ │ │ ├── lookergetconnections
│ │ │ │ ├── lookergetconnections_test.go
│ │ │ │ └── lookergetconnections.go
│ │ │ ├── lookergetconnectionschemas
│ │ │ │ ├── lookergetconnectionschemas_test.go
│ │ │ │ └── lookergetconnectionschemas.go
│ │ │ ├── lookergetconnectiontablecolumns
│ │ │ │ ├── lookergetconnectiontablecolumns_test.go
│ │ │ │ └── lookergetconnectiontablecolumns.go
│ │ │ ├── lookergetconnectiontables
│ │ │ │ ├── lookergetconnectiontables_test.go
│ │ │ │ └── lookergetconnectiontables.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
│ │ │ ├── lookergetprojectfile
│ │ │ │ ├── lookergetprojectfile_test.go
│ │ │ │ └── lookergetprojectfile.go
│ │ │ ├── lookergetprojectfiles
│ │ │ │ ├── lookergetprojectfiles_test.go
│ │ │ │ └── lookergetprojectfiles.go
│ │ │ ├── lookergetprojects
│ │ │ │ ├── lookergetprojects_test.go
│ │ │ │ └── lookergetprojects.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
│ │ │ ├── lookerrundashboard
│ │ │ │ ├── lookerrundashboard_test.go
│ │ │ │ └── lookerrundashboard.go
│ │ │ ├── lookerrunlook
│ │ │ │ ├── lookerrunlook_test.go
│ │ │ │ └── lookerrunlook.go
│ │ │ └── lookerupdateprojectfile
│ │ │ ├── lookerupdateprojectfile_test.go
│ │ │ └── lookerupdateprojectfile.go
│ │ ├── mindsdb
│ │ │ ├── mindsdbexecutesql
│ │ │ │ ├── mindsdbexecutesql_test.go
│ │ │ │ └── mindsdbexecutesql.go
│ │ │ └── mindsdbsql
│ │ │ ├── mindsdbsql_test.go
│ │ │ └── mindsdbsql.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
│ │ │ ├── postgreslistschemas
│ │ │ │ ├── postgreslistschemas_test.go
│ │ │ │ └── postgreslistschemas.go
│ │ │ ├── postgreslisttables
│ │ │ │ ├── postgreslisttables_test.go
│ │ │ │ └── postgreslisttables.go
│ │ │ ├── postgreslistviews
│ │ │ │ ├── postgreslistviews_test.go
│ │ │ │ └── postgreslistviews.go
│ │ │ └── postgressql
│ │ │ ├── postgressql_test.go
│ │ │ └── postgressql.go
│ │ ├── redis
│ │ │ ├── redis_test.go
│ │ │ └── redis.go
│ │ ├── serverlessspark
│ │ │ ├── serverlesssparkcancelbatch
│ │ │ │ ├── serverlesssparkcancelbatch_test.go
│ │ │ │ └── serverlesssparkcancelbatch.go
│ │ │ ├── serverlesssparkgetbatch
│ │ │ │ ├── serverlesssparkgetbatch_test.go
│ │ │ │ └── serverlesssparkgetbatch.go
│ │ │ └── serverlesssparklistbatches
│ │ │ ├── serverlesssparklistbatches_test.go
│ │ │ └── serverlesssparklistbatches.go
│ │ ├── singlestore
│ │ │ ├── singlestoreexecutesql
│ │ │ │ ├── singlestoreexecutesql_test.go
│ │ │ │ └── singlestoreexecutesql.go
│ │ │ └── singlestoresql
│ │ │ ├── singlestoresql_test.go
│ │ │ └── singlestoresql.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
│ ├── orderedmap
│ │ ├── orderedmap_test.go
│ │ └── orderedmap.go
│ └── 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
├── cloudhealthcare
│ └── cloud_healthcare_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
├── elasticsearch
│ └── elasticsearch_integration_test.go
├── firebird
│ └── firebird_integration_test.go
├── firestore
│ └── firestore_integration_test.go
├── http
│ └── http_integration_test.go
├── looker
│ └── looker_integration_test.go
├── mindsdb
│ └── mindsdb_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
├── serverlessspark
│ └── serverless_spark_integration_test.go
├── singlestore
│ └── singlestore_integration_test.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/firestore/firestore_integration_test.go:
--------------------------------------------------------------------------------
```go
1 | // Copyright 2025 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package firestore
16 |
17 | import (
18 | "bytes"
19 | "context"
20 | "encoding/json"
21 | "fmt"
22 | "io"
23 | "net/http"
24 | "os"
25 | "reflect"
26 | "regexp"
27 | "strings"
28 | "testing"
29 | "time"
30 |
31 | firestoreapi "cloud.google.com/go/firestore"
32 | "github.com/google/uuid"
33 | "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
34 | "github.com/googleapis/genai-toolbox/internal/testutils"
35 | "github.com/googleapis/genai-toolbox/tests"
36 | "google.golang.org/api/option"
37 | )
38 |
39 | var (
40 | FirestoreSourceKind = "firestore"
41 | FirestoreProject = os.Getenv("FIRESTORE_PROJECT")
42 | FirestoreDatabase = os.Getenv("FIRESTORE_DATABASE") // Optional, defaults to "(default)"
43 | )
44 |
45 | func getFirestoreVars(t *testing.T) map[string]any {
46 | if FirestoreProject == "" {
47 | t.Fatal("'FIRESTORE_PROJECT' not set")
48 | }
49 |
50 | vars := map[string]any{
51 | "kind": FirestoreSourceKind,
52 | "project": FirestoreProject,
53 | }
54 |
55 | // Only add database if it's explicitly set
56 | if FirestoreDatabase != "" {
57 | vars["database"] = FirestoreDatabase
58 | }
59 |
60 | return vars
61 | }
62 |
63 | // initFirestoreConnection creates a Firestore client for testing
64 | func initFirestoreConnection(project, database string) (*firestoreapi.Client, error) {
65 | ctx := context.Background()
66 |
67 | if database == "" {
68 | database = "(default)"
69 | }
70 |
71 | client, err := firestoreapi.NewClientWithDatabase(ctx, project, database, option.WithUserAgent("genai-toolbox-integration-test"))
72 | if err != nil {
73 | return nil, fmt.Errorf("failed to create Firestore client for project %q and database %q: %w", project, database, err)
74 | }
75 | return client, nil
76 | }
77 |
78 | func TestFirestoreToolEndpoints(t *testing.T) {
79 | sourceConfig := getFirestoreVars(t)
80 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
81 | defer cancel()
82 |
83 | var args []string
84 |
85 | client, err := initFirestoreConnection(FirestoreProject, FirestoreDatabase)
86 | if err != nil {
87 | t.Fatalf("unable to create Firestore connection: %s", err)
88 | }
89 | defer client.Close()
90 |
91 | // Create test collection and document names with UUID
92 | testCollectionName := fmt.Sprintf("test_collection_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
93 | testSubCollectionName := fmt.Sprintf("test_subcollection_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
94 | testDocID1 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
95 | testDocID2 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
96 | testDocID3 := fmt.Sprintf("doc_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
97 |
98 | // Document paths for testing
99 | docPath1 := fmt.Sprintf("%s/%s", testCollectionName, testDocID1)
100 | docPath2 := fmt.Sprintf("%s/%s", testCollectionName, testDocID2)
101 | docPath3 := fmt.Sprintf("%s/%s", testCollectionName, testDocID3)
102 |
103 | // Set up test data
104 | teardown := setupFirestoreTestData(t, ctx, client, testCollectionName, testSubCollectionName,
105 | testDocID1, testDocID2, testDocID3)
106 | defer teardown(t)
107 |
108 | // Write config into a file and pass it to command
109 | toolsFile := getFirestoreToolsConfig(sourceConfig)
110 |
111 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
112 | if err != nil {
113 | t.Fatalf("command initialization returned an error: %s", err)
114 | }
115 | defer cleanup()
116 |
117 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
118 | defer cancel()
119 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
120 | if err != nil {
121 | t.Logf("toolbox command logs: \n%s", out)
122 | t.Fatalf("toolbox didn't start successfully: %s", err)
123 | }
124 |
125 | // Run Firestore-specific tool get test
126 | runFirestoreToolGetTest(t)
127 |
128 | // Run Firestore-specific MCP test
129 | runFirestoreMCPToolCallMethod(t, docPath1, docPath2)
130 |
131 | // Run specific Firestore tool tests
132 | runFirestoreGetDocumentsTest(t, docPath1, docPath2)
133 | runFirestoreQueryCollectionTest(t, testCollectionName)
134 | runFirestoreQueryTest(t, testCollectionName)
135 | runFirestoreQuerySelectArrayTest(t, testCollectionName)
136 | runFirestoreListCollectionsTest(t, testCollectionName, testSubCollectionName, docPath1)
137 | runFirestoreAddDocumentsTest(t, testCollectionName)
138 | runFirestoreUpdateDocumentTest(t, testCollectionName, testDocID1)
139 | runFirestoreDeleteDocumentsTest(t, docPath3)
140 | runFirestoreGetRulesTest(t)
141 | runFirestoreValidateRulesTest(t)
142 | }
143 |
144 | func runFirestoreToolGetTest(t *testing.T) {
145 | // Test tool get endpoint for Firestore tools
146 | tcs := []struct {
147 | name string
148 | api string
149 | want map[string]any
150 | }{
151 | {
152 | name: "get my-simple-tool",
153 | api: "http://127.0.0.1:5000/api/tool/my-simple-tool/",
154 | want: map[string]any{
155 | "my-simple-tool": map[string]any{
156 | "description": "Simple tool to test end to end functionality.",
157 | "parameters": []any{
158 | map[string]any{
159 | "name": "documentPaths",
160 | "type": "array",
161 | "required": true,
162 | "description": "Array of document paths to retrieve from Firestore.",
163 | "items": map[string]any{
164 | "name": "item",
165 | "type": "string",
166 | "required": true,
167 | "description": "Document path",
168 | "authSources": []any{},
169 | },
170 | "authSources": []any{},
171 | },
172 | },
173 | "authRequired": []any{},
174 | },
175 | },
176 | },
177 | }
178 |
179 | for _, tc := range tcs {
180 | t.Run(tc.name, func(t *testing.T) {
181 | resp, err := http.Get(tc.api)
182 | if err != nil {
183 | t.Fatalf("error when sending a request: %s", err)
184 | }
185 | defer resp.Body.Close()
186 | if resp.StatusCode != 200 {
187 | t.Fatalf("response status code is not 200")
188 | }
189 |
190 | var body map[string]interface{}
191 | err = json.NewDecoder(resp.Body).Decode(&body)
192 | if err != nil {
193 | t.Fatalf("error parsing response body")
194 | }
195 |
196 | got, ok := body["tools"]
197 | if !ok {
198 | t.Fatalf("unable to find tools in response body")
199 | }
200 |
201 | // Compare as JSON strings to handle any ordering differences
202 | gotJSON, _ := json.Marshal(got)
203 | wantJSON, _ := json.Marshal(tc.want)
204 | if string(gotJSON) != string(wantJSON) {
205 | t.Logf("got %v, want %v", got, tc.want)
206 | }
207 | })
208 | }
209 | }
210 |
211 | func runFirestoreValidateRulesTest(t *testing.T) {
212 | invokeTcs := []struct {
213 | name string
214 | api string
215 | requestBody io.Reader
216 | wantRegex string
217 | isErr bool
218 | }{
219 | {
220 | name: "validate valid rules",
221 | api: "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
222 | requestBody: bytes.NewBuffer([]byte(`{
223 | "source": "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if true;\n }\n }\n}"
224 | }`)),
225 | wantRegex: `"valid":true.*"issueCount":0`,
226 | isErr: false,
227 | },
228 | {
229 | name: "validate rules with syntax error",
230 | api: "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
231 | requestBody: bytes.NewBuffer([]byte(`{
232 | "source": "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if true;;\n }\n }\n}"
233 | }`)),
234 | wantRegex: `"valid":false.*"issueCount":[1-9]`,
235 | isErr: false,
236 | },
237 | {
238 | name: "validate rules with missing version",
239 | api: "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
240 | requestBody: bytes.NewBuffer([]byte(`{
241 | "source": "service cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if true;\n }\n }\n}"
242 | }`)),
243 | wantRegex: `"valid":false.*"issueCount":[1-9]`,
244 | isErr: false,
245 | },
246 | {
247 | name: "validate empty rules",
248 | api: "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
249 | requestBody: bytes.NewBuffer([]byte(`{"source": ""}`)),
250 | isErr: true,
251 | },
252 | {
253 | name: "missing source parameter",
254 | api: "http://127.0.0.1:5000/api/tool/firestore-validate-rules/invoke",
255 | requestBody: bytes.NewBuffer([]byte(`{}`)),
256 | isErr: true,
257 | },
258 | }
259 |
260 | for _, tc := range invokeTcs {
261 | t.Run(tc.name, func(t *testing.T) {
262 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
263 | if err != nil {
264 | t.Fatalf("unable to create request: %s", err)
265 | }
266 | req.Header.Add("Content-type", "application/json")
267 |
268 | resp, err := http.DefaultClient.Do(req)
269 | if err != nil {
270 | t.Fatalf("unable to send request: %s", err)
271 | }
272 | defer resp.Body.Close()
273 |
274 | if resp.StatusCode != http.StatusOK {
275 | if tc.isErr {
276 | return
277 | }
278 | bodyBytes, _ := io.ReadAll(resp.Body)
279 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
280 | }
281 |
282 | var body map[string]interface{}
283 | err = json.NewDecoder(resp.Body).Decode(&body)
284 | if err != nil {
285 | t.Fatalf("error parsing response body: %v", err)
286 | }
287 |
288 | got, ok := body["result"].(string)
289 | if !ok {
290 | t.Fatalf("unable to find result in response body")
291 | }
292 |
293 | if tc.wantRegex != "" {
294 | matched, err := regexp.MatchString(tc.wantRegex, got)
295 | if err != nil {
296 | t.Fatalf("invalid regex pattern: %v", err)
297 | }
298 | if !matched {
299 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
300 | }
301 | }
302 | })
303 | }
304 | }
305 |
306 | func runFirestoreGetRulesTest(t *testing.T) {
307 | invokeTcs := []struct {
308 | name string
309 | api string
310 | requestBody io.Reader
311 | wantRegex string
312 | isErr bool
313 | }{
314 | {
315 | name: "get firestore rules",
316 | api: "http://127.0.0.1:5000/api/tool/firestore-get-rules/invoke",
317 | requestBody: bytes.NewBuffer([]byte(`{}`)),
318 | wantRegex: `"content":"[^"]+"`, // Should contain at least one of these fields
319 | isErr: false,
320 | },
321 | }
322 |
323 | for _, tc := range invokeTcs {
324 | t.Run(tc.name, func(t *testing.T) {
325 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
326 | if err != nil {
327 | t.Fatalf("unable to create request: %s", err)
328 | }
329 | req.Header.Add("Content-type", "application/json")
330 |
331 | resp, err := http.DefaultClient.Do(req)
332 | if err != nil {
333 | t.Fatalf("unable to send request: %s", err)
334 | }
335 | defer resp.Body.Close()
336 |
337 | if resp.StatusCode != http.StatusOK {
338 | bodyBytes, _ := io.ReadAll(resp.Body)
339 | // The test might fail if there are no active rules in the project, which is acceptable
340 | if strings.Contains(string(bodyBytes), "no active Firestore rules") {
341 | t.Skipf("No active Firestore rules found in the project")
342 | return
343 | }
344 | if tc.isErr {
345 | return
346 | }
347 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
348 | }
349 |
350 | var body map[string]interface{}
351 | err = json.NewDecoder(resp.Body).Decode(&body)
352 | if err != nil {
353 | t.Fatalf("error parsing response body: %v", err)
354 | }
355 |
356 | got, ok := body["result"].(string)
357 | if !ok {
358 | t.Fatalf("unable to find result in response body")
359 | }
360 |
361 | if tc.wantRegex != "" {
362 | matched, err := regexp.MatchString(tc.wantRegex, got)
363 | if err != nil {
364 | t.Fatalf("invalid regex pattern: %v", err)
365 | }
366 | if !matched {
367 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
368 | }
369 | }
370 | })
371 | }
372 | }
373 |
374 | func runFirestoreMCPToolCallMethod(t *testing.T, docPath1, docPath2 string) {
375 | sessionId := tests.RunInitialize(t, "2024-11-05")
376 | header := map[string]string{}
377 | if sessionId != "" {
378 | header["Mcp-Session-Id"] = sessionId
379 | }
380 |
381 | // Test tool invoke endpoint
382 | invokeTcs := []struct {
383 | name string
384 | api string
385 | requestBody jsonrpc.JSONRPCRequest
386 | requestHeader map[string]string
387 | wantContains string
388 | wantError bool
389 | }{
390 | {
391 | name: "MCP Invoke my-param-tool",
392 | api: "http://127.0.0.1:5000/mcp",
393 | requestHeader: map[string]string{},
394 | requestBody: jsonrpc.JSONRPCRequest{
395 | Jsonrpc: "2.0",
396 | Id: "my-param-tool",
397 | Request: jsonrpc.Request{
398 | Method: "tools/call",
399 | },
400 | Params: map[string]any{
401 | "name": "my-param-tool",
402 | "arguments": map[string]any{
403 | "documentPaths": []string{docPath1},
404 | },
405 | },
406 | },
407 | wantContains: `\"name\":\"Alice\"`,
408 | wantError: false,
409 | },
410 | {
411 | name: "MCP Invoke invalid tool",
412 | api: "http://127.0.0.1:5000/mcp",
413 | requestHeader: map[string]string{},
414 | requestBody: jsonrpc.JSONRPCRequest{
415 | Jsonrpc: "2.0",
416 | Id: "invalid-tool",
417 | Request: jsonrpc.Request{
418 | Method: "tools/call",
419 | },
420 | Params: map[string]any{
421 | "name": "foo",
422 | "arguments": map[string]any{},
423 | },
424 | },
425 | wantContains: `tool with name \"foo\" does not exist`,
426 | wantError: true,
427 | },
428 | {
429 | name: "MCP Invoke my-param-tool without parameters",
430 | api: "http://127.0.0.1:5000/mcp",
431 | requestHeader: map[string]string{},
432 | requestBody: jsonrpc.JSONRPCRequest{
433 | Jsonrpc: "2.0",
434 | Id: "invoke-without-parameter",
435 | Request: jsonrpc.Request{
436 | Method: "tools/call",
437 | },
438 | Params: map[string]any{
439 | "name": "my-param-tool",
440 | "arguments": map[string]any{},
441 | },
442 | },
443 | wantContains: `parameter \"documentPaths\" is required`,
444 | wantError: true,
445 | },
446 | {
447 | name: "MCP Invoke my-auth-required-tool",
448 | api: "http://127.0.0.1:5000/mcp",
449 | requestHeader: map[string]string{},
450 | requestBody: jsonrpc.JSONRPCRequest{
451 | Jsonrpc: "2.0",
452 | Id: "invoke my-auth-required-tool",
453 | Request: jsonrpc.Request{
454 | Method: "tools/call",
455 | },
456 | Params: map[string]any{
457 | "name": "my-auth-required-tool",
458 | "arguments": map[string]any{},
459 | },
460 | },
461 | wantContains: `tool with name \"my-auth-required-tool\" does not exist`,
462 | wantError: true,
463 | },
464 | {
465 | name: "MCP Invoke my-fail-tool",
466 | api: "http://127.0.0.1:5000/mcp",
467 | requestHeader: map[string]string{},
468 | requestBody: jsonrpc.JSONRPCRequest{
469 | Jsonrpc: "2.0",
470 | Id: "invoke-fail-tool",
471 | Request: jsonrpc.Request{
472 | Method: "tools/call",
473 | },
474 | Params: map[string]any{
475 | "name": "my-fail-tool",
476 | "arguments": map[string]any{
477 | "documentPaths": []string{"non-existent/path"},
478 | },
479 | },
480 | },
481 | wantContains: `\"exists\":false`,
482 | wantError: false,
483 | },
484 | }
485 |
486 | for _, tc := range invokeTcs {
487 | t.Run(tc.name, func(t *testing.T) {
488 | reqMarshal, err := json.Marshal(tc.requestBody)
489 | if err != nil {
490 | t.Fatalf("unexpected error during marshaling of request body")
491 | }
492 |
493 | req, err := http.NewRequest(http.MethodPost, tc.api, bytes.NewBuffer(reqMarshal))
494 | if err != nil {
495 | t.Fatalf("unable to create request: %s", err)
496 | }
497 | req.Header.Add("Content-type", "application/json")
498 | for k, v := range header {
499 | req.Header.Add(k, v)
500 | }
501 |
502 | resp, err := http.DefaultClient.Do(req)
503 | if err != nil {
504 | t.Fatalf("unable to send request: %s", err)
505 | }
506 | defer resp.Body.Close()
507 |
508 | respBody, err := io.ReadAll(resp.Body)
509 | if err != nil {
510 | t.Fatalf("unable to read request body: %s", err)
511 | }
512 |
513 | got := string(bytes.TrimSpace(respBody))
514 |
515 | if !strings.Contains(got, tc.wantContains) {
516 | t.Fatalf("Expected substring not found:\ngot: %q\nwant: %q (to be contained within got)", got, tc.wantContains)
517 | }
518 | })
519 | }
520 | }
521 |
522 | func getFirestoreToolsConfig(sourceConfig map[string]any) map[string]any {
523 | sources := map[string]any{
524 | "my-instance": sourceConfig,
525 | }
526 |
527 | tools := map[string]any{
528 | // Tool for RunToolGetTest
529 | "my-simple-tool": map[string]any{
530 | "kind": "firestore-get-documents",
531 | "source": "my-instance",
532 | "description": "Simple tool to test end to end functionality.",
533 | },
534 | // Tool for MCP test - this will get documents
535 | "my-param-tool": map[string]any{
536 | "kind": "firestore-get-documents",
537 | "source": "my-instance",
538 | "description": "Tool to get documents by paths",
539 | },
540 | // Tool for MCP test that fails
541 | "my-fail-tool": map[string]any{
542 | "kind": "firestore-get-documents",
543 | "source": "my-instance",
544 | "description": "Tool that will fail",
545 | },
546 | // Firestore specific tools
547 | "firestore-get-docs": map[string]any{
548 | "kind": "firestore-get-documents",
549 | "source": "my-instance",
550 | "description": "Get multiple documents from Firestore",
551 | },
552 | "firestore-list-colls": map[string]any{
553 | "kind": "firestore-list-collections",
554 | "source": "my-instance",
555 | "description": "List Firestore collections",
556 | },
557 | "firestore-delete-docs": map[string]any{
558 | "kind": "firestore-delete-documents",
559 | "source": "my-instance",
560 | "description": "Delete documents from Firestore",
561 | },
562 | "firestore-query-coll": map[string]any{
563 | "kind": "firestore-query-collection",
564 | "source": "my-instance",
565 | "description": "Query a Firestore collection",
566 | },
567 | "firestore-query-param": map[string]any{
568 | "kind": "firestore-query",
569 | "source": "my-instance",
570 | "description": "Query a Firestore collection with parameterizable filters",
571 | "collectionPath": "{{.collection}}",
572 | "filters": `{
573 | "field": "age", "op": "{{.operator}}", "value": {"integerValue": "{{.ageValue}}"}
574 | }`,
575 | "limit": 10,
576 | "parameters": []map[string]any{
577 | {
578 | "name": "collection",
579 | "type": "string",
580 | "description": "Collection to query",
581 | "required": true,
582 | },
583 | {
584 | "name": "operator",
585 | "type": "string",
586 | "description": "Comparison operator",
587 | "required": true,
588 | },
589 | {
590 | "name": "ageValue",
591 | "type": "string",
592 | "description": "Age value to compare",
593 | "required": true,
594 | },
595 | },
596 | },
597 | "firestore-query-select-array": map[string]any{
598 | "kind": "firestore-query",
599 | "source": "my-instance",
600 | "description": "Query with array-based select fields",
601 | "collectionPath": "{{.collection}}",
602 | "select": []string{"{{.fields}}"},
603 | "limit": 10,
604 | "parameters": []map[string]any{
605 | {
606 | "name": "collection",
607 | "type": "string",
608 | "description": "Collection to query",
609 | "required": true,
610 | },
611 | {
612 | "name": "fields",
613 | "type": "array",
614 | "description": "Fields to select",
615 | "required": true,
616 | "items": map[string]any{
617 | "name": "field",
618 | "type": "string",
619 | "description": "field",
620 | },
621 | },
622 | },
623 | },
624 | "firestore-get-rules": map[string]any{
625 | "kind": "firestore-get-rules",
626 | "source": "my-instance",
627 | "description": "Get Firestore security rules",
628 | },
629 | "firestore-validate-rules": map[string]any{
630 | "kind": "firestore-validate-rules",
631 | "source": "my-instance",
632 | "description": "Validate Firestore security rules",
633 | },
634 | "firestore-add-docs": map[string]any{
635 | "kind": "firestore-add-documents",
636 | "source": "my-instance",
637 | "description": "Add documents to Firestore",
638 | },
639 | "firestore-update-doc": map[string]any{
640 | "kind": "firestore-update-document",
641 | "source": "my-instance",
642 | "description": "Update a document in Firestore",
643 | },
644 | }
645 |
646 | return map[string]any{
647 | "sources": sources,
648 | "tools": tools,
649 | }
650 | }
651 |
652 | func runFirestoreUpdateDocumentTest(t *testing.T, collectionName string, docID string) {
653 | docPath := fmt.Sprintf("%s/%s", collectionName, docID)
654 |
655 | invokeTcs := []struct {
656 | name string
657 | api string
658 | requestBody io.Reader
659 | wantKeys []string
660 | validateContent bool
661 | expectedContent map[string]interface{}
662 | isErr bool
663 | }{
664 | {
665 | name: "update document with simple fields",
666 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
667 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
668 | "documentPath": "%s",
669 | "documentData": {
670 | "name": {"stringValue": "Alice Updated"},
671 | "status": {"stringValue": "active"}
672 | }
673 | }`, docPath))),
674 | wantKeys: []string{"documentPath", "updateTime"},
675 | isErr: false,
676 | },
677 | {
678 | name: "update document with selective fields using updateMask",
679 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
680 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
681 | "documentPath": "%s",
682 | "documentData": {
683 | "age": {"integerValue": "31"},
684 | "email": {"stringValue": "[email protected]"}
685 | },
686 | "updateMask": ["age"]
687 | }`, docPath))),
688 | wantKeys: []string{"documentPath", "updateTime"},
689 | isErr: false,
690 | },
691 | {
692 | name: "update document with field deletion",
693 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
694 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
695 | "documentPath": "%s",
696 | "documentData": {
697 | "name": {"stringValue": "Alice Final"}
698 | },
699 | "updateMask": ["name", "status"]
700 | }`, docPath))),
701 | wantKeys: []string{"documentPath", "updateTime"},
702 | isErr: false,
703 | },
704 | {
705 | name: "update document with complex types",
706 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
707 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
708 | "documentPath": "%s",
709 | "documentData": {
710 | "location": {
711 | "geoPointValue": {
712 | "latitude": 40.7128,
713 | "longitude": -74.0060
714 | }
715 | },
716 | "tags": {
717 | "arrayValue": {
718 | "values": [
719 | {"stringValue": "updated"},
720 | {"stringValue": "test"}
721 | ]
722 | }
723 | },
724 | "metadata": {
725 | "mapValue": {
726 | "fields": {
727 | "lastModified": {"timestampValue": "2025-01-15T10:00:00Z"},
728 | "version": {"integerValue": "2"}
729 | }
730 | }
731 | }
732 | }
733 | }`, docPath))),
734 | wantKeys: []string{"documentPath", "updateTime"},
735 | isErr: false,
736 | },
737 | {
738 | name: "update document with returnData",
739 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
740 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
741 | "documentPath": "%s",
742 | "documentData": {
743 | "testField": {"stringValue": "test value"},
744 | "testNumber": {"integerValue": "42"}
745 | },
746 | "returnData": true
747 | }`, docPath))),
748 | wantKeys: []string{"documentPath", "updateTime", "documentData"},
749 | validateContent: true,
750 | expectedContent: map[string]interface{}{
751 | "testField": "test value",
752 | "testNumber": float64(42), // JSON numbers are decoded as float64
753 | },
754 | isErr: false,
755 | },
756 | {
757 | name: "update nested fields with updateMask",
758 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
759 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
760 | "documentPath": "%s",
761 | "documentData": {
762 | "profile": {
763 | "mapValue": {
764 | "fields": {
765 | "bio": {"stringValue": "Updated bio"},
766 | "avatar": {"stringValue": "avatar.jpg"}
767 | }
768 | }
769 | }
770 | },
771 | "updateMask": ["profile.bio", "profile.avatar"]
772 | }`, docPath))),
773 | wantKeys: []string{"documentPath", "updateTime"},
774 | isErr: false,
775 | },
776 | {
777 | name: "missing documentPath parameter",
778 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
779 | requestBody: bytes.NewBuffer([]byte(`{"documentData": {"test": {"stringValue": "value"}}}`)),
780 | isErr: true,
781 | },
782 | {
783 | name: "missing documentData parameter",
784 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
785 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPath": "%s"}`, docPath))),
786 | isErr: true,
787 | },
788 | {
789 | name: "update non-existent document",
790 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
791 | requestBody: bytes.NewBuffer([]byte(`{
792 | "documentPath": "non-existent-collection/non-existent-doc",
793 | "documentData": {
794 | "field": {"stringValue": "value"}
795 | }
796 | }`)),
797 | wantKeys: []string{"documentPath", "updateTime"}, // Set with MergeAll creates if doesn't exist
798 | isErr: false,
799 | },
800 | {
801 | name: "invalid field in updateMask",
802 | api: "http://127.0.0.1:5000/api/tool/firestore-update-doc/invoke",
803 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
804 | "documentPath": "%s",
805 | "documentData": {
806 | "field1": {"stringValue": "value1"}
807 | },
808 | "updateMask": ["field1", "nonExistentField"]
809 | }`, docPath))),
810 | isErr: true, // Should fail because nonExistentField is not in documentData
811 | },
812 | }
813 |
814 | for _, tc := range invokeTcs {
815 | t.Run(tc.name, func(t *testing.T) {
816 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
817 | if err != nil {
818 | t.Fatalf("unable to create request: %s", err)
819 | }
820 | req.Header.Add("Content-type", "application/json")
821 |
822 | resp, err := http.DefaultClient.Do(req)
823 | if err != nil {
824 | t.Fatalf("unable to send request: %s", err)
825 | }
826 | defer resp.Body.Close()
827 |
828 | if resp.StatusCode != http.StatusOK {
829 | if tc.isErr {
830 | return
831 | }
832 | bodyBytes, _ := io.ReadAll(resp.Body)
833 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
834 | }
835 |
836 | var body map[string]interface{}
837 | err = json.NewDecoder(resp.Body).Decode(&body)
838 | if err != nil {
839 | t.Fatalf("error parsing response body: %v", err)
840 | }
841 |
842 | got, ok := body["result"].(string)
843 | if !ok {
844 | t.Fatalf("unable to find result in response body")
845 | }
846 |
847 | // Parse the result string as JSON
848 | var resultJSON map[string]interface{}
849 | err = json.Unmarshal([]byte(got), &resultJSON)
850 | if err != nil {
851 | t.Fatalf("error parsing result as JSON: %v", err)
852 | }
853 |
854 | // Check if all wanted keys exist
855 | for _, key := range tc.wantKeys {
856 | if _, exists := resultJSON[key]; !exists {
857 | t.Fatalf("expected key %q not found in result: %s", key, got)
858 | }
859 | }
860 |
861 | // Validate document data if required
862 | if tc.validateContent {
863 | docData, ok := resultJSON["documentData"].(map[string]interface{})
864 | if !ok {
865 | t.Fatalf("documentData is not a map: %v", resultJSON["documentData"])
866 | }
867 |
868 | // Check that expected fields are present with correct values
869 | for key, expectedValue := range tc.expectedContent {
870 | actualValue, exists := docData[key]
871 | if !exists {
872 | t.Fatalf("expected field %q not found in documentData", key)
873 | }
874 | if actualValue != expectedValue {
875 | t.Fatalf("field %q mismatch: expected %v, got %v", key, expectedValue, actualValue)
876 | }
877 | }
878 | }
879 | })
880 | }
881 | }
882 |
883 | func runFirestoreAddDocumentsTest(t *testing.T, collectionName string) {
884 | invokeTcs := []struct {
885 | name string
886 | api string
887 | requestBody io.Reader
888 | wantKeys []string
889 | validateDocData bool
890 | expectedDocData map[string]interface{}
891 | isErr bool
892 | }{
893 | {
894 | name: "add document with simple types",
895 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
896 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
897 | "collectionPath": "%s",
898 | "documentData": {
899 | "name": {"stringValue": "Test User"},
900 | "age": {"integerValue": "42"},
901 | "score": {"doubleValue": 99.5},
902 | "active": {"booleanValue": true},
903 | "notes": {"nullValue": null}
904 | }
905 | }`, collectionName))),
906 | wantKeys: []string{"documentPath", "createTime"},
907 | isErr: false,
908 | },
909 | {
910 | name: "add document with complex types",
911 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
912 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
913 | "collectionPath": "%s",
914 | "documentData": {
915 | "location": {
916 | "geoPointValue": {
917 | "latitude": 37.7749,
918 | "longitude": -122.4194
919 | }
920 | },
921 | "timestamp": {
922 | "timestampValue": "2025-01-07T10:00:00Z"
923 | },
924 | "tags": {
925 | "arrayValue": {
926 | "values": [
927 | {"stringValue": "tag1"},
928 | {"stringValue": "tag2"}
929 | ]
930 | }
931 | },
932 | "metadata": {
933 | "mapValue": {
934 | "fields": {
935 | "version": {"integerValue": "1"},
936 | "type": {"stringValue": "test"}
937 | }
938 | }
939 | }
940 | }
941 | }`, collectionName))),
942 | wantKeys: []string{"documentPath", "createTime"},
943 | isErr: false,
944 | },
945 | {
946 | name: "add document with returnData",
947 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
948 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
949 | "collectionPath": "%s",
950 | "documentData": {
951 | "name": {"stringValue": "Return Test"},
952 | "value": {"integerValue": "123"}
953 | },
954 | "returnData": true
955 | }`, collectionName))),
956 | wantKeys: []string{"documentPath", "createTime", "documentData"},
957 | validateDocData: true,
958 | expectedDocData: map[string]interface{}{
959 | "name": "Return Test",
960 | "value": float64(123), // JSON numbers are decoded as float64
961 | },
962 | isErr: false,
963 | },
964 | {
965 | name: "add document with nested maps and arrays",
966 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
967 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
968 | "collectionPath": "%s",
969 | "documentData": {
970 | "company": {
971 | "mapValue": {
972 | "fields": {
973 | "name": {"stringValue": "Tech Corp"},
974 | "employees": {
975 | "arrayValue": {
976 | "values": [
977 | {
978 | "mapValue": {
979 | "fields": {
980 | "name": {"stringValue": "John"},
981 | "role": {"stringValue": "Developer"}
982 | }
983 | }
984 | },
985 | {
986 | "mapValue": {
987 | "fields": {
988 | "name": {"stringValue": "Jane"},
989 | "role": {"stringValue": "Manager"}
990 | }
991 | }
992 | }
993 | ]
994 | }
995 | }
996 | }
997 | }
998 | }
999 | }
1000 | }`, collectionName))),
1001 | wantKeys: []string{"documentPath", "createTime"},
1002 | isErr: false,
1003 | },
1004 | {
1005 | name: "missing collectionPath parameter",
1006 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1007 | requestBody: bytes.NewBuffer([]byte(`{"documentData": {"test": {"stringValue": "value"}}}`)),
1008 | isErr: true,
1009 | },
1010 | {
1011 | name: "missing documentData parameter",
1012 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1013 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collectionPath": "%s"}`, collectionName))),
1014 | isErr: true,
1015 | },
1016 | {
1017 | name: "invalid documentData format",
1018 | api: "http://127.0.0.1:5000/api/tool/firestore-add-docs/invoke",
1019 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collectionPath": "%s", "documentData": "not an object"}`, collectionName))),
1020 | isErr: true,
1021 | },
1022 | }
1023 |
1024 | for _, tc := range invokeTcs {
1025 | t.Run(tc.name, func(t *testing.T) {
1026 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1027 | if err != nil {
1028 | t.Fatalf("unable to create request: %s", err)
1029 | }
1030 | req.Header.Add("Content-type", "application/json")
1031 |
1032 | resp, err := http.DefaultClient.Do(req)
1033 | if err != nil {
1034 | t.Fatalf("unable to send request: %s", err)
1035 | }
1036 | defer resp.Body.Close()
1037 |
1038 | if resp.StatusCode != http.StatusOK {
1039 | if tc.isErr {
1040 | return
1041 | }
1042 | bodyBytes, _ := io.ReadAll(resp.Body)
1043 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1044 | }
1045 |
1046 | var body map[string]interface{}
1047 | err = json.NewDecoder(resp.Body).Decode(&body)
1048 | if err != nil {
1049 | t.Fatalf("error parsing response body: %v", err)
1050 | }
1051 |
1052 | got, ok := body["result"].(string)
1053 | if !ok {
1054 | t.Fatalf("unable to find result in response body")
1055 | }
1056 |
1057 | // Parse the result string as JSON
1058 | var resultJSON map[string]interface{}
1059 | err = json.Unmarshal([]byte(got), &resultJSON)
1060 | if err != nil {
1061 | t.Fatalf("error parsing result as JSON: %v", err)
1062 | }
1063 |
1064 | // Check if all wanted keys exist
1065 | for _, key := range tc.wantKeys {
1066 | if _, exists := resultJSON[key]; !exists {
1067 | t.Fatalf("expected key %q not found in result: %s", key, got)
1068 | }
1069 | }
1070 |
1071 | // Validate document data if required
1072 | if tc.validateDocData {
1073 | docData, ok := resultJSON["documentData"].(map[string]interface{})
1074 | if !ok {
1075 | t.Fatalf("documentData is not a map: %v", resultJSON["documentData"])
1076 | }
1077 |
1078 | // Use reflect.DeepEqual to compare the document data
1079 | if !reflect.DeepEqual(docData, tc.expectedDocData) {
1080 | t.Fatalf("documentData mismatch:\nexpected: %v\nactual: %v", tc.expectedDocData, docData)
1081 | }
1082 | }
1083 | })
1084 | }
1085 | }
1086 |
1087 | func setupFirestoreTestData(t *testing.T, ctx context.Context, client *firestoreapi.Client,
1088 | collectionName, subCollectionName, docID1, docID2, docID3 string) func(*testing.T) {
1089 | // Create test documents
1090 | testData1 := map[string]interface{}{
1091 | "name": "Alice",
1092 | "age": 30,
1093 | }
1094 | testData2 := map[string]interface{}{
1095 | "name": "Bob",
1096 | "age": 25,
1097 | }
1098 | testData3 := map[string]interface{}{
1099 | "name": "Charlie",
1100 | "age": 35,
1101 | }
1102 |
1103 | // Create documents
1104 | _, err := client.Collection(collectionName).Doc(docID1).Set(ctx, testData1)
1105 | if err != nil {
1106 | t.Fatalf("Failed to create test document 1: %v", err)
1107 | }
1108 |
1109 | _, err = client.Collection(collectionName).Doc(docID2).Set(ctx, testData2)
1110 | if err != nil {
1111 | t.Fatalf("Failed to create test document 2: %v", err)
1112 | }
1113 |
1114 | _, err = client.Collection(collectionName).Doc(docID3).Set(ctx, testData3)
1115 | if err != nil {
1116 | t.Fatalf("Failed to create test document 3: %v", err)
1117 | }
1118 |
1119 | // Create a subcollection document
1120 | subDocData := map[string]interface{}{
1121 | "type": "subcollection_doc",
1122 | "value": "test",
1123 | }
1124 | _, err = client.Collection(collectionName).Doc(docID1).Collection(subCollectionName).Doc("subdoc1").Set(ctx, subDocData)
1125 | if err != nil {
1126 | t.Fatalf("Failed to create subcollection document: %v", err)
1127 | }
1128 |
1129 | // Return cleanup function that deletes ALL collections and documents in the database
1130 | return func(t *testing.T) {
1131 | // Helper function to recursively delete all documents in a collection
1132 | var deleteCollection func(*firestoreapi.CollectionRef) error
1133 | deleteCollection = func(collection *firestoreapi.CollectionRef) error {
1134 | // Get all documents in the collection
1135 | docs, err := collection.Documents(ctx).GetAll()
1136 | if err != nil {
1137 | return fmt.Errorf("failed to list documents in collection %s: %w", collection.Path, err)
1138 | }
1139 |
1140 | // Delete each document and its subcollections
1141 | for _, doc := range docs {
1142 | // First, get all subcollections of this document
1143 | subcollections, err := doc.Ref.Collections(ctx).GetAll()
1144 | if err != nil {
1145 | return fmt.Errorf("failed to list subcollections of document %s: %w", doc.Ref.Path, err)
1146 | }
1147 |
1148 | // Recursively delete each subcollection
1149 | for _, subcoll := range subcollections {
1150 | if err := deleteCollection(subcoll); err != nil {
1151 | return fmt.Errorf("failed to delete subcollection %s: %w", subcoll.Path, err)
1152 | }
1153 | }
1154 |
1155 | // Delete the document itself
1156 | if _, err := doc.Ref.Delete(ctx); err != nil {
1157 | return fmt.Errorf("failed to delete document %s: %w", doc.Ref.Path, err)
1158 | }
1159 | }
1160 |
1161 | return nil
1162 | }
1163 |
1164 | // Get all root collections in the database
1165 | rootCollections, err := client.Collections(ctx).GetAll()
1166 | if err != nil {
1167 | t.Errorf("Failed to list root collections: %v", err)
1168 | return
1169 | }
1170 |
1171 | // Delete each root collection and all its contents
1172 | for _, collection := range rootCollections {
1173 | if err := deleteCollection(collection); err != nil {
1174 | t.Errorf("Failed to delete collection %s and its contents: %v", collection.ID, err)
1175 | }
1176 | }
1177 |
1178 | t.Logf("Successfully deleted all collections and documents in the database")
1179 | }
1180 | }
1181 |
1182 | func runFirestoreGetDocumentsTest(t *testing.T, docPath1, docPath2 string) {
1183 | invokeTcs := []struct {
1184 | name string
1185 | api string
1186 | requestBody io.Reader
1187 | wantRegex string
1188 | isErr bool
1189 | }{
1190 | {
1191 | name: "get single document",
1192 | api: "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1193 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s"]}`, docPath1))),
1194 | wantRegex: `"name":"Alice"`,
1195 | isErr: false,
1196 | },
1197 | {
1198 | name: "get multiple documents",
1199 | api: "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1200 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s", "%s"]}`, docPath1, docPath2))),
1201 | wantRegex: `"name":"Alice".*"name":"Bob"`,
1202 | isErr: false,
1203 | },
1204 | {
1205 | name: "get non-existent document",
1206 | api: "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1207 | requestBody: bytes.NewBuffer([]byte(`{"documentPaths": ["non-existent-collection/non-existent-doc"]}`)),
1208 | wantRegex: `"exists":false`,
1209 | isErr: false,
1210 | },
1211 | {
1212 | name: "missing documentPaths parameter",
1213 | api: "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1214 | requestBody: bytes.NewBuffer([]byte(`{}`)),
1215 | isErr: true,
1216 | },
1217 | {
1218 | name: "empty documentPaths array",
1219 | api: "http://127.0.0.1:5000/api/tool/firestore-get-docs/invoke",
1220 | requestBody: bytes.NewBuffer([]byte(`{"documentPaths": []}`)),
1221 | isErr: true,
1222 | },
1223 | }
1224 |
1225 | for _, tc := range invokeTcs {
1226 | t.Run(tc.name, func(t *testing.T) {
1227 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1228 | if err != nil {
1229 | t.Fatalf("unable to create request: %s", err)
1230 | }
1231 | req.Header.Add("Content-type", "application/json")
1232 |
1233 | resp, err := http.DefaultClient.Do(req)
1234 | if err != nil {
1235 | t.Fatalf("unable to send request: %s", err)
1236 | }
1237 | defer resp.Body.Close()
1238 |
1239 | if resp.StatusCode != http.StatusOK {
1240 | if tc.isErr {
1241 | return
1242 | }
1243 | bodyBytes, _ := io.ReadAll(resp.Body)
1244 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1245 | }
1246 |
1247 | var body map[string]interface{}
1248 | err = json.NewDecoder(resp.Body).Decode(&body)
1249 | if err != nil {
1250 | t.Fatalf("error parsing response body: %v", err)
1251 | }
1252 |
1253 | got, ok := body["result"].(string)
1254 | if !ok {
1255 | t.Fatalf("unable to find result in response body")
1256 | }
1257 |
1258 | if tc.wantRegex != "" {
1259 | matched, err := regexp.MatchString(tc.wantRegex, got)
1260 | if err != nil {
1261 | t.Fatalf("invalid regex pattern: %v", err)
1262 | }
1263 | if !matched {
1264 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1265 | }
1266 | }
1267 | })
1268 | }
1269 | }
1270 |
1271 | func runFirestoreListCollectionsTest(t *testing.T, collectionName, subCollectionName, parentDocPath string) {
1272 | invokeTcs := []struct {
1273 | name string
1274 | api string
1275 | requestBody io.Reader
1276 | want string
1277 | isErr bool
1278 | }{
1279 | {
1280 | name: "list root collections",
1281 | api: "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1282 | requestBody: bytes.NewBuffer([]byte(`{}`)),
1283 | want: collectionName,
1284 | isErr: false,
1285 | },
1286 | {
1287 | name: "list subcollections",
1288 | api: "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1289 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"parentPath": "%s"}`, parentDocPath))),
1290 | want: subCollectionName,
1291 | isErr: false,
1292 | },
1293 | {
1294 | name: "list collections for non-existent parent",
1295 | api: "http://127.0.0.1:5000/api/tool/firestore-list-colls/invoke",
1296 | requestBody: bytes.NewBuffer([]byte(`{"parentPath": "non-existent-collection/non-existent-doc"}`)),
1297 | want: `[]`, // Empty array for no collections
1298 | isErr: false,
1299 | },
1300 | }
1301 |
1302 | for _, tc := range invokeTcs {
1303 | t.Run(tc.name, func(t *testing.T) {
1304 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1305 | if err != nil {
1306 | t.Fatalf("unable to create request: %s", err)
1307 | }
1308 | req.Header.Add("Content-type", "application/json")
1309 |
1310 | resp, err := http.DefaultClient.Do(req)
1311 | if err != nil {
1312 | t.Fatalf("unable to send request: %s", err)
1313 | }
1314 | defer resp.Body.Close()
1315 |
1316 | if resp.StatusCode != http.StatusOK {
1317 | if tc.isErr {
1318 | return
1319 | }
1320 | bodyBytes, _ := io.ReadAll(resp.Body)
1321 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1322 | }
1323 |
1324 | var body map[string]interface{}
1325 | err = json.NewDecoder(resp.Body).Decode(&body)
1326 | if err != nil {
1327 | t.Fatalf("error parsing response body: %v", err)
1328 | }
1329 |
1330 | got, ok := body["result"].(string)
1331 | if !ok {
1332 | t.Fatalf("unable to find result in response body")
1333 | }
1334 |
1335 | if !strings.Contains(got, tc.want) {
1336 | t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
1337 | }
1338 | })
1339 | }
1340 | }
1341 |
1342 | func runFirestoreDeleteDocumentsTest(t *testing.T, docPath string) {
1343 | invokeTcs := []struct {
1344 | name string
1345 | api string
1346 | requestBody io.Reader
1347 | want string
1348 | isErr bool
1349 | }{
1350 | {
1351 | name: "delete single document",
1352 | api: "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1353 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"documentPaths": ["%s"]}`, docPath))),
1354 | want: `"success":true`,
1355 | isErr: false,
1356 | },
1357 | {
1358 | name: "delete non-existent document",
1359 | api: "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1360 | requestBody: bytes.NewBuffer([]byte(`{"documentPaths": ["non-existent-collection/non-existent-doc"]}`)),
1361 | want: `"success":true`, // Firestore delete succeeds even if doc doesn't exist
1362 | isErr: false,
1363 | },
1364 | {
1365 | name: "missing documentPaths parameter",
1366 | api: "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1367 | requestBody: bytes.NewBuffer([]byte(`{}`)),
1368 | isErr: true,
1369 | },
1370 | {
1371 | name: "empty documentPaths array",
1372 | api: "http://127.0.0.1:5000/api/tool/firestore-delete-docs/invoke",
1373 | requestBody: bytes.NewBuffer([]byte(`{"documentPaths": []}`)),
1374 | isErr: true,
1375 | },
1376 | }
1377 |
1378 | for _, tc := range invokeTcs {
1379 | t.Run(tc.name, func(t *testing.T) {
1380 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1381 | if err != nil {
1382 | t.Fatalf("unable to create request: %s", err)
1383 | }
1384 | req.Header.Add("Content-type", "application/json")
1385 |
1386 | resp, err := http.DefaultClient.Do(req)
1387 | if err != nil {
1388 | t.Fatalf("unable to send request: %s", err)
1389 | }
1390 | defer resp.Body.Close()
1391 |
1392 | if resp.StatusCode != http.StatusOK {
1393 | if tc.isErr {
1394 | return
1395 | }
1396 | bodyBytes, _ := io.ReadAll(resp.Body)
1397 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1398 | }
1399 |
1400 | var body map[string]interface{}
1401 | err = json.NewDecoder(resp.Body).Decode(&body)
1402 | if err != nil {
1403 | t.Fatalf("error parsing response body: %v", err)
1404 | }
1405 |
1406 | got, ok := body["result"].(string)
1407 | if !ok {
1408 | t.Fatalf("unable to find result in response body")
1409 | }
1410 |
1411 | if !strings.Contains(got, tc.want) {
1412 | t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
1413 | }
1414 | })
1415 | }
1416 | }
1417 |
1418 | func runFirestoreQueryTest(t *testing.T, collectionName string) {
1419 | invokeTcs := []struct {
1420 | name string
1421 | api string
1422 | requestBody io.Reader
1423 | wantRegex string
1424 | isErr bool
1425 | }{
1426 | {
1427 | name: "query with parameterized filters - age greater than",
1428 | api: "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1429 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1430 | "collection": "%s",
1431 | "operator": ">",
1432 | "ageValue": "25"
1433 | }`, collectionName))),
1434 | wantRegex: `"name":"Alice"`,
1435 | isErr: false,
1436 | },
1437 | {
1438 | name: "query with parameterized filters - exact name match",
1439 | api: "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1440 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1441 | "collection": "%s",
1442 | "operator": "==",
1443 | "ageValue": "25"
1444 | }`, collectionName))),
1445 | wantRegex: `"name":"Bob"`,
1446 | isErr: false,
1447 | },
1448 | {
1449 | name: "query with parameterized filters - age less than or equal",
1450 | api: "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1451 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1452 | "collection": "%s",
1453 | "operator": "<=",
1454 | "ageValue": "29"
1455 | }`, collectionName))),
1456 | wantRegex: `"name":"Bob"`,
1457 | isErr: false,
1458 | },
1459 | {
1460 | name: "missing required parameter",
1461 | api: "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1462 | requestBody: bytes.NewBuffer([]byte(`{"collection": "test", "operator": ">"}`)),
1463 | isErr: true,
1464 | },
1465 | {
1466 | name: "query non-existent collection with parameters",
1467 | api: "http://127.0.0.1:5000/api/tool/firestore-query-param/invoke",
1468 | requestBody: bytes.NewBuffer([]byte(`{
1469 | "collection": "non-existent-collection",
1470 | "operator": "==",
1471 | "ageValue": "30"
1472 | }`)),
1473 | wantRegex: `^\[\]$`, // Empty array
1474 | isErr: false,
1475 | },
1476 | }
1477 |
1478 | for _, tc := range invokeTcs {
1479 | t.Run(tc.name, func(t *testing.T) {
1480 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1481 | if err != nil {
1482 | t.Fatalf("unable to create request: %s", err)
1483 | }
1484 | req.Header.Add("Content-type", "application/json")
1485 |
1486 | resp, err := http.DefaultClient.Do(req)
1487 | if err != nil {
1488 | t.Fatalf("unable to send request: %s", err)
1489 | }
1490 | defer resp.Body.Close()
1491 |
1492 | if resp.StatusCode != http.StatusOK {
1493 | if tc.isErr {
1494 | return
1495 | }
1496 | bodyBytes, _ := io.ReadAll(resp.Body)
1497 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1498 | }
1499 |
1500 | var body map[string]interface{}
1501 | err = json.NewDecoder(resp.Body).Decode(&body)
1502 | if err != nil {
1503 | t.Fatalf("error parsing response body: %v", err)
1504 | }
1505 |
1506 | got, ok := body["result"].(string)
1507 | if !ok {
1508 | t.Fatalf("unable to find result in response body")
1509 | }
1510 |
1511 | if tc.wantRegex != "" {
1512 | matched, err := regexp.MatchString(tc.wantRegex, got)
1513 | if err != nil {
1514 | t.Fatalf("invalid regex pattern: %v", err)
1515 | }
1516 | if !matched {
1517 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1518 | }
1519 | }
1520 | })
1521 | }
1522 | }
1523 |
1524 | func runFirestoreQuerySelectArrayTest(t *testing.T, collectionName string) {
1525 | invokeTcs := []struct {
1526 | name string
1527 | api string
1528 | requestBody io.Reader
1529 | wantRegex string
1530 | validateFields bool
1531 | isErr bool
1532 | }{
1533 | {
1534 | name: "query with array select fields - single field",
1535 | api: "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1536 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1537 | "collection": "%s",
1538 | "fields": ["name"]
1539 | }`, collectionName))),
1540 | wantRegex: `"name":"`,
1541 | validateFields: true,
1542 | isErr: false,
1543 | },
1544 | {
1545 | name: "query with array select fields - multiple fields",
1546 | api: "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1547 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1548 | "collection": "%s",
1549 | "fields": ["name", "age"]
1550 | }`, collectionName))),
1551 | wantRegex: `"name":".*"age":`,
1552 | validateFields: true,
1553 | isErr: false,
1554 | },
1555 | {
1556 | name: "query with empty array select fields",
1557 | api: "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1558 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1559 | "collection": "%s",
1560 | "fields": []
1561 | }`, collectionName))),
1562 | wantRegex: `\[.*\]`, // Should return documents with all fields
1563 | isErr: false,
1564 | },
1565 | {
1566 | name: "missing fields parameter",
1567 | api: "http://127.0.0.1:5000/api/tool/firestore-query-select-array/invoke",
1568 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"collection": "%s"}`, collectionName))),
1569 | isErr: true,
1570 | },
1571 | }
1572 |
1573 | for _, tc := range invokeTcs {
1574 | t.Run(tc.name, func(t *testing.T) {
1575 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1576 | if err != nil {
1577 | t.Fatalf("unable to create request: %s", err)
1578 | }
1579 | req.Header.Add("Content-type", "application/json")
1580 |
1581 | resp, err := http.DefaultClient.Do(req)
1582 | if err != nil {
1583 | t.Fatalf("unable to send request: %s", err)
1584 | }
1585 | defer resp.Body.Close()
1586 |
1587 | if resp.StatusCode != http.StatusOK {
1588 | if tc.isErr {
1589 | return
1590 | }
1591 | bodyBytes, _ := io.ReadAll(resp.Body)
1592 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1593 | }
1594 |
1595 | var body map[string]interface{}
1596 | err = json.NewDecoder(resp.Body).Decode(&body)
1597 | if err != nil {
1598 | t.Fatalf("error parsing response body: %v", err)
1599 | }
1600 |
1601 | got, ok := body["result"].(string)
1602 | if !ok {
1603 | t.Fatalf("unable to find result in response body")
1604 | }
1605 |
1606 | if tc.wantRegex != "" {
1607 | matched, err := regexp.MatchString(tc.wantRegex, got)
1608 | if err != nil {
1609 | t.Fatalf("invalid regex pattern: %v", err)
1610 | }
1611 | if !matched {
1612 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1613 | }
1614 | }
1615 |
1616 | // Additional validation for field selection
1617 | if tc.validateFields {
1618 | // Parse the result to check if only selected fields are present
1619 | var results []map[string]interface{}
1620 | err = json.Unmarshal([]byte(got), &results)
1621 | if err != nil {
1622 | t.Fatalf("error parsing result as JSON array: %v", err)
1623 | }
1624 |
1625 | // For single field test, ensure only 'name' field is present in data
1626 | if tc.name == "query with array select fields - single field" && len(results) > 0 {
1627 | for _, result := range results {
1628 | if data, ok := result["data"].(map[string]interface{}); ok {
1629 | if _, hasName := data["name"]; !hasName {
1630 | t.Fatalf("expected 'name' field in data, but not found")
1631 | }
1632 | // The 'age' field should not be present when only 'name' is selected
1633 | if _, hasAge := data["age"]; hasAge {
1634 | t.Fatalf("unexpected 'age' field in data when only 'name' was selected")
1635 | }
1636 | }
1637 | }
1638 | }
1639 |
1640 | // For multiple fields test, ensure both fields are present
1641 | if tc.name == "query with array select fields - multiple fields" && len(results) > 0 {
1642 | for _, result := range results {
1643 | if data, ok := result["data"].(map[string]interface{}); ok {
1644 | if _, hasName := data["name"]; !hasName {
1645 | t.Fatalf("expected 'name' field in data, but not found")
1646 | }
1647 | if _, hasAge := data["age"]; !hasAge {
1648 | t.Fatalf("expected 'age' field in data, but not found")
1649 | }
1650 | }
1651 | }
1652 | }
1653 | }
1654 | })
1655 | }
1656 | }
1657 |
1658 | func runFirestoreQueryCollectionTest(t *testing.T, collectionName string) {
1659 | invokeTcs := []struct {
1660 | name string
1661 | api string
1662 | requestBody io.Reader
1663 | wantRegex string
1664 | isErr bool
1665 | }{
1666 | {
1667 | name: "query collection with filter",
1668 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1669 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1670 | "collectionPath": "%s",
1671 | "filters": ["{\"field\": \"age\", \"op\": \">\", \"value\": 25}"],
1672 | "orderBy": "",
1673 | "limit": 10
1674 | }`, collectionName))),
1675 | wantRegex: `"name":"Alice"`,
1676 | isErr: false,
1677 | },
1678 | {
1679 | name: "query collection with orderBy",
1680 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1681 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1682 | "collectionPath": "%s",
1683 | "filters": [],
1684 | "orderBy": "{\"field\": \"age\", \"direction\": \"DESCENDING\"}",
1685 | "limit": 2
1686 | }`, collectionName))),
1687 | wantRegex: `"age":35.*"age":30`, // Should be ordered by age descending (Charlie=35, Alice=30)
1688 | isErr: false,
1689 | },
1690 | {
1691 | name: "query collection with multiple filters",
1692 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1693 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1694 | "collectionPath": "%s",
1695 | "filters": [
1696 | "{\"field\": \"age\", \"op\": \">=\", \"value\": 25}",
1697 | "{\"field\": \"age\", \"op\": \"<=\", \"value\": 30}"
1698 | ],
1699 | "orderBy": "",
1700 | "limit": 10
1701 | }`, collectionName))),
1702 | wantRegex: `"name":"Bob".*"name":"Alice"`, // Results may be ordered by document ID
1703 | isErr: false,
1704 | },
1705 | {
1706 | name: "query with limit",
1707 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1708 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1709 | "collectionPath": "%s",
1710 | "filters": [],
1711 | "orderBy": "",
1712 | "limit": 1
1713 | }`, collectionName))),
1714 | wantRegex: `^\[{.*}\]$`, // Should return exactly one document
1715 | isErr: false,
1716 | },
1717 | {
1718 | name: "query non-existent collection",
1719 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1720 | requestBody: bytes.NewBuffer([]byte(`{
1721 | "collectionPath": "non-existent-collection",
1722 | "filters": [],
1723 | "orderBy": "",
1724 | "limit": 10
1725 | }`)),
1726 | wantRegex: `^\[\]$`, // Empty array
1727 | isErr: false,
1728 | },
1729 | {
1730 | name: "missing collectionPath parameter",
1731 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1732 | requestBody: bytes.NewBuffer([]byte(`{}`)),
1733 | isErr: true,
1734 | },
1735 | {
1736 | name: "invalid filter operator",
1737 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1738 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1739 | "collectionPath": "%s",
1740 | "filters": ["{\"field\": \"age\", \"op\": \"INVALID\", \"value\": 25}"],
1741 | "orderBy": ""
1742 | }`, collectionName))),
1743 | isErr: true,
1744 | },
1745 | {
1746 | name: "query with analyzeQuery",
1747 | api: "http://127.0.0.1:5000/api/tool/firestore-query-coll/invoke",
1748 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{
1749 | "collectionPath": "%s",
1750 | "filters": [],
1751 | "orderBy": "",
1752 | "analyzeQuery": true,
1753 | "limit": 1
1754 | }`, collectionName))),
1755 | wantRegex: `"documents":\[.*\]`,
1756 | isErr: false,
1757 | },
1758 | }
1759 |
1760 | for _, tc := range invokeTcs {
1761 | t.Run(tc.name, func(t *testing.T) {
1762 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
1763 | if err != nil {
1764 | t.Fatalf("unable to create request: %s", err)
1765 | }
1766 | req.Header.Add("Content-type", "application/json")
1767 |
1768 | resp, err := http.DefaultClient.Do(req)
1769 | if err != nil {
1770 | t.Fatalf("unable to send request: %s", err)
1771 | }
1772 | defer resp.Body.Close()
1773 |
1774 | if resp.StatusCode != http.StatusOK {
1775 | if tc.isErr {
1776 | return
1777 | }
1778 | bodyBytes, _ := io.ReadAll(resp.Body)
1779 | t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
1780 | }
1781 |
1782 | var body map[string]interface{}
1783 | err = json.NewDecoder(resp.Body).Decode(&body)
1784 | if err != nil {
1785 | t.Fatalf("error parsing response body: %v", err)
1786 | }
1787 |
1788 | got, ok := body["result"].(string)
1789 | if !ok {
1790 | t.Fatalf("unable to find result in response body")
1791 | }
1792 |
1793 | if tc.wantRegex != "" {
1794 | matched, err := regexp.MatchString(tc.wantRegex, got)
1795 | if err != nil {
1796 | t.Fatalf("invalid regex pattern: %v", err)
1797 | }
1798 | if !matched {
1799 | t.Fatalf("result does not match expected pattern.\nGot: %s\nWant pattern: %s", got, tc.wantRegex)
1800 | }
1801 | }
1802 | })
1803 | }
1804 | }
1805 |
```