"""What a classroom is allowed to send. This is the only place classroom input is read, and everything that reaches a sanctuary screen comes through it. """ import json import pytest from parent_call.config import Settings from parent_call.validation import Invalid, parse NOW = 1_780_000_000.0 @pytest.fixture def settings(): return Settings(max_child_length=40, max_room_length=32, request_ttl=180, max_clock_skew=120) def body(**fields): payload = {"id": "nursery-1", "room": "nursery", "child": "Emma", "created_at": NOW} payload.update(fields) return json.dumps(payload) def test_a_good_request(settings): request = parse(body(), NOW, settings) assert (request.id, request.room, request.child) == ("nursery-1", "nursery", "Emma") def test_only_the_four_fields_survive(settings): """A payload cannot smuggle anything through to ProPresenter.""" request = parse( body(message="{Name} OWNS YOU", theme="evil", propresenter="anything"), NOW, settings, ) assert set(vars(request)) == {"id", "room", "child", "created_at"} @pytest.mark.parametrize("payload, reason", [ (b"\xff\xfe", "payload is not utf-8"), ("{{{", "payload is not json"), ("[1,2,3]", "payload is not an object"), (json.dumps({"room": "nursery", "child": "E", "created_at": NOW}), "id is missing"), (json.dumps({"id": 7, "room": "n", "child": "E", "created_at": NOW}), "id is missing"), ]) def test_unusable_payloads(settings, payload, reason): with pytest.raises(Invalid) as raised: parse(payload, NOW, settings) assert raised.value.reason == reason @pytest.mark.parametrize("field, value", [ ("id", "has/slash"), ("id", "has#hash"), ("id", "has+plus"), ("id", ""), ("id", "x" * 200), ]) def test_ids_that_could_reshape_a_topic(settings, field, value): with pytest.raises(Invalid): parse(body(**{field: value}), NOW, settings) def test_the_id_is_reported_back_so_the_classroom_can_be_answered(settings): with pytest.raises(Invalid) as raised: parse(body(child=""), NOW, settings) assert raised.value.request_id == "nursery-1" assert raised.value.room == "nursery" @pytest.mark.parametrize("room", ["a/b", "a+b", "a#b", "", "x" * 99, " "]) def test_rooms_that_are_not_rooms(settings, room): with pytest.raises(Invalid): parse(body(room=room), NOW, settings) def test_room_is_lowercased(settings): assert parse(body(room="Nursery"), NOW, settings).room == "nursery" @pytest.mark.parametrize("child", ["", " ", "x" * 41]) def test_child_names_that_are_refused(settings, child): with pytest.raises(Invalid): parse(body(child=child), NOW, settings) @pytest.mark.parametrize("child, expected", [ ("Emma\nPASTOR SAYS EVACUATE", "Emma PASTOR SAYS EVACUATE"), ("Emma​Rose", "Emma Rose"), ("Emma‮", "Emma"), (" Emma Rose ", "Emma Rose"), ("Zoé", "Zoé"), ]) def test_names_are_made_safe_to_put_on_a_screen(settings, child, expected): """Nothing invisible, and nothing that can push text around a screen, survives into a sanctuary display.""" assert parse(body(child=child), NOW, settings).child == expected def test_an_old_timestamp_is_still_a_valid_shape(settings): """How old is too old is policy, and the state machine decides it — it has to make the same judgement again when a late approval arrives. See test_state.test_a_stale_request_is_never_accepted.""" assert parse(body(created_at=NOW - 9999), NOW, settings).child == "Emma" def test_requests_from_the_future(settings): with pytest.raises(Invalid) as raised: parse(body(created_at=NOW + 3600), NOW, settings) assert "future" in raised.value.reason def test_a_slightly_fast_classroom_clock_is_tolerated(settings): assert parse(body(created_at=NOW + 60), NOW, settings).child == "Emma" @pytest.mark.parametrize("created_at", [None, "soon", True, {}]) def test_timestamps_that_are_not_timestamps(settings, created_at): with pytest.raises(Invalid) as raised: parse(body(created_at=created_at), NOW, settings) assert raised.value.reason == "created_at is missing"