"""The shell: which navigation entries a caller sees, and what folding persists.
`show` on a nav item decides whether a link is *drawn*. It is not a permission
check, and the tests below are careful to say so - the route behind a hidden link
must refuse the caller on its own, or hiding it is decoration.
"""
import pytest
from app.core import nav_state as nav_core
from app.core.context import Invalid, Principal, Scope
from app.endpoints import shell
from db.db_declarations import Organization
from utils.db_wrapper import db_session
PASSWORD = "correcthorsebatterystaple"
def _principal(session, role="owner", email="dev@example.com"):
from app.core import auth as auth_core
org = session.query(Organization).filter(Organization.slug == "acme").one_or_none()
if org is None:
org = Organization(name="Acme", slug="acme")
session.add(org)
session.commit()
user = auth_core.create_user(session, email, PASSWORD, "Dev")
auth_core.add_membership(session, user, org, role)
return user, auth_core.principal_for_user(session, user)
# --- fold state -------------------------------------------------------------
def test_nothing_is_folded_by_default(session):
_, principal = _principal(session)
assert nav_core.collapsed(session, principal) == []
def test_a_fold_round_trips(session):
_, principal = _principal(session)
nav_core.set_collapsed(session, principal, ["sec:account"])
assert nav_core.collapsed(session, principal) == ["sec:account"]
def test_a_bad_key_is_refused(session):
"""The column is JSON, which is otherwise an invitation to store anything on
a user row that no migration ever describes."""
_, principal = _principal(session)
for bad in ["../etc", "sec:UPPER", "cat:", "cat:abc", "cat:" + "9" * 20,
"", "sec:" + "x" * 40, 12, "prj:1"]:
with pytest.raises(Invalid):
nav_core.set_collapsed(session, principal, [bad])
def test_both_kinds_of_key_are_accepted(session):
"""A section and a category fold into the same list - one write, one column,
and no second way for the same preference to be stored."""
_, principal = _principal(session)
assert nav_core.set_collapsed(
session, principal, ["sec:account", "cat:12"]) == ["sec:account", "cat:12"]
def test_a_non_list_is_refused(session):
_, principal = _principal(session)
with pytest.raises(Invalid):
nav_core.set_collapsed(session, principal, "sec:account")
def test_too_many_keys_are_refused(session):
_, principal = _principal(session)
with pytest.raises(Invalid):
nav_core.set_collapsed(session, principal,
[f"sec:s{i}" for i in range(nav_core.MAX_KEYS + 1)])
def test_duplicates_are_collapsed(session):
_, principal = _principal(session)
assert nav_core.set_collapsed(
session, principal, ["sec:account", "sec:account"]) == ["sec:account"]
def test_folding_is_per_organisation(session):
"""A tenant with three projects and one with forty do not want the same
things collapsed, and neither may wipe the other's state."""
from app.core import auth as auth_core
user, principal = _principal(session)
second = Organization(name="Other", slug="other")
session.add(second)
session.commit()
auth_core.add_membership(session, user, second, "owner")
nav_core.set_collapsed(session, principal, ["sec:account"])
other = auth_core.principal_for_user(session, user, org_id=second.id)
assert nav_core.collapsed(session, other) == []
nav_core.set_collapsed(session, other, ["sec:workspace"])
assert nav_core.collapsed(session, principal) == ["sec:account"]
def test_a_stale_key_is_dropped_on_the_way_out(session):
"""A key written before a rename should disappear rather than be
misapplied - somebody's sidebar opens up once, which is the right failure."""
user, principal = _principal(session)
user.nav_collapsed = {str(principal.org_id): ["sec:account", "sec:GONE", 7]}
session.commit()
assert nav_core.collapsed(session, principal) == ["sec:account"]
# --- which entries are drawn ------------------------------------------------
def test_an_admin_only_entry_is_hidden_from_a_member(session):
_, member = _principal(session, role="member", email="member@example.com")
labels = [i["label"] for s in shell.sections_for(member, []) for i in s["items"]]
assert "Plan & billing" not in labels
_, owner = _principal(session, role="owner", email="owner@example.com")
labels = [i["label"] for s in shell.sections_for(owner, []) for i in s["items"]]
assert "Plan & billing" in labels
def test_an_empty_section_is_dropped(session):
"""A heading with nothing under it is worse than no heading."""
viewer = Principal(subject="v@example.com", surface="ui",
scopes=frozenset({Scope.READ}), org_id=1, user_id=1)
keys = [s["key"] for s in shell.sections_for(viewer, [])]
# "Account" holds one always-visible entry and one admin-only one, so it
# survives; a section whose every entry is hidden would not appear at all.
assert all(s["items"] for s in shell.sections_for(viewer, []))
assert keys
def test_fold_state_reaches_the_section(session):
_, principal = _principal(session)
sections = shell.sections_for(principal, ["sec:account"])
folded = {s["key"]: s["folded"] for s in sections}
assert folded["sec:account"] is True
assert folded["sec:workspace"] is False
def test_every_declared_key_is_a_valid_nav_key(session):
"""NAV and nav_state._KEY_RE have to agree, or a section can be folded and
never saved."""
import re
for section in shell.NAV:
assert re.match(nav_core._KEY_RE, section["key"]), section["key"]
# --- the page ---------------------------------------------------------------
def test_the_shell_renders_its_sections(account):
client, _, _ = account
html = client.get("/").get_data(as_text=True)
assert 'class="sidebar"' in html
assert "Workspace" in html
# Server-rendered, not fetched: the sidebar is part of the page, so a
# navigation never shows an empty rail.
assert 'href="/account"' in html
def test_the_current_page_is_marked(account):
client, _, _ = account
html = client.get("/account").get_data(as_text=True)
assert 'aria-current="page"' in html
def _section_classes(html: str) -> dict[str, str]:
"""{nav key -> the class attribute of its section div}.
Matched with a regex over the two attributes rather than a substring of the
whole tag, because the template wraps between them and a literal check would
be asserting the template's indentation instead of its meaning.
"""
import re
return {m.group(2): m.group(1) for m in re.finditer(
r'<div class="(navsec[^"]*)"\s+data-key="([^"]+)"', html)}
def test_folding_persists_across_requests(account):
from tests.conftest import csrf_for
client, _, _ = account
csrf = csrf_for(client)
r = client.put("/api/nav/collapsed", json={"collapsed": ["sec:account"]},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
classes = _section_classes(client.get("/").get_data(as_text=True))
assert "folded" in classes["sec:account"]
assert "folded" not in classes["sec:workspace"]
def test_a_fold_is_reflected_in_aria(account):
"""The twisty is the control, so its state has to be announced and not only
drawn - a rotated glyph says nothing to a screen reader."""
from tests.conftest import csrf_for
client, _, _ = account
client.put("/api/nav/collapsed", json={"collapsed": ["sec:account"]},
headers={"X-CSRF-Token": csrf_for(client)})
html = client.get("/").get_data(as_text=True)
assert 'aria-expanded="false"' in html
assert 'aria-expanded="true"' in html