4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
430 lines
14 KiB
Python
430 lines
14 KiB
Python
"""Dedicated unit tests for SnowflakeQueryHistoryTool.
|
|
|
|
Covers contract metadata, source-availability gating, parameter extraction,
|
|
SQL limit enforcement, account/token validation, row normalization across the
|
|
two response shapes the Snowflake SQL API returns, and HTTP failure handling.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from integrations.snowflake.tools.snowflake_query_history_tool import query_snowflake_history
|
|
from tests.tools.conftest import BaseToolContract, MockHttpxResponse
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Contract — metadata, is_available, extract_params surface
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSnowflakeQueryHistoryToolContract(BaseToolContract):
|
|
def get_tool_under_test(self) -> Any:
|
|
return query_snowflake_history.__opensre_registered_tool__
|
|
|
|
|
|
def _rt() -> Any:
|
|
return query_snowflake_history.__opensre_registered_tool__
|
|
|
|
|
|
def test_is_available_true_when_token_account_and_verified() -> None:
|
|
sources = {
|
|
"snowflake": {
|
|
"connection_verified": True,
|
|
"account_identifier": "xy12345.us-east-1",
|
|
"token": "sf-token",
|
|
}
|
|
}
|
|
assert _rt().is_available(sources) is True
|
|
|
|
|
|
def test_is_available_false_when_connection_not_verified() -> None:
|
|
sources = {
|
|
"snowflake": {
|
|
"connection_verified": False,
|
|
"account_identifier": "xy12345.us-east-1",
|
|
"token": "sf-token",
|
|
}
|
|
}
|
|
assert _rt().is_available(sources) is False
|
|
|
|
|
|
def test_is_available_false_when_token_missing() -> None:
|
|
sources = {
|
|
"snowflake": {
|
|
"connection_verified": True,
|
|
"account_identifier": "xy12345.us-east-1",
|
|
"token": " ",
|
|
}
|
|
}
|
|
assert _rt().is_available(sources) is False
|
|
|
|
|
|
def test_is_available_false_when_account_identifier_missing() -> None:
|
|
sources = {
|
|
"snowflake": {
|
|
"connection_verified": True,
|
|
"account_identifier": "",
|
|
"token": "sf-token",
|
|
}
|
|
}
|
|
assert _rt().is_available(sources) is False
|
|
|
|
|
|
def test_is_available_false_when_no_snowflake_source() -> None:
|
|
assert _rt().is_available({}) is False
|
|
|
|
|
|
def test_extract_params_strips_and_defaults_max_results() -> None:
|
|
sources = {
|
|
"snowflake": {
|
|
"account_identifier": " xy12345.us-east-1 ",
|
|
"user": " sf-user ",
|
|
"password": " sf-pass ",
|
|
"token": " sf-token ",
|
|
"warehouse": " WH_X ",
|
|
"role": " ANALYST ",
|
|
"database": " ANALYTICS ",
|
|
"schema": " PUBLIC ",
|
|
"query": " SELECT 1 ",
|
|
"integration_id": " sf-1 ",
|
|
}
|
|
}
|
|
params = _rt().extract_params(sources)
|
|
assert params["account_identifier"] == "xy12345.us-east-1"
|
|
assert params["user"] == "sf-user"
|
|
assert params["password"] == "sf-pass"
|
|
assert params["token"] == "sf-token"
|
|
assert params["warehouse"] == "WH_X"
|
|
assert params["role"] == "ANALYST"
|
|
assert params["database"] == "ANALYTICS"
|
|
assert params["db_schema"] == "PUBLIC"
|
|
assert params["query"] == "SELECT 1"
|
|
assert params["integration_id"] == "sf-1"
|
|
# Default max_results when key absent
|
|
assert params["max_results"] == 50
|
|
# The tool always extracts a fixed limit of 50; bounding happens at run time
|
|
assert params["limit"] == 50
|
|
|
|
|
|
def test_extract_params_uses_default_max_results_when_zero_or_missing() -> None:
|
|
# max_results explicitly 0 should fall back to the default (truthy guard)
|
|
sources = {"snowflake": {"account_identifier": "x", "token": "t", "max_results": 0}}
|
|
assert _rt().extract_params(sources)["max_results"] == 50
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation guards — missing config / required fields
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_returns_missing_account_when_account_identifier_blank() -> None:
|
|
result = query_snowflake_history(account_identifier=" ", token="sf-token")
|
|
assert result["available"] is False
|
|
assert result["error"] == "Missing account identifier."
|
|
assert result["rows"] == []
|
|
|
|
|
|
def test_returns_missing_token_when_token_blank() -> None:
|
|
result = query_snowflake_history(account_identifier="xy12345.us-east-1", token=" ")
|
|
assert result["available"] is False
|
|
assert result["error"] == "Missing Snowflake token."
|
|
assert result["rows"] == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request shaping — SQL LIMIT enforcement, payload contents, endpoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_default_query_used_when_query_blank(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
captured["url"] = url
|
|
captured["statement"] = json["statement"]
|
|
captured["headers"] = headers
|
|
return MockHttpxResponse({"data": []})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
query="",
|
|
max_results=10,
|
|
)
|
|
assert result["available"] is True
|
|
assert "INFORMATION_SCHEMA.QUERY_HISTORY" in captured["statement"]
|
|
# Default query embeds RESULT_LIMIT directly, not a trailing LIMIT clause
|
|
assert "RESULT_LIMIT => 10" in captured["statement"]
|
|
# Endpoint is correctly templated from the account identifier
|
|
assert captured["url"] == "https://xy12345.us-east-1.snowflakecomputing.com/api/v2/statements"
|
|
assert captured["headers"]["Authorization"] == "Bearer sf-token"
|
|
assert captured["headers"]["Content-Type"] == "application/json"
|
|
|
|
|
|
def test_user_query_gets_limit_appended_if_missing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
captured["statement"] = json["statement"]
|
|
return MockHttpxResponse({"data": []})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
query="SELECT * FROM T;",
|
|
limit=1000, # caller asks for 1000 but max_results caps it
|
|
max_results=7,
|
|
)
|
|
|
|
assert captured["statement"].endswith("LIMIT 7")
|
|
assert ";" not in captured["statement"] # trailing semicolon stripped
|
|
|
|
|
|
def test_user_query_with_existing_limit_is_left_untouched(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
captured["statement"] = json["statement"]
|
|
return MockHttpxResponse({"data": []})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
query="SELECT * FROM T LIMIT 3",
|
|
limit=500,
|
|
max_results=50,
|
|
)
|
|
|
|
# The ensure_sql_limit helper must not double-stamp the LIMIT clause
|
|
assert captured["statement"].lower().count("limit") == 1
|
|
assert captured["statement"].endswith("LIMIT 3")
|
|
|
|
|
|
def test_optional_session_params_only_included_when_set(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
captured["payload"] = json
|
|
return MockHttpxResponse({"data": []})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
warehouse="WH_X",
|
|
role="ANALYST",
|
|
database="DB",
|
|
db_schema="PUBLIC",
|
|
max_results=5,
|
|
)
|
|
payload = captured["payload"]
|
|
assert payload["warehouse"] == "WH_X"
|
|
assert payload["role"] == "ANALYST"
|
|
assert payload["database"] == "DB"
|
|
assert payload["schema"] == "PUBLIC"
|
|
assert "statement" in payload
|
|
assert "timeout" in payload
|
|
|
|
|
|
def test_optional_session_params_omitted_when_blank(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
captured["payload"] = json
|
|
return MockHttpxResponse({"data": []})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=5,
|
|
)
|
|
payload = captured["payload"]
|
|
for key in ("warehouse", "role", "database", "schema"):
|
|
assert key not in payload
|
|
|
|
|
|
def test_bounded_limit_caps_caller_request(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Caller asks for limit=500 but max_results=6 ⇒ server query and rows capped at 6."""
|
|
|
|
def _fake_post(url: str, headers: dict[str, str], json: dict[str, Any], timeout: float):
|
|
# Server returns 20 rows — tool must trim to effective_limit (6)
|
|
return MockHttpxResponse({"data": [{"id": idx} for idx in range(20)]})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
query="SELECT * FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY())",
|
|
limit=500,
|
|
max_results=6,
|
|
)
|
|
|
|
assert result["available"] is True
|
|
assert len(result["rows"]) == 6
|
|
assert result["total_returned"] == 6
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Row normalization — both API response shapes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_normalizes_dict_rows_passthrough(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""When the API returns ``data=[{...}, ...]``, rows are passed through as-is."""
|
|
|
|
def _fake_post(*_args: Any, **_kwargs: Any) -> MockHttpxResponse:
|
|
return MockHttpxResponse(
|
|
{
|
|
"data": [
|
|
{"query_id": "q1", "user_name": "alice"},
|
|
{"query_id": "q2", "user_name": "bob"},
|
|
]
|
|
}
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=10,
|
|
)
|
|
assert result["rows"] == [
|
|
{"query_id": "q1", "user_name": "alice"},
|
|
{"query_id": "q2", "user_name": "bob"},
|
|
]
|
|
|
|
|
|
def test_normalizes_tabular_rows_using_result_set_metadata(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""When the API returns lists of values + rowType metadata, rows are zipped to dicts."""
|
|
|
|
def _fake_post(*_args: Any, **_kwargs: Any) -> MockHttpxResponse:
|
|
return MockHttpxResponse(
|
|
{
|
|
"data": [
|
|
["q1", "alice"],
|
|
["q2", "bob"],
|
|
],
|
|
"resultSetMetaData": {
|
|
"rowType": [
|
|
{"name": "query_id"},
|
|
{"name": "user_name"},
|
|
]
|
|
},
|
|
}
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=10,
|
|
)
|
|
assert result["rows"] == [
|
|
{"query_id": "q1", "user_name": "alice"},
|
|
{"query_id": "q2", "user_name": "bob"},
|
|
]
|
|
|
|
|
|
def test_normalizes_to_empty_when_payload_unrecognized(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Unknown response shapes degrade gracefully to an empty rows list, not an exception."""
|
|
|
|
def _fake_post(*_args: Any, **_kwargs: Any) -> MockHttpxResponse:
|
|
return MockHttpxResponse({"unexpected": "shape"})
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=10,
|
|
)
|
|
assert result["available"] is True
|
|
assert result["rows"] == []
|
|
assert result["total_returned"] == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HTTP failure handling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_returns_unavailable_on_http_error_status(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def _fake_post(*_args: Any, **_kwargs: Any) -> MockHttpxResponse:
|
|
return MockHttpxResponse(
|
|
{"error": "boom"},
|
|
raise_for_status_error=RuntimeError("HTTP 500 from Snowflake"),
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _fake_post
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=5,
|
|
)
|
|
assert result["available"] is False
|
|
assert "HTTP 500 from Snowflake" in result["error"]
|
|
assert result["rows"] == []
|
|
|
|
|
|
def test_returns_unavailable_on_request_exception(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def _raise(*_args: Any, **_kwargs: Any) -> None:
|
|
raise ConnectionError("connection refused")
|
|
|
|
monkeypatch.setattr(
|
|
"integrations.snowflake.tools.snowflake_query_history_tool.httpx.post", _raise
|
|
)
|
|
|
|
result = query_snowflake_history(
|
|
account_identifier="xy12345.us-east-1",
|
|
token="sf-token",
|
|
max_results=5,
|
|
)
|
|
assert result["available"] is False
|
|
assert "connection refused" in result["error"]
|
|
assert result["rows"] == []
|