import http.client import shutil import threading from pathlib import Path from urllib.parse import urlencode import pytest import yaml from rtut import project as J, run as R, run2, ui 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 psrv(tmp_path): repo = tmp_path shutil.copy(ROOT / "runbooks/showcase-lighthouse/pack.yaml", repo / "runbooks/show/pack.yaml") if (repo / "runbooks/show").mkdir(parents=True) is None else None hello = yaml.safe_load((ROOT / "runbooks/example-hello/pack.yaml").read_text()) hello["parameters"] = [{"id": "host", "label": "Host to ping", "type": "string", "pattern": "[a-zA-Z0-9.-]+"}, {"id": "count", "label": "Ping count", "type": "int", "default": 1}] write(repo / "runbooks/hello/pack.yaml", hello) v2 = yaml.safe_load((ROOT / "runbooks/showcase-lighthouse/pack.v0.2.yaml").read_text()) v2["id"] = "rtut.showcase-lighthouse-v2" write(repo / "runbooks/lh2/pack.yaml", v2) for f in (ROOT / "attribute-sets").glob("*.yaml"): write(repo / "attribute-sets" / f.name, yaml.safe_load(f.read_text())) tpl = yaml.safe_load((ROOT / "project-templates/lighthouse-watch.yaml").read_text()) tpl["runbooks"].append({"pack": "rtut.showcase-lighthouse-v2", "after": ["rtut.example-hello"]}) write(repo / "project-templates/lighthouse-watch.yaml", tpl) s = ui.serve([repo], 0) threading.Thread(target=s.serve_forever, daemon=True).start() s.repo = repo yield s s.shutdown() def req(s, method, path, form=None, host="127.0.0.1"): 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": host, "Content-Type": "application/x-www-form-urlencoded"}) r = c.getresponse() return r.status, r.getheader("Location"), r.read().decode() def make_project(s, pid="harbour", host=None): t = s.app.token form = {"_t": t, "project_id": pid, "environment": "rehearsal", "a_keeper": "Ada", "a_logdir": f"/tmp/rtut-ui-{pid}", "title": "Harbour watch", "description": "Our own harbour, two lights.", "goal": "Both lights keep watch tonight"} if host: form["a_host"] = host return req(s, "POST", "/template/lighthouse-watch/new", form) def test_index_has_projects_templates_runbooks_and_maturity(psrv): _, _, body = req(psrv, "GET", "/") assert "

Projects

" in body and "Project templates" in body and "

Runbooks

" in body assert "/template/lighthouse-watch" in body assert ">draft<" in body # maturity pill on every native runbook def test_template_page_shows_attributes_with_requirement_and_notes(psrv): st, _, body = req(psrv, "GET", "/template/lighthouse-watch") assert st == 200 and "A watch at a lighthouse" in body assert "name='a_keeper'" in body and "Who keeps this light" in body and ">required<" in body assert "name='a_weather'" not in body # weather is set per target, so it is not a template question assert "name=title" in body and "name=description" in body and "name=goal" in body # the project's own identity assert "Create project" in body and "" in body and "" in body assert req(psrv, "GET", "/template/nope")[0] == 404 def test_create_project_from_template_and_view_it(psrv): st, loc, _ = make_project(psrv) assert st == 303 and loc == "/project/harbour" made = yaml.safe_load((psrv.repo / "projects/harbour/project.yaml").read_text()) assert made["template"] == "lighthouse-watch" and made["environment"] == "rehearsal" and made["attributes"]["keeper"] == "Ada" _, _, body = req(psrv, "GET", "/project/harbour") assert "

Harbour watch

" in body and "Our own harbour, two lights." in body and "Both lights keep watch tonight" in body assert "from template lighthouse-watch" in body # origin is shown, as a link assert "A watch at a lighthouse" not in body # the template's title... assert "The night watch is played through" not in body # ...and its description are not assert ">planned<" in body and ">rehearsal<" in body assert "target:north-light" in body # provenance of the fog weather assert "Still needed" in body and "name='a_host'" in body # hello needs a host assert "waiting for rtut.example-hello" in body # the v0.2 member waits for its predecessor def test_needed_attributes_can_be_filled_in_and_are_validated(psrv): make_project(psrv) t = psrv.app.token st, _, body = req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "bad host;rm"}) assert st == 400 and "host" in body and "Still needed" in body assert req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "hvs.ABCDEFGHIJKLMN"})[0] == 400 assert req(psrv, "POST", "/project/harbour/attributes", {"_t": t, "a_host": "example.org"})[0] == 303 _, _, body = req(psrv, "GET", "/project/harbour") assert "Still needed" not in body and "example.org" in body def test_start_a_run_from_a_project_uses_its_attributes_and_links_back(psrv): make_project(psrv, host="example.org") t = psrv.app.token _, _, body = req(psrv, "GET", "/project/harbour") assert "waiting for rtut.showcase-lighthouse" in body # hello waits for the showcase assert body.count("Start run") == 1 # only the member that may start st, loc, _ = req(psrv, "POST", "/project/harbour/run", {"_t": t, "pack": "rtut.showcase-lighthouse", "target": "north-light", "operator": "bernd"}) assert st == 303 and loc.startswith("/run/") and loc.endswith("#current") _, _, run_page = req(psrv, "GET", loc.split("#")[0]) assert "project harbour @north-light ยท rehearsal" in run_page _, _, body = req(psrv, "GET", "/project/harbour") assert ">applying<" in body and "latest run" in body st, loc2, _ = req(psrv, "POST", "/project/harbour/run", {"_t": t, "pack": "rtut.showcase-lighthouse-v2", "operator": "bernd"}) assert st == 303 # a v0.2 member can be started too, in either mode assert "Guided flow" in req(psrv, "GET", loc2.split("#")[0])[2] def test_attribute_table_shows_state_source_modified_and_problems(psrv): make_project(psrv) reg_file = psrv.repo / "projects/harbour/project.yaml" d = yaml.safe_load(reg_file.read_text()); d["attributes"]["logdir"] = "/etc" reg_file.write_text(yaml.safe_dump(d, sort_keys=False)) _, _, body = req(psrv, "GET", "/project/harbour") assert "Source" in body and "set:harbour-defaults" in body assert ">err<" in body and "/etc" in body # invalid value is shown as err, not hidden assert ">nil<" in body # hello's host has no value yet def test_review_form_records_a_review_and_raises_maturity(psrv): t = psrv.app.token i = psrv.app.pack_index("rtut.showcase-lighthouse") _, _, body = req(psrv, "GET", f"/pack/{i}") assert "Maturity" in body and "Reviewed: no" in body and "Record my review" in body st, loc, _ = req(psrv, "POST", f"/pack/{i}/review", {"_t": t, "by": "alice", "note": "read it"}) assert st == 303 and loc.endswith("#maturity") _, _, body = req(psrv, "GET", f"/pack/{i}") assert "Reviewed: yes" in body and "alice" in body and "current revision" in body and ">reviewed<" in body st, _, body = req(psrv, "POST", f"/pack/{i}/review", {"_t": t, "by": "two words"}) assert st == 400 and "one word" in body def test_idempotency_shown_per_spec_version(psrv): i = psrv.app.pack_index("rtut.showcase-lighthouse") _, _, body = req(psrv, "GET", f"/pack/{i}") assert "Idempotency: not declared (v0.1 packs cannot declare it)" in body def test_csrf_host_and_escaping(psrv): assert req(psrv, "POST", "/template/lighthouse-watch/new", {"project_id": "x", "environment": "rehearsal"})[0] == 403 assert req(psrv, "POST", "/project/harbour/run", {"pack": "x"})[0] == 403 assert req(psrv, "GET", "/project/harbour", host="evil.example")[0] == 403 tpl = psrv.repo / "project-templates/lighthouse-watch.yaml" d = yaml.safe_load(tpl.read_text()); d["title"] = " watch" tpl.write_text(yaml.safe_dump(d, sort_keys=False)) _, _, body = req(psrv, "GET", "/") assert "