"""Unit tests for the unified Monitor: dispatch, config plumbing, form parsing. No DB / no network — the per-type probes are monkeypatched so we exercise the run_monitor dispatcher and the route helpers in isolation. The DB-backed status source + migration are covered in the integration lane. """ import asyncio from steward.models.monitors import Monitor, MONITOR_TYPE_VALUES from steward.monitors import runner as runner_mod from steward.monitors.routes import _build_config, _normalise_target def _monitor(mtype, target="example.com", cfg=None): m = Monitor(id="m1", name="t", type=mtype, target=target) m.set_config(cfg or {}) return m def test_monitor_type_values(): assert MONITOR_TYPE_VALUES == ("icmp", "tcp", "dns", "http") def test_config_json_roundtrip(): m = _monitor("tcp", cfg={"port": 8443}) assert m.config == {"port": 8443} m.set_config({"port": 22}) assert m.config["port"] == 22 def test_config_handles_bad_json(): m = Monitor(id="x", name="t", type="tcp", target="h", config_json="not json") assert m.config == {} def test_run_monitor_unknown_type_is_safe(): m = _monitor("bogus") res = asyncio.run(runner_mod.run_monitor(m)) assert res["is_up"] is False assert "bogus" in res["error_msg"] def test_run_monitor_tcp_uses_config_port(monkeypatch): seen = {} async def fake_tcp(address, port): seen["address"], seen["port"] = address, port return {"is_up": True, "response_ms": 12.0} monkeypatch.setattr(runner_mod, "tcp_check", fake_tcp) res = asyncio.run(runner_mod.run_monitor(_monitor("tcp", "db.local", {"port": 5432}))) assert seen == {"address": "db.local", "port": 5432} assert res["is_up"] is True and res["response_ms"] == 12.0 # Untouched fields stay None (full MonitorResult superset is always present). assert res["status_code"] is None and res["resolved_ip"] is None def test_run_monitor_http_passes_config(monkeypatch): seen = {} async def fake_http(**kwargs): seen.update(kwargs) return {"is_up": True, "status_code": 200, "response_ms": 5.0} monkeypatch.setattr(runner_mod, "http_check", fake_http) cfg = {"method": "HEAD", "expected_status": 204, "content_match": "ok", "headers": {"X": "1"}, "timeout_seconds": 3, "follow_redirects": False, "verify_ssl": False} res = asyncio.run(runner_mod.run_monitor(_monitor("http", "https://x.test", cfg))) assert seen["method"] == "HEAD" and seen["expected_status"] == 204 assert seen["headers"] == {"X": "1"} and seen["verify_ssl"] is False assert res["status_code"] == 200 def test_run_monitor_dns_passes_expected_ip(monkeypatch): seen = {} async def fake_dns(address, expected_ip=None): seen["address"], seen["expected_ip"] = address, expected_ip return {"is_up": True, "resolved_ip": "192.0.2.1"} monkeypatch.setattr(runner_mod, "dns_check", fake_dns) res = asyncio.run(runner_mod.run_monitor(_monitor("dns", "host.test", {"expected_ip": "192.0.2.1"}))) assert seen == {"address": "host.test", "expected_ip": "192.0.2.1"} assert res["resolved_ip"] == "192.0.2.1" # ── route form helpers ──────────────────────────────────────────────────────── def test_normalise_target_adds_https_for_bare_http_host(): assert _normalise_target("http", "example.com") == "https://example.com" assert _normalise_target("http", "http://x.com") == "http://x.com" assert _normalise_target("tcp", "example.com") == "example.com" class _Form(dict): """Minimal stand-in for a Quart form (dict + 'in' membership for checkboxes).""" def test_build_config_per_type(): assert _build_config(_Form(port="8080"), "tcp") == {"port": 8080} assert _build_config(_Form(), "tcp") == {"port": 80} assert _build_config(_Form(expected_ip="10.0.0.1"), "dns") == {"expected_ip": "10.0.0.1"} assert _build_config(_Form(), "dns") == {"expected_ip": None} assert _build_config(_Form(), "icmp") == {} http = _build_config(_Form(method="get", expected_status="201", follow_redirects="on"), "http") assert http["method"] == "GET" and http["expected_status"] == 201 assert http["follow_redirects"] is True # present in form → checked assert http["verify_ssl"] is False # absent from form → unchecked