Files
FabledSteward/tests/core/test_monitors_page.py
T
bvandeusenandClaude Opus 5.5 c6fc02def5
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m3s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m22s
feat(monitors): a monitor's page reads as a page — response time over 24h, labelled actions, how long, not "ago"; the add form says what each type checks (#5214)
Monitor page: a response-time chart for the last day with failed checks
marked; Edit / Pause / Delete as labelled buttons; the state tile says
"for 29m" instead of "for at least 29m 25s ago"; the check strip fills
its card; no failures is one line, not an empty box; Settings names the
type, target and the actual global interval, with readable field names.

Add monitor: a line under the fields says what the selected type checks
and when it counts as up; Name says it defaults to the target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:57:09 -04:00

184 lines
10 KiB
Python

"""The Monitors page leads with what needs looking at (#5121): a count strip,
problems first, each monitor on a page of its own, adding behind a button."""
import re
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
import pytest
from quart import render_template, session
from steward.core.settings import to_thresholds_cfg
from steward.monitors.service import problems_first, row_state
from tests.test_style_tokens import hex_colours
TH = to_thresholds_cfg({}) # latency warn 50 / crit 200 ms; uptime warn 99 / crit 95 %
NOW = datetime.now(timezone.utc)
def _mon(name, mtype="http", enabled=True, mid=None):
return SimpleNamespace(id=mid or name, name=name, type=mtype, target=f"https://{name}/",
enabled=enabled, host_id=None, check_interval_seconds=0, config={})
def _check(up=True, ms=20.0, ago_min=1):
return SimpleNamespace(is_up=up, response_ms=ms, resolved_ip=None, status_code=200 if up else 503,
error_msg=None if up else "503 Service Unavailable", tls_expires_at=None,
checked_at=NOW - timedelta(minutes=ago_min))
@pytest.mark.parametrize("enabled, latest, uptime, state", [
(False, _check(False), 50.0, "paused"), # paused wins over everything
(True, None, None, "pending"),
(True, _check(False), 100.0, "down"),
(True, _check(ms=20), 100.0, "up"),
(True, _check(ms=120), 100.0, "up"), # slow but tolerable: coloured, not sorted
(True, _check(ms=250), 100.0, "degraded"), # past the latency critical cutoff
(True, _check(ms=20), 98.0, "degraded"), # uptime under its warning floor
(True, _check(ms=20), None, "up"), # no uptime yet is not a problem
])
def test_row_state(enabled, latest, uptime, state):
assert row_state(_mon("m", enabled=enabled), latest, uptime, TH) == state
def test_problems_come_first_then_by_name():
rows = [{"monitor": _mon(n), "state": s} for n, s in [
("alpha", "up"), ("Zed", "down"), ("beta", "paused"), ("gamma", "degraded"),
("delta", "pending"), ("able", "down")]]
assert [r["monitor"].name for r in problems_first(rows)] == [
"able", "Zed", "gamma", "delta", "alpha", "beta"]
def _row(m, state, latest, uptime=None, tls_days=None):
return {"monitor": m, "state": state, "latest": latest, "uptime": {"24h": uptime} if uptime is not None else {},
"tls_days": tls_days, "spark": "",
"heartbeat": [{"state": "up" if latest.is_up else "down", "title": "t"}] if latest else []}
async def test_the_list_counts_then_shows_problems_first(app):
data = problems_first([
_row(_mon("web", mid="m1"), "up", _check(ms=30), 100.0, tls_days=2.0),
_row(_mon("api", mid="m2"), "down", _check(False), 90.0),
_row(_mon("nas", "icmp", mid="m3", enabled=False), "paused", None),
])
counts = {"up": 1, "degraded": 0, "down": 1, "pending": 0, "paused": 1}
async with app.test_request_context("/monitors/rows"):
session["user_role"] = "operator"
page = await render_template("monitors/rows.html", monitor_data=data, counts=counts)
assert page.index('<span class="stat-tile-label">Down</span>') < page.index("ping-row")
assert page.index('href="/monitors/m2"') < page.index('href="/monitors/m1"') < page.index('href="/monitors/m3"')
assert "HTTP · https://web/" in page and "Ping · https://nas/" in page
assert '<span class="tone-ok">30 ms</span>' in page
assert '<span class="tone-crit">90.00%</span>' in page and '<span class="tone-ok">100.00%</span>' in page
assert 'class="tone-crit" title="TLS certificate expires in 2 days">2d' in page
# One header row names every column (#5270, absorbing the Status page).
head = page[page.index("mon-head"):page.index('href="/monitors/m2"')]
for label in ("Monitor", "Last 30 checks", "Latest", "24h", "7d", "30d", "TLS"):
assert f">{label}<" in head, label
assert '<span class="badge badge-crit">Down</span>' in page and '<span class="badge badge-dim">Paused</span>' in page
assert 'aria-label="Delete api"' in page and 'data-confirm="Delete monitor api?"' in page
assert "onclick" not in page and not hex_colours(page), hex_colours(page)
async def test_a_viewer_gets_no_row_actions_and_an_empty_board_says_so(app):
async with app.test_request_context("/monitors/rows"):
page = await render_template("monitors/rows.html", monitor_data=[_row(_mon("web"), "up", _check())],
counts={"up": 1, "degraded": 0, "down": 0, "pending": 0, "paused": 0})
empty = await render_template("monitors/rows.html", monitor_data=[],
counts=dict.fromkeys(("up", "degraded", "down", "pending", "paused"), 0))
assert "row-actions" not in page
assert ">TLS<" not in page # no monitor has a certificate: no column of dashes
assert "No monitors yet" in empty and "/monitors/new" not in empty
async def test_the_monitor_page_says_since_when_and_why(app):
m = _mon("shop <api>", mid="m9")
m.config = {"method": "GET", "expected_status": 200, "headers": {"Authorization": "s3cr3t-token"}, "verify_ssl": True}
down = _check(False, ms=None)
async with app.test_request_context("/monitors/m9"):
session["user_role"] = "admin"
page = await render_template(
"monitors/detail.html", monitor=m, host=None, latest=down, failures=[down], state="down",
since=datetime.now(timezone.utc) - timedelta(minutes=12), since_at_least=False, tls_days=None,
uptime={"24h": 93.0, "7d": None, "30d": 99.5},
heartbeat=[{"state": "up", "title": "a"}, {"state": "down", "title": "b"}],
history=[{"at": 1, "ms": 20.0, "down": 0}, {"at": 2, "ms": 30.0, "down": 0},
{"at": 3, "ms": None, "down": 3}], poll_interval=60)
assert '<a href="/monitors/">Monitors</a>' in page and "shop &lt;api&gt;" in page
assert ">for 12m<" in page # how long, not "… ago"
assert "Response time · last 24h" in page and '"down": 3' in page
assert "Expected status" in page and "1m <span" in page # the global interval, named
assert "503 Service Unavailable" in page
assert 'class="stat-tile stat-crit"' in page and "93.00%" in page
assert "Authorization" in page and "s3cr3t-token" not in page # header names, never their values
assert 'data-confirm="Delete monitor shop &lt;api&gt;? Its check history goes with it."' in page
assert ">Edit</a>" in page and ">Pause</button>" in page
assert not hex_colours(page), hex_colours(page)
async def test_adding_is_its_own_page_and_keeps_what_was_typed(app):
async with app.test_request_context("/monitors/new"):
session["user_role"] = "operator"
page = await render_template(
"monitors/form.html", monitor=None, mtype="tcp", config={"port": "22"}, types=[],
values={"name": "db", "target": "10.0.0.9", "host_id": "", "check_interval_seconds": "30"},
error="port must be between 1 and 65535, got 0.")
assert '<a href="/monitors/">Monitors</a>' in page and 'action="/monitors/add"' in page
for kind in ("Ping", "TCP", "HTTP", "DNS"): # each type says what it checks
assert f"<strong>{kind}</strong>" in page
assert '<p class="field-error" role="alert">port must be between 1 and 65535, got 0.</p>' in page
assert 'value="db"' in page and 'value="10.0.0.9"' in page and 'value="30"' in page
def test_down_reason_says_why_in_a_few_words():
from steward.monitors.service import down_reason
assert down_reason(_check(False)) == "503 Service Unavailable" # the probe's own error first
assert down_reason(SimpleNamespace(error_msg=None, status_code=502, content_matched=None)) == "HTTP 502"
assert down_reason(SimpleNamespace(error_msg=None, status_code=None, content_matched=False)) == \
"expected content not found"
assert down_reason(None) is None
async def test_a_down_monitor_says_since_when_and_why_in_the_list(app):
row = _row(_mon("api", mid="m2"), "down", _check(False), 90.0)
row.update(down_since=datetime.now(timezone.utc) - timedelta(minutes=12),
down_reason="connection refused")
async with app.test_request_context("/monitors/rows"):
page = await render_template("monitors/rows.html", monitor_data=[row],
counts={"up": 0, "degraded": 0, "down": 1, "pending": 0, "paused": 0})
page = " ".join(page.split())
assert re.search(r'Down <time [^>]*>12m</time> · connection refused', page), page
async def _widget(app, problems, rest, more=0, counts=None):
counts = counts or {"up": len(rest), "degraded": 0, "down": len(problems), "pending": 0, "paused": 0}
async with app.test_request_context("/monitors/widget"):
session["user_id"] = "u1"
return await render_template("monitors/widget.html", problems=problems, rest=rest, more=more,
counts=counts, total=len(problems) + len(rest) + more,
show_down_only=False, widget_id="1")
async def test_the_widget_gives_a_verdict_then_problems_with_uptime_columns(app):
"""#5250: one home for uptime. A verdict, the down monitor first with why
and since when, then each monitor's latency and 24h/7d/30d under one header."""
down = {**_row(_mon("api", mid="m2"), "down", _check(False)),
"uptime": {"24h": 90.0, "7d": 97.5, "30d": 99.12},
"down_since": datetime.now(timezone.utc) - timedelta(minutes=12), "down_reason": "HTTP 503"}
up = {**_row(_mon("web", mid="m1"), "up", _check(ms=31)), "uptime": {"24h": 100.0, "7d": 99.96}}
page = await _widget(app, [down], [up], more=3)
assert "1 of 5 monitors down." in page
assert page.index(">api<") < page.index(">web<")
assert 'href="/monitors/m2"' in page and "went down" in page and "12m ago" in page and "HTTP 503" in page
assert page.count("<th>") == 5 and "<th>30d</th>" in page
assert "99.96%" in page and "31 ms" in page
assert ">—<" in page # web has no 30d yet: a dash, not 0
assert "+3 more up" in page
assert not hex_colours(page), hex_colours(page)
async def test_the_widget_says_when_all_is_well(app):
up = {**_row(_mon("web", mid="m1"), "up", _check()), "uptime": {"24h": 100.0}}
page = await _widget(app, [], [up])
assert "All 1 monitor up." in page and "tone-ok" in page