Files
checkbot/tests/test_selftest.py
patrickandClaude Sonnet 5.5 b937a57602
test / pytest (3.11) (push) Waiting to run
test / pytest (3.13) (push) Waiting to run
feat: Selbsttest bei Versionswechsel und Spaltenprüfung ohne Probleme
Der Runner prüft die Checkmk-Version alle 5 Minuten und startet bei einem
Wechsel sofort einen Selbsttest (Cooldown, Erstlauf ausgenommen). Die
Spaltenprüfung fragt bei leerer Problemliste eine ungefilterte Stichprobe ab;
leeres Checkmk gilt als nicht prüfbar, nicht als Fehler.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 13:00:36 +01:00

231 lines
9.2 KiB
Python

import json
import os
import httpx
from app.checkmk import CheckmkClient
from app.crypto import SecretBox
from app.db import Database
from app.selftest import (CHECK_DOWNTIMES, CHECK_HOSTS, CHECK_SERVICES, CHECK_VERSION, STATUS_DEVIATED,
STATUS_FAILED, STATUS_OK, SelftestNotConfigured, SelftestRepository, SelftestRun,
SelftestService, message_for)
from app.selftest.checks import check_downtimes, check_hosts, check_services, check_version
from app.selftest.model import CheckResult
from app.settings import SettingsStore
def make_client(handler):
return CheckmkClient("https://cmk.local", "prod", "bot", "s3cret", transport=httpx.MockTransport(handler))
def service_row(host="h1", service="s1", state=2, **extra):
data = {"host_name": host, "description": service, "state": state, "state_type": 1, "plugin_output": "x",
"last_state_change": 1, "scheduled_downtime_depth": 0, "is_flapping": 0, "acknowledged": 0}
data.update(extra)
return {"extensions": data}
def host_row(name="h2", state=1, **extra):
data = {"name": name, "state": state, "plugin_output": "x", "last_state_change": 1,
"scheduled_downtime_depth": 0, "acknowledged": 0}
data.update(extra)
return {"extensions": data}
def good_handler(request):
if request.url.path.endswith("/version"):
return httpx.Response(200, json={"versions": {"checkmk": "2.5.0p1"}})
if request.url.path.endswith("/service/collections/all"):
return httpx.Response(200, json={"value": [service_row()]})
if request.url.path.endswith("/host/collections/all"):
return httpx.Response(200, json={"value": [host_row()]})
if request.url.path.endswith("/downtime/collections/all"):
return httpx.Response(200, json={"value": []})
raise AssertionError(request.url.path)
async def test_check_version_ok_deviated_and_failed():
ok, version = await check_version(make_client(good_handler))
assert ok.status == STATUS_OK and ok.name == CHECK_VERSION and version == "2.5.0p1"
deviated, version = await check_version(make_client(lambda r: httpx.Response(200, json={"versions": {}})))
assert deviated.status == STATUS_DEVIATED and version == ""
failed, version = await check_version(make_client(lambda r: httpx.Response(401, text="x")))
assert failed.status == STATUS_FAILED and version == ""
async def test_check_services_reports_missing_columns():
client = make_client(lambda r: httpx.Response(200, json={"value": [service_row()]}))
result, services = await check_services(client)
assert result.status == STATUS_OK and len(services) == 1
missing_optional = make_client(lambda r: httpx.Response(
200, json={"value": [{"extensions": {"host_name": "h", "description": "s", "state": 2}}]}))
result, services = await check_services(missing_optional)
assert result.status == STATUS_DEVIATED and "last_state_change" in result.text
missing_required = make_client(lambda r: httpx.Response(
200, json={"value": [{"extensions": {"description": "s", "state": 2}}]}))
result, services = await check_services(missing_required)
assert result.status == STATUS_FAILED and "host_name" in result.text
async def test_check_hosts_reports_get_fallback_as_deviated():
def handler(request):
if request.method == "POST":
return httpx.Response(422, text="x")
return httpx.Response(200, json={"value": [host_row(state=2)]})
result, hosts = await check_hosts(make_client(handler))
assert result.status == STATUS_DEVIATED and "GET-Fallback" in result.text and len(hosts) == 1
async def test_check_downtimes_falls_back_to_query_then_to_derived():
def query_fallback(request):
if "downtime_type" in request.url.params:
return httpx.Response(422, text="x")
return httpx.Response(200, json={"value": []})
result = await check_downtimes(make_client(query_fallback), [], [])
assert result.status == STATUS_DEVIATED and "query" in result.text
def all_fail(request):
return httpx.Response(404, text="x")
from app.checkmk import ServiceState
services = [ServiceState("h1", "s1", 2, True, "", 1, True, False)]
result = await check_downtimes(make_client(all_fail), services, [])
assert result.status == STATUS_DEVIATED and "abgeleitet" in result.text and "1 Downtime" in result.text
async def test_check_services_probes_sample_when_no_problems():
def handler(request):
body = json.loads(request.content)
if "query" in body:
return httpx.Response(200, json={"value": []})
return httpx.Response(200, json={"value": [service_row()]})
result, services = await check_services(make_client(handler))
assert result.status == STATUS_OK and "Stichprobe" in result.text and services == []
async def test_check_services_sample_detects_missing_column():
def handler(request):
body = json.loads(request.content)
if "query" in body:
return httpx.Response(200, json={"value": []})
return httpx.Response(200, json={"value": [
{"extensions": {"host_name": "h", "description": "s", "state": 2}}]})
result, services = await check_services(make_client(handler))
assert result.status == STATUS_DEVIATED and "last_state_change" in result.text
async def test_check_services_empty_checkmk_is_ok_not_checkable():
def handler(request):
return httpx.Response(200, json={"value": []})
result, services = await check_services(make_client(handler))
assert result.status == STATUS_OK and "nicht prüfbar" in result.text and services == []
async def test_check_hosts_probes_sample_when_no_problems():
def handler(request):
body = json.loads(request.content)
if "query" in body:
return httpx.Response(200, json={"value": []})
return httpx.Response(200, json={"value": [host_row()]})
result, hosts = await check_hosts(make_client(handler))
assert result.status == STATUS_OK and "Stichprobe" in result.text and hosts == []
async def test_check_hosts_empty_checkmk_is_ok_not_checkable():
def handler(request):
return httpx.Response(200, json={"value": []})
result, hosts = await check_hosts(make_client(handler))
assert result.status == STATUS_OK and "nicht prüfbar" in result.text and hosts == []
def make_env():
db = Database(":memory:")
store = SettingsStore(db, SecretBox(os.urandom(32)))
return db, store
def configure(store):
from app.constants import CHECKMK_SECRET, CHECKMK_SITE, CHECKMK_URL, CHECKMK_USER
store.set(CHECKMK_URL, "https://cmk.local", "t")
store.set(CHECKMK_SITE, "prod", "t")
store.set(CHECKMK_USER, "bot", "t")
store.set(CHECKMK_SECRET, "s3cret", "t", secret=True)
async def test_service_run_not_configured():
db, store = make_env()
service = SelftestService(store, SelftestRepository(db))
try:
await service.run()
except SelftestNotConfigured:
pass
else:
raise AssertionError("expected SelftestNotConfigured")
async def test_service_run_persists_and_detects_version_change(monkeypatch):
db, store = make_env()
configure(store)
repo = SelftestRepository(db)
service = SelftestService(store, repo,
client_factory=lambda s: make_client(good_handler), clock=lambda: 1_800_000_000.0)
run = await service.run()
assert run.version == "2.5.0p1" and run.version_changed is False
assert repo.last_run().version == "2.5.0p1"
def newer(request):
if request.url.path.endswith("/version"):
return httpx.Response(200, json={"versions": {"checkmk": "2.5.0p2"}})
return good_handler(request)
service2 = SelftestService(store, repo, client_factory=lambda s: make_client(newer),
clock=lambda: 1_800_000_100.0)
run2 = await service2.run()
assert run2.version == "2.5.0p2" and run2.version_changed is True
def test_overall_status_priority():
ok = CheckResult("a", STATUS_OK, "")
deviated = CheckResult("b", STATUS_DEVIATED, "")
failed = CheckResult("c", STATUS_FAILED, "")
assert SelftestRun(1, "v", [ok]).overall_status == STATUS_OK
assert SelftestRun(1, "v", [ok, deviated]).overall_status == STATUS_DEVIATED
assert SelftestRun(1, "v", [ok, deviated, failed]).overall_status == STATUS_FAILED
def run_with(status):
return SelftestRun(1, "2.5.0", [CheckResult(CHECK_VERSION, status, "x")])
def test_message_for_no_previous_is_silent():
assert message_for(None, run_with(STATUS_OK)) is None
def test_message_for_same_status_is_silent():
assert message_for(run_with(STATUS_OK), run_with(STATUS_OK)) is None
def test_message_for_status_change_to_failed_and_back_to_ok():
text = message_for(run_with(STATUS_OK), run_with(STATUS_FAILED))
assert text is not None and "fehler" in text
text = message_for(run_with(STATUS_FAILED), run_with(STATUS_OK))
assert text == "Selbsttest wieder ok"
def test_message_for_version_change():
previous = SelftestRun(1, "2.4.0", [CheckResult(CHECK_VERSION, STATUS_OK, "x")])
current = SelftestRun(2, "2.5.0", [CheckResult(CHECK_VERSION, STATUS_OK, "x")], version_changed=True)
text = message_for(previous, current)
assert text == "Checkmk-Version von 2.4.0 auf 2.5.0, Selbsttest: ok"