#5818 "First-run setup: Restore from a backup, by upload": beside creating the first admin, setup offers "Or restore from a backup". Only while no user exists; the upload is checked (a newer backup or one whose key can't open its secrets is refused and not kept), then restored with no safety backup (nothing to keep), and Steward restarts into the backup's users. #5819 "Docs and CI": backups.md leads with restoring from Settings → Backups and from setup; `steward restore --in-place` does the same-schema restore from the shell, and CI's image check now uses it with a real pg_restore: a changed setting comes back and the audit log stays. Also exempts errors/restoring.html from the breadcrumb guard, as with the other whole-app error pages: it replaces every page while a restore runs, so there is nowhere to trail back to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
5.0 KiB
Python
107 lines
5.0 KiB
Python
"""Every page below a top-level section shows its trail back up (rule 134).
|
|
|
|
The breadcrumb block is empty by default, so a nested page that forgets it
|
|
ships with no way back up and nothing says so. Every full page (one that
|
|
extends base.html) must fill the block, unless it is a section's own top
|
|
page, a logged-out page or an error page: those are named below, so adding
|
|
one is a decision someone makes in this file.
|
|
"""
|
|
import re
|
|
|
|
from tests._templates import REPO, template_files
|
|
|
|
TOP_LEVEL = {
|
|
"steward/templates/dashboard/index.html",
|
|
"steward/templates/hosts/list.html",
|
|
"steward/templates/monitors/index.html",
|
|
"steward/templates/alerts/list.html",
|
|
"steward/templates/ansible/index.html",
|
|
"steward/templates/audit/list.html",
|
|
"steward/templates/ai_access/index.html",
|
|
"steward/templates/account/index.html",
|
|
"plugins/docker/templates/docker/index.html",
|
|
"plugins/snmp/templates/snmp/index.html",
|
|
"plugins/prometheus/templates/prometheus/index.html",
|
|
"plugins/traefik/templates/traefik/index.html",
|
|
"plugins/unifi/templates/unifi/index.html",
|
|
# Logged out, or no page to go back up from.
|
|
"steward/templates/auth/login.html",
|
|
"steward/templates/auth/setup.html",
|
|
"steward/templates/auth/invite.html",
|
|
"steward/templates/auth/forgot.html",
|
|
"steward/templates/auth/reset.html",
|
|
"steward/templates/errors/404.html",
|
|
"steward/templates/errors/500.html",
|
|
"steward/templates/errors/database_unavailable.html",
|
|
"steward/templates/errors/restoring.html",
|
|
}
|
|
|
|
_EXTENDS_BASE = re.compile(r"""\{%-?\s*extends\s+["']base\.html["']""")
|
|
_BLOCK = re.compile(r"\{%-?\s*block\s+breadcrumb\s*-?%\}(.*?)\{%-?\s*endblock", re.S)
|
|
|
|
|
|
def needs_crumbs_and_lacks_them(text: str) -> bool:
|
|
block = _BLOCK.search(text)
|
|
return bool(_EXTENDS_BASE.search(text)) and not (block and "crumbs(" in block.group(1))
|
|
|
|
|
|
def test_the_guard_can_fail():
|
|
assert needs_crumbs_and_lacks_them('{% extends "base.html" %}{% block content %}x{% endblock %}')
|
|
assert needs_crumbs_and_lacks_them('{% extends "base.html" %}{% block breadcrumb %}{% endblock %}')
|
|
assert not needs_crumbs_and_lacks_them(
|
|
'{% extends "base.html" %}{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), ("x", "")]) }}{% endblock %}')
|
|
assert not needs_crumbs_and_lacks_them("<tr><td>a fragment</td></tr>")
|
|
|
|
|
|
def test_every_nested_page_has_a_trail():
|
|
pages = {str(p.relative_to(REPO)): p for p in template_files()}
|
|
assert TOP_LEVEL <= set(pages), sorted(TOP_LEVEL - set(pages)) # a renamed page leaves no stale entry
|
|
missing = sorted(name for name, p in pages.items()
|
|
if name not in TOP_LEVEL and needs_crumbs_and_lacks_them(p.read_text()))
|
|
assert not missing, f"add {{% block breadcrumb %}}{{{{ crumbs([...]) }}}} (rule 134): {missing}"
|
|
|
|
|
|
# A section's tabs go in base.html's section_tabs block, above the trail, so the
|
|
# strip holds still whether or not the page has a breadcrumb (#5214). Drawn in
|
|
# the content block instead, they sit under the trail and jump down a line on
|
|
# every nested page.
|
|
_TABS_BLOCK = re.compile(r"\{%-?\s*block\s+section_tabs\s*-?%\}(.*?)\{%-?\s*endblock", re.S)
|
|
_TABS_CALL = re.compile(r"\{\{-?\s*\w*_tabs\(")
|
|
|
|
|
|
def tabs_outside_their_block(text: str) -> bool:
|
|
calls = len(_TABS_CALL.findall(text))
|
|
inside = sum(len(_TABS_CALL.findall(b)) for b in _TABS_BLOCK.findall(text))
|
|
return calls > inside
|
|
|
|
|
|
def test_the_tabs_guard_can_fail():
|
|
assert tabs_outside_their_block('{% block content %}{{ ansible_tabs("runs") }}{% endblock %}')
|
|
assert not tabs_outside_their_block('{% block section_tabs %}{{ ansible_tabs("runs") }}{% endblock %}')
|
|
|
|
|
|
def test_section_tabs_sit_above_the_trail():
|
|
base = (REPO / "steward/templates/base.html").read_text()
|
|
assert base.index("{% block section_tabs %}") < base.index("{% block breadcrumb %}")
|
|
pages = [p for p in template_files() if _EXTENDS_BASE.search(p.read_text())]
|
|
tabbed = [p for p in pages if _TABS_CALL.search(p.read_text())]
|
|
assert len(tabbed) >= 18, [str(p) for p in tabbed] # the ten Ansible pages and eight Settings pages
|
|
wrong = sorted(str(p.relative_to(REPO)) for p in tabbed if tabs_outside_their_block(p.read_text()))
|
|
assert not wrong, f"move the tabs into {{% block section_tabs %}}: {wrong}"
|
|
|
|
|
|
# A tab strip pulled in with {% include %} lands wherever the include sits, which
|
|
# is how Settings drew its tabs under the trail unnoticed (#5377). Tabs come only
|
|
# from a *_tabs() macro call, which the guard above places.
|
|
_TABS_INCLUDE = re.compile(r"\{%-?\s*include\s+[\"'][^\"']*_tabs\.html")
|
|
|
|
|
|
def test_the_include_guard_can_fail():
|
|
assert _TABS_INCLUDE.search('{% include "settings/_tabs.html" %}')
|
|
assert not _TABS_INCLUDE.search('{% from "settings/_tabs.html" import settings_tabs %}')
|
|
|
|
|
|
def test_no_page_includes_a_tab_strip():
|
|
included = sorted(str(p.relative_to(REPO)) for p in template_files() if _TABS_INCLUDE.search(p.read_text()))
|
|
assert not included, f"call the section's *_tabs() macro in {{% block section_tabs %}}: {included}"
|