Skip to content

Commit 11a88eb

Browse files
7vigneshCopilot
andauthored
test(tools): add unit tests for Azure Monitor Logs tool (#1007)
Fixes #996 Adds comprehensive unit tests for AzureMonitorLogsTool covering: - BaseToolContract - is_available (all required fields + missing azure key) - extract_params mapping and defaults - _ensure_take_clause all 3 branches - _bounded_limit: max_results cap, _MAX_HARD_LIMIT hard ceiling, minimum-of-one - run happy path with outgoing request assertions - run HTTP error path with full return contract - run unavailable path Co-authored-by: 7vignesh <97684755+7vignesh@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6e8f880 commit 11a88eb

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""Tests for AzureMonitorLogsTool (function-based, @tool decorated)."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, cast
6+
from unittest.mock import MagicMock
7+
8+
import pytest
9+
10+
from app.tools.AzureMonitorLogsTool import (
11+
_bounded_limit,
12+
_ensure_take_clause,
13+
query_azure_monitor_logs,
14+
)
15+
from tests.tools.conftest import BaseToolContract
16+
17+
18+
def _registered_tool() -> Any:
19+
return cast(Any, query_azure_monitor_logs).__opensre_registered_tool__
20+
21+
22+
class TestAzureMonitorLogsToolContract(BaseToolContract):
23+
def get_tool_under_test(self):
24+
return _registered_tool()
25+
26+
27+
@pytest.mark.parametrize(
28+
"sources,expected",
29+
[
30+
(
31+
{
32+
"azure": {
33+
"connection_verified": True,
34+
"workspace_id": "workspace-123",
35+
"access_token": "token-abc",
36+
}
37+
},
38+
True,
39+
),
40+
(
41+
{
42+
"azure": {
43+
"connection_verified": False,
44+
"workspace_id": "workspace-123",
45+
"access_token": "token-abc",
46+
}
47+
},
48+
False,
49+
),
50+
(
51+
{
52+
"azure": {
53+
"connection_verified": True,
54+
"workspace_id": "",
55+
"access_token": "token-abc",
56+
}
57+
},
58+
False,
59+
),
60+
(
61+
{
62+
"azure": {
63+
"connection_verified": True,
64+
"workspace_id": "workspace-123",
65+
"access_token": "",
66+
}
67+
},
68+
False,
69+
),
70+
({}, False),
71+
],
72+
)
73+
def test_is_available_requires_verified_workspace_and_token(sources: dict, expected: bool) -> None:
74+
rt = _registered_tool()
75+
assert rt.is_available(sources) is expected
76+
77+
78+
def test_extract_params_maps_fields_and_defaults() -> None:
79+
rt = _registered_tool()
80+
params = rt.extract_params(
81+
{
82+
"azure": {
83+
"workspace_id": " workspace-123 ",
84+
"access_token": " token-abc ",
85+
"endpoint": " https://api.loganalytics.io ",
86+
}
87+
}
88+
)
89+
90+
assert params["workspace_id"] == "workspace-123"
91+
assert params["access_token"] == "token-abc"
92+
assert params["endpoint"] == "https://api.loganalytics.io"
93+
assert params["time_range_minutes"] == 60
94+
assert params["limit"] == 50
95+
96+
97+
def test_bounded_limit_caps_requested_limit() -> None:
98+
assert _bounded_limit(300, 100) == 100
99+
100+
101+
def test_bounded_limit_enforces_hard_ceiling() -> None:
102+
# max_results above _MAX_HARD_LIMIT (200) must still be capped at 200
103+
assert _bounded_limit(500, 300) == 200
104+
105+
106+
def test_bounded_limit_enforces_minimum_of_one() -> None:
107+
assert _bounded_limit(0, 100) == 1
108+
assert _bounded_limit(-10, 100) == 1
109+
110+
111+
@pytest.mark.parametrize(
112+
"query,limit,expected",
113+
[
114+
("", 10, "AppTraces | order by TimeGenerated desc | take 10"),
115+
(
116+
"AppTraces | order by TimeGenerated desc",
117+
5,
118+
"AppTraces | order by TimeGenerated desc | take 5",
119+
),
120+
("AppTraces | take 100", 5, "AppTraces | take 100"),
121+
],
122+
)
123+
def test_ensure_take_clause_branches(query: str, limit: int, expected: str) -> None:
124+
assert _ensure_take_clause(query, limit) == expected
125+
126+
127+
def test_run_happy_path(monkeypatch: pytest.MonkeyPatch) -> None:
128+
mocked_response = MagicMock()
129+
mocked_response.raise_for_status.return_value = None
130+
mocked_response.json.return_value = {
131+
"tables": [
132+
{
133+
"columns": [
134+
{"name": "TimeGenerated"},
135+
{"name": "Message"},
136+
],
137+
"rows": [
138+
["2026-04-27T10:00:00Z", "error: failed to connect"],
139+
["2026-04-27T10:01:00Z", "info: retry succeeded"],
140+
],
141+
}
142+
]
143+
}
144+
145+
captured: dict[str, Any] = {}
146+
147+
def fake_post(url: str, **kwargs: Any) -> MagicMock:
148+
captured["url"] = url
149+
captured["headers"] = kwargs.get("headers", {})
150+
captured["json"] = kwargs.get("json", {})
151+
return mocked_response
152+
153+
monkeypatch.setattr("app.tools.AzureMonitorLogsTool.httpx.post", fake_post)
154+
155+
result = query_azure_monitor_logs(
156+
workspace_id="workspace-123",
157+
access_token="token-abc",
158+
query="AppTraces | order by TimeGenerated desc",
159+
limit=2,
160+
)
161+
162+
assert result["available"] is True
163+
assert result["source"] == "azure"
164+
assert result["total_returned"] == 2
165+
assert result["rows"][0]["Message"] == "error: failed to connect"
166+
# Assert the outgoing request was constructed correctly
167+
assert "workspace-123" in captured["url"]
168+
assert captured["headers"]["Authorization"] == "Bearer token-abc"
169+
assert "query" in captured["json"]
170+
171+
172+
def test_run_http_error_path(monkeypatch: pytest.MonkeyPatch) -> None:
173+
mocked_response = MagicMock()
174+
mocked_response.raise_for_status.side_effect = Exception("401 Client Error: Unauthorized")
175+
mocked_response.json.return_value = {}
176+
177+
monkeypatch.setattr(
178+
"app.tools.AzureMonitorLogsTool.httpx.post",
179+
lambda *_args, **_kwargs: mocked_response,
180+
)
181+
182+
result = query_azure_monitor_logs(
183+
workspace_id="workspace-123",
184+
access_token="token-abc",
185+
query="AppTraces",
186+
)
187+
188+
assert "error" in result
189+
assert "401" in result["error"]
190+
assert result["source"] == "azure"
191+
assert result["available"] is False
192+
assert result["rows"] == []
193+
194+
195+
def test_run_unavailable_without_credentials() -> None:
196+
result = query_azure_monitor_logs(workspace_id="", access_token="", query="AppTraces")
197+
198+
assert result["available"] is False
199+
assert "missing azure credentials" in result["error"].lower()

0 commit comments

Comments
 (0)