"""The ProPresenter adapter, against a mock of the real API. The mock behaves the way the church's ProPresenter 21.3 was observed to behave, including the part that matters most: an unknown token name still returns 204. """ import json import threading from http.server import BaseHTTPRequestHandler, HTTPServer import pytest from parent_call.propresenter import ProPresenter, ProPresenterError UUID = "7D044452-B5A1-49F5-AC9C-1FD53A9A7353" MESSAGES = [ {"id": {"name": "Gbreaker", "index": 0, "uuid": "FE2A"}, "tokens": []}, { "id": {"name": "Parent Caller", "index": 2, "uuid": UUID}, "message": "{Name}'s parent please go to the {Room}", "tokens": [ {"name": "Name", "text": {"text": ""}}, {"name": "Room", "text": {"text": ""}}, ], }, ] class Fake(BaseHTTPRequestHandler): messages = MESSAGES triggers = [] clears = [] trigger_status = 204 messages_status = 200 def log_message(self, *args): pass def _send(self, status, body=None): self.send_response(status) if body is None: self.send_header("Content-Length", "0") self.end_headers() return raw = json.dumps(body).encode() self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self): if self.path == "/version": return self._send(200, {"host_description": "ProPresenter 21.3", "api_version": "v1"}) if self.path == "/v1/messages": if Fake.messages_status != 200: return self._send(Fake.messages_status) return self._send(200, Fake.messages) if self.path.endswith("/clear"): Fake.clears.append(self.path) return self._send(204) return self._send(404) def do_POST(self): if not self.path.endswith("/trigger"): return self._send(404) if UUID not in self.path: return self._send(404) length = int(self.headers.get("Content-Length", 0)) Fake.triggers.append(json.loads(self.rfile.read(length) or b"[]")) # Exactly as the real one behaves: it does not check token names. return self._send(Fake.trigger_status) @pytest.fixture def server(): Fake.messages = [dict(m) for m in MESSAGES] Fake.triggers = [] Fake.clears = [] Fake.trigger_status = 204 Fake.messages_status = 200 httpd = HTTPServer(("127.0.0.1", 0), Fake) threading.Thread(target=httpd.serve_forever, daemon=True).start() yield f"http://127.0.0.1:{httpd.server_port}" httpd.shutdown() def adapter(base, child="Name", room="Room", message="Parent Caller"): return ProPresenter(base, message, child, room, timeout=3) def test_finds_the_message_and_its_tokens(server): message = adapter(server).check() assert message.uuid == UUID assert message.tokens == ("Name", "Room") def test_trigger_sends_both_tokens(server): adapter(server).trigger("Emma", "Nursery") assert Fake.triggers == [[ {"name": "Name", "text": {"text": "Emma"}}, {"name": "Room", "text": {"text": "Nursery"}}, ]] def test_both_tokens_always_go_even_though_only_one_changed(server): """ProPresenter keeps the previous value of any token left out, so a partial trigger would show half of the call before it.""" pp = adapter(server) pp.trigger("Emma", "Nursery") pp.trigger("Noah", "Nursery") assert [t["text"]["text"] for t in Fake.triggers[1]] == ["Noah", "Nursery"] def test_a_wrong_token_name_is_refused_before_anything_is_triggered(server): """The whole reason this adapter reads the message first. The real API returns 204 for an unknown token and puts the previous child's name on screen. Nothing may be sent unless the tokens exist. """ with pytest.raises(ProPresenterError) as raised: adapter(server, child="child", room="room").trigger("Emma", "Nursery") assert "no token" in str(raised.value) assert Fake.triggers == [] def test_a_missing_message_is_an_error_not_a_success(server): with pytest.raises(ProPresenterError) as raised: adapter(server, message="Parent Call").trigger("Emma", "Nursery") assert "no message named" in str(raised.value) assert Fake.triggers == [] def test_an_unreachable_propresenter_raises(server): pp = ProPresenter("http://127.0.0.1:9", "Parent Caller", "Name", "Room", timeout=1) with pytest.raises(ProPresenterError) as raised: pp.trigger("Emma", "Nursery") assert "cannot reach" in str(raised.value) def test_a_non_2xx_trigger_is_an_error(server): Fake.trigger_status = 500 with pytest.raises(ProPresenterError) as raised: adapter(server).trigger("Emma", "Nursery") assert "500" in str(raised.value) def test_a_renamed_message_is_noticed_rather_than_cached_forever(server): pp = adapter(server) pp.trigger("Emma", "Nursery") Fake.messages = [{"id": {"name": "Renamed", "uuid": UUID}, "tokens": []}] with pytest.raises(ProPresenterError): pp.trigger("Noah", "Nursery") def test_clear_uses_the_resolved_uuid(server): pp = adapter(server) pp.trigger("Emma", "Nursery") pp.clear() assert Fake.clears == [f"/v1/message/{UUID}/clear"] def test_no_address_configured_is_an_error(server): with pytest.raises(ProPresenterError): ProPresenter("", "Parent Caller", "Name", "Room").trigger("Emma", "Nursery")