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
379 lines
13 KiB
Python
379 lines
13 KiB
Python
"""Tests for the Honeycomb service client."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from integrations.config_models import HoneycombIntegrationConfig
|
|
from integrations.honeycomb.client import HoneycombClient
|
|
|
|
|
|
@pytest.fixture
|
|
def config() -> HoneycombIntegrationConfig:
|
|
return HoneycombIntegrationConfig(
|
|
api_key="test-api-key",
|
|
dataset="test-dataset",
|
|
base_url="https://api.honeycomb.io",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def client(config: HoneycombIntegrationConfig) -> HoneycombClient:
|
|
return HoneycombClient(config)
|
|
|
|
|
|
def test_is_configured(config: HoneycombIntegrationConfig) -> None:
|
|
# Both present
|
|
client = HoneycombClient(config)
|
|
assert client.is_configured is True
|
|
|
|
# Missing API key
|
|
config_no_key = HoneycombIntegrationConfig(api_key="", dataset="ds")
|
|
client_no_key = HoneycombClient(config_no_key)
|
|
assert client_no_key.is_configured is False
|
|
|
|
# Missing dataset
|
|
# Note: HoneycombIntegrationConfig has a default for dataset, so we test with empty string
|
|
config_no_ds = HoneycombIntegrationConfig(api_key="key", dataset="")
|
|
client_no_ds = HoneycombClient(config_no_ds)
|
|
# The normalize_dataset validator might prevent empty string, let's check the code
|
|
# Actually it returns DEFAULT_HONEYCOMB_DATASET if stripped value is empty
|
|
# So we should check if is_configured handles that or if we need to force it.
|
|
# But based on the code: bool(self.config.api_key and self.config.dataset)
|
|
# If dataset is "__all__", it's truthy.
|
|
assert client_no_ds.is_configured is True # because it defaults to "__all__"
|
|
|
|
|
|
def test_validate_access_success(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"environment": {"name": "prod"},
|
|
"team": {"name": "my-team"},
|
|
"type": "ingest",
|
|
}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.get.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.validate_access()
|
|
mock_http.get.assert_called_once_with("/1/auth")
|
|
|
|
assert result["success"] is True
|
|
assert result["environment"] == {"name": "prod"}
|
|
assert result["team"] == {"name": "my-team"}
|
|
assert result["key_type"] == "ingest"
|
|
|
|
|
|
def test_validate_access_http_error(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 401
|
|
mock_response.text = "Unauthorized"
|
|
|
|
# We need to simulate httpx.HTTPStatusError
|
|
error = httpx.HTTPStatusError("Auth failed", request=MagicMock(), response=mock_response)
|
|
mock_response.raise_for_status.side_effect = error
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.get.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.validate_access()
|
|
|
|
assert result["success"] is False
|
|
assert "HTTP 401: Unauthorized" in result["error"]
|
|
|
|
|
|
def test_validate_access_generic_exception(client: HoneycombClient) -> None:
|
|
mock_http = MagicMock()
|
|
mock_http.get.side_effect = Exception("Connection refused")
|
|
client._client = mock_http
|
|
|
|
result = client.validate_access()
|
|
|
|
assert result["success"] is False
|
|
assert "Connection refused" in result["error"]
|
|
|
|
|
|
def test_probe_access_success(client: HoneycombClient) -> None:
|
|
client.validate_access = MagicMock(
|
|
return_value={"success": True, "environment": {"slug": "prod"}}
|
|
)
|
|
client.run_query = MagicMock(return_value={"success": True, "results": [{}]})
|
|
|
|
result = client.probe_access()
|
|
|
|
assert result.status == "passed"
|
|
assert "prod" in result.detail
|
|
assert "test-dataset" in result.detail
|
|
|
|
|
|
def test_create_query_success(client: HoneycombClient) -> None:
|
|
query_spec = {"calculations": [{"op": "COUNT"}]}
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"id": "query-123", "calculations": []}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.post.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.create_query(query_spec)
|
|
mock_http.post.assert_any_call("/1/queries/test-dataset", json=query_spec)
|
|
|
|
assert result["success"] is True
|
|
assert result["query_id"] == "query-123"
|
|
assert result["query"] == {"id": "query-123", "calculations": []}
|
|
|
|
|
|
def test_create_query_no_id(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"something": "else"}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.post.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.create_query({})
|
|
|
|
assert result["success"] is False
|
|
assert "returned no query ID" in result["error"]
|
|
|
|
|
|
def test_create_query_http_error(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 400
|
|
mock_response.text = "Bad Request"
|
|
error = httpx.HTTPStatusError("Err", request=MagicMock(), response=mock_response)
|
|
mock_response.raise_for_status.side_effect = error
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.post.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.create_query({})
|
|
|
|
assert result["success"] is False
|
|
assert "HTTP 400: Bad Request" in result["error"]
|
|
|
|
|
|
def test_create_query_result_success(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"id": "result-456", "complete": False}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.post.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.create_query_result("query-123", limit=10)
|
|
# Check payload
|
|
args, kwargs = mock_http.post.call_args
|
|
assert args[0] == "/1/query_results/test-dataset"
|
|
assert kwargs["json"]["query_id"] == "query-123"
|
|
assert kwargs["json"]["limit"] == 10
|
|
|
|
assert result["success"] is True
|
|
assert result["result"]["id"] == "result-456"
|
|
|
|
|
|
def test_create_query_result_http_error(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal Server Error"
|
|
error = httpx.HTTPStatusError("Err", request=MagicMock(), response=mock_response)
|
|
mock_response.raise_for_status.side_effect = error
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.post.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.create_query_result("q", limit=1)
|
|
|
|
assert result["success"] is False
|
|
assert "HTTP 500: Internal Server Error" in result["error"]
|
|
|
|
|
|
def test_get_query_result_success(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"id": "result-456",
|
|
"complete": True,
|
|
"data": {"results": []},
|
|
}
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.get.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.get_query_result("result-456")
|
|
mock_http.get.assert_called_once_with("/1/query_results/test-dataset/result-456")
|
|
|
|
assert result["success"] is True
|
|
assert result["result"]["complete"] is True
|
|
|
|
|
|
def test_get_query_result_http_error(client: HoneycombClient) -> None:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 404
|
|
mock_response.text = "Not Found"
|
|
error = httpx.HTTPStatusError("Err", request=MagicMock(), response=mock_response)
|
|
mock_response.raise_for_status.side_effect = error
|
|
|
|
mock_http = MagicMock()
|
|
mock_http.get.return_value = mock_response
|
|
client._client = mock_http
|
|
|
|
result = client.get_query_result("missing")
|
|
|
|
assert result["success"] is False
|
|
assert "HTTP 404: Not Found" in result["error"]
|
|
|
|
|
|
def test_get_query_result_generic_exception(client: HoneycombClient) -> None:
|
|
mock_http = MagicMock()
|
|
mock_http.get.side_effect = Exception("Timeout")
|
|
client._client = mock_http
|
|
|
|
result = client.get_query_result("result-id")
|
|
|
|
assert result["success"] is False
|
|
assert "Timeout" in result["error"]
|
|
|
|
|
|
def test_run_query_full_flow(client: HoneycombClient) -> None:
|
|
# We'll mock the internal methods to avoid deep httpx patching
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={
|
|
"success": True,
|
|
"result": {"id": "r1", "complete": True, "data": {"results": [{"data": {"val": 10}}]}},
|
|
}
|
|
)
|
|
|
|
# We also need to mock time.sleep to speed up tests if it ever hits it
|
|
with patch("time.sleep"):
|
|
result = client.run_query({"calculations": []})
|
|
|
|
assert result["success"] is True
|
|
assert result["query_id"] == "q1"
|
|
assert result["query_result_id"] == "r1"
|
|
assert result["results"] == [{"val": 10}]
|
|
|
|
|
|
def test_query_traces_success(client: HoneycombClient) -> None:
|
|
client.run_query = MagicMock(return_value={"success": True, "results": []})
|
|
|
|
result = client.query_traces(service_name="auth-service", trace_id="trace-abc")
|
|
|
|
assert result["success"] is True
|
|
# Verify run_query was called with correct filter shape
|
|
args, kwargs = client.run_query.call_args
|
|
query = args[0]
|
|
filters = query["filters"]
|
|
assert any(f["column"] == "service.name" and f["value"] == "auth-service" for f in filters)
|
|
assert any(f["column"] == "trace.trace_id" and f["value"] == "trace-abc" for f in filters)
|
|
|
|
|
|
def test_run_query_with_polling(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={"success": True, "result": {"id": "r1", "complete": False}}
|
|
)
|
|
# Mock two calls to get_query_result: first False, then True
|
|
client.get_query_result = MagicMock(
|
|
side_effect=[
|
|
{"success": True, "result": {"id": "r1", "complete": False}},
|
|
{"success": True, "result": {"id": "r1", "complete": True, "data": {"results": []}}},
|
|
]
|
|
)
|
|
|
|
with patch("time.sleep"):
|
|
result = client.run_query({"calculations": []}, poll_attempts=3)
|
|
|
|
assert result["success"] is True
|
|
assert client.get_query_result.call_count == 2
|
|
|
|
|
|
def test_run_query_timeout(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={"success": True, "result": {"id": "r1", "complete": False}}
|
|
)
|
|
# Always return incomplete
|
|
client.get_query_result = MagicMock(
|
|
return_value={"success": True, "result": {"id": "r1", "complete": False}}
|
|
)
|
|
|
|
with patch("time.sleep"):
|
|
result = client.run_query({"calculations": []}, poll_attempts=2)
|
|
|
|
assert result["success"] is False
|
|
assert "did not complete before the timeout" in result["error"]
|
|
# 2 poll attempts + initial check from create_query_result
|
|
# wait, run_query does:
|
|
# for _ in range(max(poll_attempts, 1)):
|
|
# if current_result.get("complete") is True: break
|
|
# time.sleep(...)
|
|
# fetched = self.get_query_result(...)
|
|
# So if poll_attempts=2:
|
|
# iteration 1: complete is False, sleep, get_query_result (fetched 1)
|
|
# iteration 2: complete is False, sleep, get_query_result (fetched 2)
|
|
# loop ends, return timeout error.
|
|
assert client.get_query_result.call_count == 2
|
|
|
|
|
|
def test_run_query_create_result_failure(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={"success": False, "error": "Internal failure"}
|
|
)
|
|
|
|
result = client.run_query({"calculations": []})
|
|
|
|
assert result["success"] is False
|
|
assert result["error"] == "Internal failure"
|
|
|
|
|
|
def test_query_traces_no_filters(client: HoneycombClient) -> None:
|
|
result = client.query_traces()
|
|
|
|
assert result["success"] is False
|
|
assert "require a service_name or trace_id" in result["error"]
|
|
|
|
|
|
def test_run_query_create_query_failure(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": False, "error": "Query error"})
|
|
result = client.run_query({"calculations": []})
|
|
assert result["success"] is False
|
|
assert result["error"] == "Query error"
|
|
|
|
|
|
def test_run_query_missing_result_id(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={"success": True, "result": {"complete": False}} # No 'id'
|
|
)
|
|
result = client.run_query({"calculations": []})
|
|
assert result["success"] is False
|
|
assert "no result ID" in result["error"]
|
|
|
|
|
|
def test_run_query_polling_fetch_failure(client: HoneycombClient) -> None:
|
|
client.create_query = MagicMock(return_value={"success": True, "query_id": "q1"})
|
|
client.create_query_result = MagicMock(
|
|
return_value={"success": True, "result": {"id": "r1", "complete": False}}
|
|
)
|
|
# Fail on the first poll
|
|
client.get_query_result = MagicMock(return_value={"success": False, "error": "Fetch failed"})
|
|
|
|
with patch("time.sleep"):
|
|
result = client.run_query({"calculations": []}, poll_attempts=1)
|
|
|
|
assert result["success"] is False
|
|
assert result["error"] == "Fetch failed"
|