import pytest
from app.agent import AgentResult
from app.checkmk import ServiceState
from app.commands import AuditEvent, CommandDeps, CommandHandler, IncomingMessage
from app.ratelimit import FailureLimiter
ALLOWED = "111"
def svc(host="web01", service="CPU load", state=2, output="CRIT", downtime=False):
return ServiceState(host, service, state, True, output, 1, downtime, False)
class FakeReader:
def __init__(self):
self.problem_list = [svc(), svc(service="Disk", state=1), svc(service="Mem", state=3, downtime=True)]
self.services = [svc(), svc(service="Mem", state=0, output="ok")]
self.fail = False
async def problems(self):
if self.fail:
raise RuntimeError("http://cmk/secret")
return self.problem_list
async def host_services(self, host):
if self.fail:
raise RuntimeError("boom")
return self.services if host == "web01" else []
async def service_status(self, host, service):
return None
async def downtimes(self, host=None):
return []
class FakeAgent:
def __init__(self):
self.calls = []
async def ask(self, question):
self.calls.append(("ask", question))
return AgentResult("Antwort fett")
async def explain(self, host, service):
self.calls.append(("explain", host, service))
return AgentResult("Erklärung")
class Clock:
now = 0.0
def __call__(self):
return self.now
@pytest.fixture
def env():
reader, agent, events, clock = FakeReader(), FakeAgent(), [], Clock()
handler = CommandHandler(CommandDeps(reader, agent), lambda backend: ALLOWED if backend == "telegram"
else "@alice:Example.org", events.append,
limiter=FailureLimiter(5, 60.0, clock))
return handler, reader, agent, events, clock
def tg(text, sender="111"):
return IncomingMessage("telegram", sender, "chat1", text)
async def test_plain_chat_ignored(env):
handler, *_, events, _ = env
assert await handler.handle(tg("hallo")) is None
assert events == []
async def test_not_allowed_silent_but_audited(env):
handler, _, agent, events, _ = env
assert await handler.handle(tg("/ask x", sender="999")) is None
assert agent.calls == []
assert events == [AuditEvent("telegram", "999", "ask", "x", "denied")]
async def test_matrix_allowed_with_domain_case(env):
handler, *_ = env
reply = await handler.handle(IncomingMessage("matrix", "@alice:EXAMPLE.org", "!r:x", "!status"))
assert reply.text.startswith("Probleme: 3")
async def test_rate_limit_ask_only(env):
handler, _, agent, events, clock = env
for _ in range(5):
assert (await handler.handle(tg("/ask hi"))).text.startswith("Antwort")
blocked = await handler.handle(tg("/ask hi"))
assert "Zu viele" in blocked.text and len(agent.calls) == 5
assert events[-1].outcome == "rate_limited"
assert (await handler.handle(tg("/status"))).text.startswith("Probleme")
assert "Zu viele" in (await handler.handle(tg("/explain a b"))).text
clock.now = 61.0
assert (await handler.handle(tg("/ask hi"))).text.startswith("Antwort")
async def test_rate_limit_per_sender(env):
handler, *_ = env
for _ in range(5):
await handler.handle(tg("/ask hi"))
other = IncomingMessage("matrix", "@alice:example.org", "!r:x", "!ask hi")
assert (await handler.handle(other)).text.startswith("Antwort")
async def test_status_counts(env):
handler, *_ = env
reply = await handler.handle(tg("/status"))
assert reply.text == "Probleme: 3 (WARN: 1, CRIT: 1, UNKNOWN: 1), davon in Downtime: 1"
async def test_status_no_problems(env):
handler, reader, *_ = env
reader.problem_list = []
assert "Alles OK" in (await handler.handle(tg("/status"))).text
async def test_problems_limited_to_20(env):
handler, reader, *_ = env
reader.problem_list = [svc(service=f"s{i:02}") for i in range(25)]
reply = await handler.handle(tg("/problems"))
lines = reply.text.splitlines()
assert lines[0] == "Probleme (25):" and len(lines) == 1 + 20 + 1
assert lines[-1] == "... und 5 weitere"
async def test_problems_html_escaped(env):
handler, reader, *_ = env
reader.problem_list = [svc(host="