Files
FabledSteward/tests/plugins/syslog/test_syslog_page.py
T
bvandeusenandClaude Opus 5.5 ed9da56efa
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m29s
CI / integration (push) Successful in 1m38s
CI / publish (push) Successful in 20s
feat(syslog): set-up help on the page, port 5514 published, Docker-hidden senders called out (#6003)
The Syslog page says where to send (this server, its port and protocols) and
where the setting is on Synology, QNAP, TrueNAS, OpenWrt, UniFi and rsyslog,
open until a device sends. A sender whose address is on Steward's own
container network (Swarm ingress publishing hides the real sender) is
flagged, with the host-mode ports to publish instead. compose files and the
Dockerfile publish 5514/udp and /tcp; docs/plugins/syslog.md describes the
plugin and the deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 07:01:56 -04:00

151 lines
8.5 KiB
Python

"""The Syslog page (#5999): whether the listener is open or why not, the
sources kept, and the senders offered to an admin to add or ignore."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace as NS
from tests._templates import render_plugin_template
from tests.test_style_tokens import hex_colours
def _listener(**over):
base = {"port": 5514, "protocols": ["UDP", "TCP"], "listening": True, "error": None,
"bound_at": datetime.now(timezone.utc) - timedelta(hours=2), "received": 1204,
"dropped": 0, "unoffered": 0, "connections": 1, "senders_full": False, "max_senders": 200}
return {**base, **over}
def _source(name, state, level=6, line="nfsd: server not responding", host=None):
now = datetime.now(timezone.utc)
return {"id": f"id-{name}", "address": "192.168.0.7", "state": state, "name": name,
"hostname_seen": "synnas", "host": host, "first_seen_at": now - timedelta(days=1),
"last_seen_at": now - timedelta(minutes=3), "lines_total": 1204,
"severity": "err" if level == 3 else "info", "level": level, "last_line": line,
"suggested_host": "h1", "rate": 12 if state == "accepted" else None}
async def _render(app, role, **ctx):
from quart import session
ctx = {"listener": _listener(), "sources": [], "offered": [], "ignored": [],
"hosts": [NS(id="h1", name="SynNAS", address="192.168.0.7")],
"server": "steward.lan", "now": datetime.now(timezone.utc), **ctx}
async with app.test_request_context("/plugins/syslog/"):
session["user_role"] = role
page = await render_plugin_template(app, "syslog", "syslog/index.html", **ctx)
return " ".join(page.split())
async def test_an_offered_sender_can_be_added_or_ignored_by_an_admin(app):
page = await _render(app, "admin", offered=[_source("synnas", "offered", level=3)])
assert "Sending, not added" in page and "calls itself synnas" in page
assert 'action="/plugins/syslog/sources/id-synnas/add"' in page
assert 'action="/plugins/syslog/sources/id-synnas/ignore"' in page
assert '<option value="h1" selected>SynNAS</option>' in page # the host at its address
assert 'class="tone-crit">err</span> nfsd: server not responding' in page
assert "UDP and TCP port 5514" in page and "1,204 lines received" in page
main = page[page.index("<main"):]
assert "font-weight:600" not in main and not hex_colours(main), hex_colours(main)
async def test_a_viewer_sees_sources_but_no_controls(app):
page = await _render(app, "viewer", sources=[_source("SynNAS", "accepted", host=NS(id="h1", name="SynNAS"))],
offered=[_source("router", "offered")])
assert 'href="/hosts/h1"' in page and 'href="/plugins/syslog/sources/id-SynNAS"' in page
assert "/add" not in page and "/ignore" not in page and "/edit" not in page
assert "Sending, not added" in page
async def test_a_port_it_cannot_open_is_said(app):
page = await _render(app, "admin", listener=_listener(
listening=False, error="Port 514 couldn't be opened (UDP: Permission denied)."))
assert "Not listening" in page and "Port 514 couldn&#39;t be opened" in page
assert "below 1024" in page
assert "No sources yet." in page
async def test_setup_help_names_this_server_and_opens_until_a_device_sends(app):
page = await _render(app, "viewer")
assert '<details class="card" id="syslog-setup" open>' in page
assert "send to <span class=\"mono\">steward.lan</span>, port <span class=\"mono\">5514</span>, UDP or TCP" in page
assert "*.* @@steward.lan:5514" in page and "mode: host" in page
added = await _render(app, "viewer", sources=[_source("SynNAS", "accepted")])
assert '<details class="card" id="syslog-setup">' in added # closed once one sends
async def test_a_sender_on_dockers_own_network_is_called_out(app):
hidden = {**_source("ingress", "offered"), "address": "10.0.0.2", "docker_address": True}
page = await _render(app, "admin", offered=[hidden, {**hidden, "id": "id-2"}])
assert "10.0.0.2 is on Steward's own container network" in page
assert page.count("Docker's address, not the device's") == 2
assert "own container network" not in await _render(app, "admin", offered=[_source("synnas", "offered")])
async def test_the_edit_page_has_its_trail_and_a_remove(app):
from quart import session
source = NS(id="s1", name="SynNAS", hostname_seen="synnas", address="192.168.0.7", host_id=None)
async with app.test_request_context("/plugins/syslog/sources/s1/edit"):
session["user_role"] = "admin"
page = await render_plugin_template(app, "syslog", "syslog/edit.html", source=source,
hosts=[], label="SynNAS")
page = " ".join(page.split())
assert 'href="/plugins/syslog/">Syslog</a>' in page
assert 'action="/plugins/syslog/sources/s1/delete"' in page and "data-confirm=" in page
def _line(i, level=6, app="kernel", message="nfsd: server not responding", minutes=1):
now = datetime.now(timezone.utc)
return {"id": f"l{i}", "received_at": now - timedelta(minutes=minutes), "sent_at": None,
"level": level, "severity": {3: "err", 4: "warning"}.get(level, "info"),
"facility": "kern", "hostname": "synnas", "app": app, "message": message,
"group": "Today", "opens_group": i == 0}
async def test_a_source_page_filters_its_lines_and_scrolls_on(app):
from quart import session
from plugins.syslog.service import SEVERITY_CHOICES, LineFilter
src = {**_source("SynNAS", "accepted"), "rate": 42}
async with app.test_request_context("/plugins/syslog/sources/id-SynNAS"):
session["user_role"] = "viewer"
page = await render_plugin_template(
app, "syslog", "syslog/source.html", source=src, f=LineFilter(severity=3), filtered=True,
lines=[_line(0, level=3), _line(1, app="", message="a <b> tag")],
more_url="/plugins/syslog/sources/id-SynNAS?severity=3&before=x", severities=SEVERITY_CHOICES,
apps=["kernel", "nfsd"], ranges=["1h", "24h"])
page = " ".join(page.split())
assert 'href="/plugins/syslog/">Syslog</a>' in page # its trail
assert "42 lines in the last hour" in page
assert '<option value="3" selected>Errors and worse</option>' in page
assert '<tr class="table-divider"><th colspan="4" scope="rowgroup">Today</th></tr>' in page
assert '<span class="tone-crit">err</span>' in page and "a &lt;b&gt; tag" in page
assert 'hx-get="/plugins/syslog/sources/id-SynNAS?severity=3&amp;before=x"' in page
assert "/edit" not in page # a viewer
main = page[page.index("<main"):]
assert "font-weight:600" not in main and not hex_colours(main), hex_colours(main)
async def test_a_filtered_source_with_no_lines_offers_them_all_back(app):
from quart import session
from plugins.syslog.service import LineFilter
async with app.test_request_context("/plugins/syslog/sources/id-SynNAS"):
session["user_role"] = "viewer"
table = await render_plugin_template(
app, "syslog", "syslog/_lines_table.html", source=_source("SynNAS", "accepted"),
f=LineFilter(text="nope"), filtered=True, lines=[], more_url=None)
assert "No lines match these filters." in table and 'href="/plugins/syslog/sources/id-SynNAS"' in table
async def test_the_panel_names_each_source_when_there_are_several(app):
from quart import session
lines = [{**_line(0, level=4), "source": "SynNAS", "source_id": "s1"},
{**_line(1), "source": "router", "source_id": "s2"}]
async with app.test_request_context("/plugins/syslog/panel"):
session["user_role"] = "viewer"
one = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=lines[:1],
sources=[{"id": "s1", "name": "SynNAS"}])
two = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=lines,
sources=[{"id": "s1", "name": "SynNAS"}, {"id": "s2", "name": "router"}])
none = await render_plugin_template(app, "syslog", "syslog/panel.html", lines=[],
sources=[{"id": "s1", "name": "SynNAS"}])
assert "<th>Source</th>" not in one and '<span class="tone-warn">warning</span>' in one
assert "<th>Source</th>" in two and 'href="/plugins/syslog/sources/s2">router</a>' in two
assert "No lines in the last day." in none