mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
import importlib.util
|
|
import io
|
|
import json
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
|
|
def load_module_from_path(mod_name: str, path: str):
|
|
"""Dynamically load a module from a filesystem path."""
|
|
spec = importlib.util.spec_from_file_location(mod_name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module) # type: ignore[attr-defined]
|
|
return module
|
|
|
|
|
|
def _target(codes, scheme="https", timeout=10):
|
|
return {"codes": codes, "scheme": scheme, "timeout": timeout}
|
|
|
|
|
|
class _Response:
|
|
def __init__(self, status_code):
|
|
self.status_code = status_code
|
|
|
|
|
|
class TestStandaloneCheckerScript(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
from . import PROJECT_ROOT
|
|
|
|
cls.script_path = str(
|
|
PROJECT_ROOT
|
|
/ "roles"
|
|
/ "sys-ctl-hlth-webserver"
|
|
/ "files"
|
|
/ "python"
|
|
/ "script.py"
|
|
)
|
|
if not Path(cls.script_path).is_file():
|
|
raise FileNotFoundError(f"Cannot find script.py at {cls.script_path}")
|
|
cls.script = load_module_from_path("health_script", cls.script_path)
|
|
|
|
def test_rejects_invalid_json(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.script.main(["--targets", '{"bad json": {'])
|
|
|
|
def test_rejects_non_mapping_json(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.script.main(["--targets", '["not", "a", "mapping"]'])
|
|
|
|
def test_rejects_unknown_scheme(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.script.main(
|
|
["--targets", json.dumps({"a.example.org": _target([200], "ftp")})]
|
|
)
|
|
|
|
def test_rejects_missing_timeout(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.script.main(
|
|
[
|
|
"--targets",
|
|
json.dumps({"a.example.org": {"codes": [200], "scheme": "https"}}),
|
|
]
|
|
)
|
|
|
|
@patch("requests.head")
|
|
def test_all_ok_returns_zero(self, mock_head):
|
|
statuses = {"ok1.example.org": 200, "ok2.example.org": 301}
|
|
mock_head.side_effect = lambda url, **_: _Response(
|
|
statuses[url.split("://", 1)[1]]
|
|
)
|
|
|
|
exit_code, output = self._run_main(
|
|
{
|
|
"ok1.example.org": _target([200, 302, 301]),
|
|
"ok2.example.org": _target([301]),
|
|
}
|
|
)
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertIn("ok1.example.org: OK", output)
|
|
self.assertIn("ok2.example.org: OK", output)
|
|
|
|
@patch("requests.head")
|
|
def test_mismatches_counted(self, mock_head):
|
|
mock_head.side_effect = lambda url, **_: _Response(200)
|
|
|
|
exit_code, output = self._run_main(
|
|
{"bad.example.org": _target([404]), "ok.example.org": _target([200])}
|
|
)
|
|
self.assertEqual(exit_code, 1)
|
|
self.assertIn("bad.example.org: ERROR: Expected [404]. Got 200.", output)
|
|
|
|
@patch("requests.head")
|
|
def test_non_list_codes_sanitize_to_empty_and_fail(self, mock_head):
|
|
mock_head.side_effect = lambda url, **_: _Response(200)
|
|
|
|
exit_code, output = self._run_main({"foo.example.org": _target("not-a-list")})
|
|
self.assertEqual(exit_code, 1)
|
|
self.assertIn(
|
|
"foo.example.org: ERROR: No expectations provided. Got 200.", output
|
|
)
|
|
|
|
@patch("requests.head")
|
|
def test_each_domain_uses_its_own_scheme_and_timeout(self, mock_head):
|
|
seen = {}
|
|
|
|
def head(url, allow_redirects=False, timeout=None, verify=True):
|
|
seen[url.split("://", 1)[1]] = (url, timeout)
|
|
return _Response(200)
|
|
|
|
mock_head.side_effect = head
|
|
|
|
exit_code, _ = self._run_main(
|
|
{
|
|
"clear.example.org": _target([200], "https", 10),
|
|
"svc.abcd1234efgh.onion": _target([200], "http", 30),
|
|
}
|
|
)
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(
|
|
seen["svc.abcd1234efgh.onion"], ("http://svc.abcd1234efgh.onion", 30)
|
|
)
|
|
self.assertEqual(seen["clear.example.org"], ("https://clear.example.org", 10))
|
|
|
|
@patch("requests.head")
|
|
def test_output_stays_sorted_by_domain(self, mock_head):
|
|
mock_head.side_effect = lambda url, **_: _Response(200)
|
|
|
|
targets = {f"d{i:02d}.example.org": _target([200]) for i in reversed(range(40))}
|
|
exit_code, output = self._run_main(targets)
|
|
self.assertEqual(exit_code, 0)
|
|
self.assertEqual(
|
|
output.splitlines(), [f"d{i:02d}.example.org: OK" for i in range(40)]
|
|
)
|
|
|
|
def _run_main(self, targets):
|
|
buffer = io.StringIO()
|
|
with redirect_stdout(buffer):
|
|
exit_code = self.script.main(["--targets", json.dumps(targets)])
|
|
return exit_code, buffer.getvalue()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|