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}"}
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 "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 "A watch at a lighthouse" in body and "from template lighthouse-watch" in body
assert ">planned<" in body and ">rehearsal<" in body
assert "The night watch is played through" in body # the goal
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 "cannot be run in this UI yet" in body # the v0.2 member
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 # the engine accepts it; the UI just cannot walk v0.2 runs yet
assert "cannot walk v0.2 runs yet" 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 "