import http.client import json import shutil import subprocess import threading import uuid from pathlib import Path from urllib.parse import urlencode import pytest from rtut import run as R1, run2 as R, ui ROOT = Path(__file__).resolve().parent.parent _scratch = [] @pytest.fixture def srv(tmp_path): for name, src in (("v1", "showcase-lighthouse"), ("v2", "showcase-lighthouse-v2")): d = tmp_path / "runbooks" / name; d.mkdir(parents=True) shutil.copy(ROOT / f"runbooks/{src}/pack.yaml", d / "pack.yaml") s = ui.serve([tmp_path], 0) threading.Thread(target=s.serve_forever, daemon=True).start() s.repo = tmp_path yield s s.shutdown() while _scratch: shutil.rmtree(_scratch.pop(), ignore_errors=True) 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}) def start(s, pack_id): i = s.app.pack_index(pack_id) logdir = f"/tmp/rtut-lock-{uuid.uuid4().hex[:8]}" _scratch.append(logdir) st, loc, _ = post(s, f"/run/new/{i}", operator="bernd", p_logdir=logdir) assert st == 303 return loc.split("#")[0], i def run_of(s, base): return R1.load(s.app.packs[int(base.split("/")[2])], base.rsplit("/", 1)[1]) # ---- Home headlines are shortcuts --------------------------------------------------------------------------- def test_home_headlines_link_to_the_sidebar_sections_and_the_section_pages_do_not(srv): _, _, home = req(srv, "GET", "/") assert "

Projects

" in home assert "

Project templates

" in home assert "Latest activities" in home and "

Runbooks

" in home _, _, projects = req(srv, "GET", "/projects") assert "

Projects

" in projects and "class=hl" not in projects # on its own page the headline is just a headline assert "

Runbooks

" in req(srv, "GET", "/runbooks")[2] assert "

Latest activities" in req(srv, "GET", "/activity")[2] # ---- no "back to the runbook list" ------------------------------------------------------------------------------- def test_there_is_no_back_to_the_runbook_list_link_in_either_run_view(srv): for pack in ("rtut.showcase-lighthouse", "rtut.showcase-lighthouse-v2"): base, i = start(srv, pack) assert post(srv, f"{base}/feedback", rating="good", comment="fine")[0] == 303 assert "Back to the runbook list" not in req(srv, "GET", base)[2] assert post(srv, f"{base}/finalize", outcome="abandoned")[0] == 303 assert "Back to the runbook list" not in req(srv, "GET", base)[2] # ---- closed runs are locked ------------------------------------------------------------------------------------------ @pytest.mark.parametrize("pack", ["rtut.showcase-lighthouse", "rtut.showcase-lighthouse-v2"]) def test_a_closed_run_shows_its_feedback_read_only_without_a_change_option(srv, pack): base, _ = start(srv, pack) assert post(srv, f"{base}/feedback", rating="neutral", comment="a bit long")[0] == 303 assert "Change my feedback" in req(srv, "GET", base)[2] # while open it can still be changed assert post(srv, f"{base}/finalize", outcome="abandoned")[0] == 303 body = req(srv, "GET", base)[2] assert "Change my feedback" not in body and f"action='{base}/feedback'" not in body assert "

Feedback

" in body and "a bit long" in body and "closed and locked, so its feedback can no longer be changed" in body st, _, err = post(srv, f"{base}/feedback", rating="good", comment="changed my mind") assert st == 400 and "closed and locked" in err assert run_of(srv, base).data["feedback"][0]["comment"] == "a bit long" # nothing reached the record @pytest.mark.parametrize("pack", ["rtut.showcase-lighthouse", "rtut.showcase-lighthouse-v2"]) def test_a_closed_run_without_feedback_shows_no_feedback_section(srv, pack): base, _ = start(srv, pack) assert post(srv, f"{base}/finalize", outcome="abandoned")[0] == 303 body = req(srv, "GET", base)[2] assert "

Feedback

" not in body and "

How was this runbook?

" not in body and "Change my feedback" not in body @pytest.mark.parametrize("pack", ["rtut.showcase-lighthouse", "rtut.showcase-lighthouse-v2"]) def test_the_closing_form_takes_optional_feedback_so_abandoned_runs_are_not_silent(srv, pack): base, _ = start(srv, pack) body = req(srv, "GET", base)[2] assert "How was this runbook? (optional; it cannot be added after closing)" in body and "name=fb_comment" in body assert post(srv, f"{base}/finalize", outcome="abandoned", rating="poor", fb_comment="step 1 was unclear")[0] == 303 body = req(srv, "GET", base)[2] assert "

Feedback

" in body and "step 1 was unclear" in body assert run_of(srv, base).data["feedback"][0]["rating"] == "poor" def test_the_closing_form_does_not_ask_again_when_feedback_was_already_given(srv): base, _ = start(srv, "rtut.showcase-lighthouse-v2") post(srv, f"{base}/feedback", rating="good", comment="ok") assert "name=fb_comment" not in req(srv, "GET", base)[2] def test_a_bad_rating_on_closing_refuses_the_whole_close(srv): base, _ = start(srv, "rtut.showcase-lighthouse-v2") st, _, err = post(srv, f"{base}/finalize", outcome="abandoned", rating="great") assert st == 400 and "rating must be" in err assert not run_of(srv, base).data.get("outcome") # still open @pytest.mark.parametrize("pack", ["rtut.showcase-lighthouse", "rtut.showcase-lighthouse-v2"]) def test_a_closed_run_says_it_is_locked_and_refuses_every_change(srv, pack): base, _ = start(srv, pack) post(srv, f"{base}/finalize", outcome="abandoned") body = req(srv, "GET", base)[2] assert "closed and locked" in body and "its record is sealed and cannot be changed" in body first = "read-the-sign" for path, form in ((f"{base}/{first}/start", {}), (f"{base}/mode", {"to": "auto-confirm"}), (f"{base}/data", {"d_weather": "clear"}), (f"{base}/{first}/capture", {"output": "x"}), (f"{base}/finalize", {"outcome": "succeeded"})): st, _, err = post(srv, path, **form) assert st in (400,) and ("closed" in err or "finished" in err or "already" in err), path assert run_of(srv, base).data["outcome"] == "abandoned" def test_the_finished_comment_is_shown_as_a_comment_not_as_a_stray_dash(srv): base, _ = start(srv, "rtut.showcase-lighthouse-v2") post(srv, f"{base}/finalize", outcome="abandoned", note="gave up on step two") body = req(srv, "GET", base)[2] assert "

Comment: gave up on step two

" in body and "

- " not in body def test_the_record_file_is_read_only_after_closing_and_tampering_is_called_out(srv): base, _ = start(srv, "rtut.showcase-lighthouse-v2") post(srv, f"{base}/finalize", outcome="abandoned") r = run_of(srv, base) assert r.path.stat().st_mode & 0o222 == 0 and R1.seal_state(r) == "sealed" assert "changed outside the app" not in req(srv, "GET", base)[2] r.path.chmod(0o644) data = json.loads(r.path.read_text()); data["operator"] = "forged"; r.path.write_text(json.dumps(data)) body = req(srv, "GET", base)[2] assert "changed outside the app after it was sealed" in body and "no longer counts towards maturity" in body