runbook-tutorials/tests/test_ui_shell.py

198 lines
11 KiB
Python
Raw Normal View History

import http.client
import shutil
import threading
from pathlib import Path
from urllib.parse import urlencode
import pytest
import yaml
from rtut import ui, user
ROOT = Path(__file__).resolve().parent.parent
def write(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.safe_dump(data, sort_keys=False))
@pytest.fixture
def srv(tmp_path):
shutil.copy(ROOT / "runbooks/showcase-lighthouse/pack.yaml", tmp_path / "runbooks/show/pack.yaml") if (tmp_path / "runbooks/show").mkdir(parents=True) is None else None
for f in (ROOT / "attribute-sets").glob("*.yaml"):
write(tmp_path / "attribute-sets" / f.name, yaml.safe_load(f.read_text()))
(tmp_path / "project-templates").mkdir()
shutil.copy(ROOT / "project-templates/lighthouse-watch.yaml", tmp_path / "project-templates/lighthouse-watch.yaml")
write(tmp_path / "projects/watch/project.yaml", {
"spec": "project/v0.1", "id": "watch", "title": "Watch project", "description": "Old description", "environment": "rehearsal",
"attribute_sets": ["stormy-coast", "user-attributes"], "attributes": {"logdir": "/tmp/rtut-shell-x", "keeper": "Ada"},
"runbooks": [{"pack": "rtut.showcase-lighthouse"}]})
s = ui.serve([tmp_path], 0)
threading.Thread(target=s.serve_forever, daemon=True).start()
s.repo = tmp_path
yield s
s.shutdown()
def req(s, method, path, form=None):
c = http.client.HTTPConnection("127.0.0.1", s.server_address[1])
body = urlencode(form) if form is not None else None
c.request(method, path, body, {"Host": "127.0.0.1", "Content-Type": "application/x-www-form-urlencoded"})
r = c.getresponse()
return r.status, r.getheader("Location"), r.read().decode()
def post(s, path, **form):
return req(s, "POST", path, {"_t": s.app.token, **form})
# ---- shell: sidebar, user, theme ---------------------------------------------------------------------------------
PAGES = ["/", "/projects", "/runbooks", "/activity", "/attribute-sets", "/user", "/pack/0", "/project/watch",
"/template/lighthouse-watch", "/attribute-set/harbour-defaults"]
@pytest.mark.parametrize("path", PAGES)
def test_every_page_has_the_sidebar_with_sections_user_and_theme_switch(srv, path):
st, _, body = req(srv, "GET", path)
assert st == 200
assert "<aside class=side>" in body and "<a class=brand href='/'>runbook-tutorials</a>" in body
for href, label in (("/", "Home"), ("/projects", "Projects"), ("/runbooks", "Runbooks"), ("/attribute-sets", "Attribute sets"), ("/activity", "Activity")):
assert f"<a href='{href}' data-nav>{label}</a>" in body
assert "href='/user' data-nav" in body and "tester-login" in body # who you are, linking to your settings
assert "data-theme-toggle" in body and "Theme: system" in body
def test_the_theme_can_be_chosen_and_is_remembered_and_dark_styles_exist(srv):
_, _, body = req(srv, "GET", "/")
assert ":root[data-theme=dark]" in body and "prefers-color-scheme:dark" in body and ":root:not([data-theme=light])" in body
head = body.split("</head>")[0]
assert "localStorage.getItem('rtut-theme')" in head and "URLSearchParams" in head # applied before painting; ?theme=dark also works
assert "system:'light',light:'dark',dark:'system'" in body # the button cycles system, light, dark
assert "aria-current" in body # the current section is marked
def test_the_sidebar_shows_the_saved_name(srv):
post(srv, "/user", operator="bernd", display_name="Bernd W.")
_, _, body = req(srv, "GET", "/projects")
assert "<span class=avatar>B</span><span>Bernd W.</span>" in body
def test_sections_are_separate_pages(srv):
_, _, body = req(srv, "GET", "/projects")
assert "<h1>Projects</h1>" in body and "Project templates" in body and "<h2>Runbooks</h2>" not in body and "<h1>Runbooks</h1>" not in body
_, _, body = req(srv, "GET", "/runbooks")
assert "<h1>Runbooks</h1>" in body and "Showcase: a night at the lighthouse" in body and "Project templates" not in body
_, _, body = req(srv, "GET", "/activity")
assert "<h1>Latest activities</h1>" in body and "Nothing has happened yet" in body
assert post(srv, "/run/new/0", operator="bernd", p_logdir="/tmp/rtut-shell-act")[0] == 303
_, _, body = req(srv, "GET", "/activity")
assert "<h1>Latest activities" in body and "(times in UTC)" in body and "started a run" in body
_, _, body = req(srv, "GET", "/")
assert "<h1>Projects</h1>" in body and "Latest activities" in body and "<h2>Runbooks</h2>" in body # Home shows all three
# ---- attribute sets ----------------------------------------------------------------------------------------------------
def test_attribute_set_list_shows_where_they_live_extends_and_who_reuses_them(srv):
_, _, body = req(srv, "GET", "/attribute-sets")
assert "<h1>Attribute sets</h1>" in body and "harbour-defaults" in body and "stormy-coast" in body
assert "app (you)" in body and "user-attributes" in body # the app-level set is listed with the others
assert "<a href='/project/watch'>watch</a>" in body and "(extends it)" in body and "(template)" in body
assert "New attribute set" in body and "action='/attribute-sets/new'" in body
def test_attribute_set_page_resolves_layers_with_sources_and_offers_editing(srv):
_, _, body = req(srv, "GET", "/attribute-set/stormy-coast")
assert "What this set resolves to" in body and "set:harbour-defaults" in body and "set:stormy-coast" in body
assert ">modified<" in body # lamp/signal replaced a base value
assert "<b>Reused by:</b> <a href='/project/watch'>watch</a>" in body and "Other things reuse this set" in body
assert "<h2>Edit</h2>" in body and "name=n0 value='weather'" in body and "name=n3 placeholder='new_attribute'" in body # own rows + three empty ones
assert "name=extends value='harbour-defaults'" in body
assert req(srv, "GET", "/attribute-set/nope")[0] == 404
def test_the_users_own_set_is_shown_read_only_with_a_link_to_settings(srv):
_, _, body = req(srv, "GET", "/attribute-set/user-attributes")
assert "tester-login" in body and "<a href='/user'>your settings page</a>" in body and "<h2>Edit</h2>" not in body
st, _, err = post(srv, "/attribute-set/user-attributes", title="Hacked", rows="0")
assert st == 400 and "settings page" in err
def test_editing_a_set_changes_values_adds_and_removes_attributes(srv):
path = srv.repo / "attribute-sets/stormy-coast.yaml"
st, loc, _ = post(srv, "/attribute-set/stormy-coast", title="Stormy coast", extends="harbour-defaults", rows="6",
n0="weather", v0="fog", r0="", t0="", n1="signal", v1="flags", r1="", t1="", x1="1", # remove signal
n2="lamp", v2="electric", r2="optional", t2="the lamp",
n3="cape", v3="Cape Wrath", r3="", t3="") # a new attribute
assert st == 303 and loc == "/attribute-set/stormy-coast"
made = yaml.safe_load(path.read_text())
assert made["attributes"] == {"weather": "fog", "lamp": {"value": "electric", "requirement": "optional", "note": "the lamp"}, "cape": "Cape Wrath"}
_, _, body = req(srv, "GET", "/attribute-set/stormy-coast")
assert "Cape Wrath" in body and "the lamp" in body
@pytest.mark.parametrize("fields,frag", [
({"title": "ab"}, "3 characters"),
({"extends": "ghost"}, "unknown attribute set"),
({"n0": "Bad-Name", "v0": "x"}, "bad attribute name"),
({"n0": "a", "v0": "hvs.ABCDEFGHIJKLMN"}, "secret"),
({"n0": "a", "v0": "1", "n1": "a", "v1": "2"}, "twice"),
({"extends": "stormy-coast"}, "itself"),
])
def test_edit_refusals_change_nothing_and_say_why(srv, fields, frag):
path = srv.repo / "attribute-sets/stormy-coast.yaml"
before = path.read_text()
form = {"title": "Stormy coast", "extends": "harbour-defaults", "rows": "2", "n0": "weather", "v0": "storm", **fields}
st, _, body = post(srv, "/attribute-set/stormy-coast", **form)
assert st == 400 and frag in body and path.read_text() == before
def test_a_cycle_is_refused(srv):
st, _, body = post(srv, "/attribute-set/harbour-defaults", title="Harbour defaults", extends="stormy-coast", rows="1", n0="lamp", v0="x")
assert st == 400 and "circular" in body
def test_new_attribute_set_is_created_in_the_chosen_repository(srv):
st, loc, _ = post(srv, "/attribute-sets/new", set_id="team-defaults", title="Team defaults", root="0", extends="harbour-defaults")
assert st == 303 and loc == "/attribute-set/team-defaults"
made = yaml.safe_load((srv.repo / "attribute-sets/team-defaults.yaml").read_text())
assert made["extends"] == ["harbour-defaults"] and made["attributes"] == {}
assert "Team defaults" in req(srv, "GET", "/attribute-sets")[2]
st, _, err = post(srv, "/attribute-sets/new", set_id="team-defaults", title="Again", root="0")
assert st == 400 and "already exists" in err
assert post(srv, "/attribute-sets/new", set_id="x-set", title="X set", root="9")[0] == 400
def test_attribute_pages_escape_user_text_and_need_the_token(srv):
post(srv, "/attribute-set/harbour-defaults", title="Harbour defaults", extends="", rows="1", n0="lamp", v0="paraffin", t0="<script>alert(1)</script>")
_, _, body = req(srv, "GET", "/attribute-set/harbour-defaults")
assert "<script>alert(1)" not in body and "&lt;script&gt;alert(1)" in body
assert req(srv, "POST", "/attribute-set/harbour-defaults", {"title": "x"})[0] == 403
assert req(srv, "POST", "/attribute-sets/new", {"set_id": "z-set"})[0] == 403
# ---- editing a project ------------------------------------------------------------------------------------------------------
def test_a_project_can_be_renamed_and_described_from_its_page(srv):
_, _, body = req(srv, "GET", "/project/watch")
assert "Edit name, description and goal" in body and "name=title value='Watch project'" in body
st, loc, _ = post(srv, "/project/watch/edit", title="North harbour watch", description="Our harbour", goal="Both lights burn")
assert st == 303 and loc == "/project/watch"
_, _, body = req(srv, "GET", "/project/watch")
assert "<h1>North harbour watch</h1>" in body and "Our harbour" in body and "<b>Goal:</b> Both lights burn" in body
assert "North harbour watch" in req(srv, "GET", "/projects")[2]
made = yaml.safe_load((srv.repo / "projects/watch/project.yaml").read_text())
assert made["id"] == "watch" and made["attributes"]["keeper"] == "Ada" and made["runbooks"] # nothing else changed
def test_project_edit_refusals(srv):
path = srv.repo / "projects/watch/project.yaml"
before = path.read_text()
st, _, body = post(srv, "/project/watch/edit", title="ab")
assert st == 400 and "3 characters" in body
st, _, body = post(srv, "/project/watch/edit", title="Fine name", description="hvs.ABCDEFGHIJKLMN")
assert st == 400 and "secret" in body and path.read_text() == before
assert req(srv, "POST", "/project/watch/edit", {"title": "Fine name"})[0] == 403