Files
FabledSteward/tests/integration/test_hosts_list.py
T
bvandeusenandClaude Opus 5.5 2990113e10
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 52s
CI / unit (push) Successful in 1m17s
CI / publish (push) Skipped
feat(hosts): the Hosts list searches, filters by group and status, sorts by column, and shows each host's groups (#5294)
#5294 "Hosts list: search, group and status filters; sortable columns; a Groups column".
Ansible inventory groups are Steward's grouping system (operator's ruling): a host's
groups are its linked target's, and membership overlaps, so the list filters by group
rather than sectioning by it.

- Filter bar: search on name or address, a group (or "No group"), a status (Down,
  Agent silent, Up, No monitors). The table swaps in alone and the URL keeps the filters.
- Name, Status, Latency, 30d uptime, CPU and Memory headers sort (sort_th in _macros);
  problems-first stays the default, and a host with no value goes last either way.
- A Groups column of chips, each filtering to its group; shown when any group exists.
- A swap is keyed on HX-Target, so Back to a pushed URL still gets the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:53:03 -04:00

86 lines
3.9 KiB
Python

"""Integration: the Hosts list's search, group and status filters and its
sort (#5294) against real Postgres. Groups are the linked Ansible target's;
the filter bar's swap (HX-Target: hosts-table) gets the table alone, and a
history restore gets the whole page."""
from __future__ import annotations
import asyncio
import os
import time
import uuid
import pytest
pytestmark = pytest.mark.integration
@pytest.fixture
def app():
if not os.environ.get("STEWARD_DATABASE_URL"):
pytest.skip("needs Postgres")
from steward.app import create_app
return create_app(testing=False)
def test_the_hosts_list_filters_by_search_group_and_status(app):
from steward.models.ansible_inventory import AnsibleGroup, AnsibleTarget
from steward.models.hosts import Host
from steward.models.users import User, UserRole
async def _go():
tag = uuid.uuid4().hex[:8]
uid = str(uuid.uuid4())
async with app.db_sessionmaker() as s:
async with s.begin():
s.add(User(id=uid, username=f"hl-{tag}", email=f"hl-{tag}@test",
password_hash="x", role=UserRole.viewer, is_active=True))
swarm = Host(name=f"swarm-{tag}", address="10.9.0.1")
lone = Host(name=f"lone-{tag}", address="10.9.0.2")
s.add_all([swarm, lone])
await s.flush()
group = AnsibleGroup(name=f"Swarm {tag}")
target = AnsibleTarget(name=f"t-{tag}", address="10.9.0.1", host_id=swarm.id)
target.groups = [group]
s.add_all([group, target])
client = app.test_client()
async with client.session_transaction() as sess:
sess.update(user_id=uid, user_role="viewer", username=f"hl-{tag}", login_at=time.time())
page = await (await client.get(f"/hosts/?q={tag}")).get_data(as_text=True)
assert 'class="filter-bar"' in page and f"swarm-{tag}" in page and f"lone-{tag}" in page
assert f'<option value="Swarm {tag}"' in page # the group filter lists it
assert f'>Swarm {tag}</a>' in page # and swarm's row shows it as a chip
swap = {"HX-Request": "true", "HX-Target": "hosts-table"}
resp = await client.get("/hosts/", query_string={"q": tag, "group": f"Swarm {tag}"}, headers=swap)
table = await resp.get_data(as_text=True)
assert resp.headers["Vary"] == "HX-Request"
assert 'class="filter-bar"' not in table and "<html" not in table
assert f"swarm-{tag}" in table and f"lone-{tag}" not in table
table = await (await client.get("/hosts/", query_string={"q": tag, "group": "__none__"},
headers=swap)).get_data(as_text=True)
assert f"lone-{tag}" in table and f"swarm-{tag}" not in table
# Neither host has a monitor: "No monitors" has both, "Down" neither.
table = await (await client.get("/hosts/", query_string={"q": tag, "state": "none"},
headers=swap)).get_data(as_text=True)
assert f"swarm-{tag}" in table and f"lone-{tag}" in table
table = await (await client.get("/hosts/", query_string={"q": tag, "state": "down"},
headers=swap)).get_data(as_text=True)
assert "No hosts match these filters." in table
# Sorted by name, descending: lone after swarm.
page = await (await client.get("/hosts/", query_string={"q": tag, "sort": "name", "dir": "desc"})
).get_data(as_text=True)
assert page.index(f"swarm-{tag}") < page.index(f"lone-{tag}")
assert 'aria-sort="descending"' in page
# Back to a pushed URL: HTMX asks again, and gets the page.
restore = {"HX-Request": "true", "HX-History-Restore-Request": "true"}
page = await (await client.get(f"/hosts/?q={tag}", headers=restore)).get_data(as_text=True)
assert 'class="filter-bar"' in page
asyncio.run(_go())