Skip to content

Commit 38d1d32

Browse files
committed
add monitor validate route
1 parent e6b159b commit 38d1d32

File tree

2 files changed

+168
-4
lines changed

2 files changed

+168
-4
lines changed

src/components/http_server/monitor_routes.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,58 @@ async def monitor_enable(request: Request) -> Response:
108108
return web.json_response(error_response, status=400)
109109

110110

111+
@monitor_routes.post(base_route + "/validate")
112+
@monitor_routes.post(base_route + "/validate/")
113+
async def monitor_validate(request: Request) -> Response:
114+
"""Route to check a monitor without registering it"""
115+
request_data = await request.json()
116+
monitor_code = request_data.get("monitor_code")
117+
118+
error_response: dict[str, str | list[Any]]
119+
120+
if monitor_code is None:
121+
error_response = {"status": "error", "message": "'monitor_code' parameter is required"}
122+
return web.json_response(error_response, status=400)
123+
124+
try:
125+
await commands.monitor_code_validate(monitor_code)
126+
except pydantic.ValidationError as e:
127+
error_response = {
128+
"status": "error",
129+
"message": "Type validation error",
130+
"error": [
131+
{
132+
"loc": list(error["loc"]),
133+
"type": error["type"],
134+
"msg": error["msg"],
135+
}
136+
for error in e.errors()
137+
],
138+
}
139+
return web.json_response(error_response, status=400)
140+
except MonitorValidationError as e:
141+
error_response = {
142+
"status": "error",
143+
"message": "Module didn't pass check",
144+
"error": e.get_error_message(),
145+
}
146+
return web.json_response(error_response, status=400)
147+
except Exception as e:
148+
error_response = {"status": "error", "error": str(e)}
149+
_logger.error(traceback.format_exc().strip())
150+
return web.json_response(error_response, status=400)
151+
152+
success_response = {"status": "monitor_validated"}
153+
return web.json_response(success_response)
154+
155+
111156
@monitor_routes.post(base_route + "/register/{monitor_name}")
112157
@monitor_routes.post(base_route + "/register/{monitor_name}/")
113158
async def monitor_register(request: Request) -> Response:
114159
"""Route to register a monitor"""
115-
request_data = await request.json()
116-
117160
monitor_name = request.match_info["monitor_name"]
161+
162+
request_data = await request.json()
118163
monitor_code = request_data.get("monitor_code")
119164
additional_files = request_data.get("additional_files", {})
120165

tests/components/http_server/test_monitor_routes.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import AsyncMock
1+
from unittest.mock import AsyncMock, MagicMock
22

33
import aiohttp
44
import pytest
@@ -7,6 +7,7 @@
77
import commands as commands
88
import components.controller.controller as controller
99
import components.http_server as http_server
10+
import components.monitors_loader as monitors_loader
1011
import databases as databases
1112
from models import CodeModule, Monitor
1213

@@ -200,6 +201,124 @@ async def test_monitor_enable_error(mocker, clear_database):
200201
}
201202

202203

204+
async def test_monitor_validate(mocker):
205+
"""The 'monitor validate' route should validate the provided module code"""
206+
monitor_code_validate_spy: AsyncMock = mocker.spy(commands, "monitor_code_validate")
207+
208+
with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
209+
monitor_code = file.read()
210+
211+
request_payload = {"monitor_code": monitor_code}
212+
213+
url = BASE_URL + "/validate"
214+
async with aiohttp.ClientSession() as session:
215+
async with session.post(url, json=request_payload) as response:
216+
response_data = await response.json()
217+
218+
assert response_data == {"status": "monitor_validated"}
219+
monitor_code_validate_spy.assert_awaited_once_with(monitor_code)
220+
221+
222+
async def test_monitor_validate_missing_monitor_code():
223+
"""The 'monitor validate' route should return an error any required parameter is missing"""
224+
url = BASE_URL + "/validate"
225+
async with aiohttp.ClientSession() as session:
226+
async with session.post(url, json={}) as response:
227+
assert await response.json() == {
228+
"status": "error",
229+
"message": "'monitor_code' parameter is required",
230+
}
231+
232+
233+
async def test_monitor_validate_dataclass_validation_error():
234+
"""The 'monitor validate' route should return an error if the provided module code has a
235+
'pydantic.ValidationError'"""
236+
request_payload = {
237+
"monitor_code": "\n".join(
238+
[
239+
"from pydantic.dataclasses import dataclass",
240+
"\n",
241+
"@dataclass",
242+
"class Data:",
243+
" value: str",
244+
"\n",
245+
"data = Data(value=123)",
246+
]
247+
),
248+
}
249+
250+
url = BASE_URL + "/validate"
251+
async with aiohttp.ClientSession() as session:
252+
async with session.post(url, json=request_payload) as response:
253+
assert await response.json() == {
254+
"status": "error",
255+
"message": "Type validation error",
256+
"error": [
257+
{
258+
"loc": ["value"],
259+
"type": "string_type",
260+
"msg": "Input should be a valid string",
261+
},
262+
],
263+
}
264+
265+
266+
async def test_monitor_validate_check_fail(mocker):
267+
"""The 'monitor validate' route should return an error if the provided module code is invalid"""
268+
check_monitor_spy: MagicMock = mocker.spy(monitors_loader, "check_monitor")
269+
270+
monitor_code = "import time"
271+
272+
request_payload = {"monitor_code": monitor_code}
273+
274+
url = BASE_URL + "/validate"
275+
async with aiohttp.ClientSession() as session:
276+
async with session.post(url, json=request_payload) as response:
277+
assert await response.json() == {
278+
"status": "error",
279+
"message": "Module didn't pass check",
280+
"error": "\n".join(
281+
[
282+
f"Monitor '{check_monitor_spy.call_args[0][0]}' has the following errors:",
283+
" 'monitor_options' is required",
284+
" 'issue_options' is required",
285+
" 'IssueDataType' is required",
286+
" 'search' function is required",
287+
" 'update' function is required",
288+
]
289+
),
290+
}
291+
292+
293+
@pytest.mark.parametrize(
294+
"monitor_code, expected_error",
295+
[
296+
("something", "name 'something' is not defined"),
297+
("import time;\n\ntime.abc()", "module 'time' has no attribute 'abc'"),
298+
(
299+
"print('a",
300+
"unterminated string literal (detected at line 1) ({monitor_name}.py, line 1)",
301+
),
302+
],
303+
)
304+
async def test_monitor_validate_invalid_monitor_code(mocker, monitor_code, expected_error):
305+
"""The 'monitor validate' route should return an error if the provided module code has any
306+
errors"""
307+
check_monitor_spy: MagicMock = mocker.spy(monitors_loader, "check_monitor")
308+
309+
request_payload = {
310+
"monitor_code": monitor_code,
311+
}
312+
313+
url = BASE_URL + "/validate"
314+
async with aiohttp.ClientSession() as session:
315+
async with session.post(url, json=request_payload) as response:
316+
assert await response.json() == {
317+
"status": "error",
318+
"error": expected_error.format(monitor_name=check_monitor_spy.call_args.args[0]),
319+
}
320+
321+
203322
@pytest.mark.parametrize(
204323
"monitor_name",
205324
[
@@ -350,7 +469,7 @@ async def test_monitor_register_dataclass_validation_error():
350469
}
351470

352471

353-
async def test_monitor_register_check_fail(caplog):
472+
async def test_monitor_register_check_fail():
354473
"""The 'monitor register' route should return an error if the provided module code is invalid"""
355474
monitor_code = "import time"
356475

0 commit comments

Comments
 (0)