# retoor import json import pytest from devplacepy.services.nodes import frames from devplacepy.services.nodes.frames import FrameError, decode, encode from devplacepy.services.nodes.spec import ( CLIENT_TO_SERVER, CLOSE_FRAME_TOO_LARGE, CLOSE_PROTOCOL, SERVER_TO_CLIENT, SPEC, ) HELLO = { "type": "hello", "protocol": 1, "client_version": "0.1.0", "kind": "native", "platform": "linux/amd64", "modes": ["tunnel"], } def reason(raw, direction=CLIENT_TO_SERVER): with pytest.raises(FrameError) as info: decode(raw, direction) return info.value def test_a_valid_frame_round_trips(): assert decode(json.dumps(HELLO)) == HELLO assert decode(json.dumps({"type": "pong"})) == {"type": "pong"} @pytest.mark.parametrize("raw", ["", "nope", "[]", "1", "null", '"x"']) def test_non_object_json_is_rejected(raw): assert reason(raw).code == "bad_json" def test_unknown_and_wrong_direction_types_are_rejected(): assert reason(json.dumps({"type": "teleport"})).code == "unknown_frame" assert reason(json.dumps({"type": "welcome"})).code == "unknown_frame" assert reason(json.dumps({"type": 5})).code == "unknown_frame" assert reason(json.dumps({})).code == "unknown_frame" def test_missing_unexpected_and_mistyped_fields_are_rejected(): assert reason(json.dumps({**HELLO, "protocol": None})).code == "bad_field_type" missing = {key: value for key, value in HELLO.items() if key != "platform"} assert reason(json.dumps(missing)).code == "missing_field" assert reason(json.dumps({**HELLO, "extra": 1})).code == "unexpected_field" assert reason(json.dumps({**HELLO, "protocol": "1"})).code == "bad_field_type" assert reason(json.dumps({**HELLO, "modes": "tunnel"})).code == "bad_field_type" def test_a_bool_is_not_an_int(): assert reason(json.dumps({**HELLO, "protocol": True})).code == "bad_field_type" usage = {"type": "usage", "bytes_up": False, "bytes_down": 1} assert reason(json.dumps(usage)).code == "bad_field_type" def test_optional_fields_may_be_absent_but_not_wrong(): with_limits = {**HELLO, "limits": {"a": 1}} assert decode(json.dumps(with_limits))["limits"] == {"a": 1} assert reason(json.dumps({**HELLO, "limits": []})).code == "bad_field_type" def test_the_size_cap_is_enforced_before_and_after_parsing(): huge = "x" * (frames.ceiling(CLIENT_TO_SERVER) + 1) error = reason(huge) assert error.code == "frame_too_large" assert error.close_code == CLOSE_FRAME_TOO_LARGE padded = {**HELLO, "platform": "x" * SPEC.frame("hello", CLIENT_TO_SERVER).max_bytes} assert reason(json.dumps(padded)).code == "frame_too_large" assert reason(b"{" + b" " * frames.ceiling(CLIENT_TO_SERVER)).close_code == ( CLOSE_FRAME_TOO_LARGE ) def test_the_big_frame_may_exceed_the_small_cap(): body = "A" * (SPEC.frame("hello", CLIENT_TO_SERVER).max_bytes * 2) result = {"type": "job_result", "job_id": "j", "status": 200, "body_b64": body} assert decode(json.dumps(result))["body_b64"] == body def test_protocol_errors_close_with_the_protocol_code(): assert reason("nope").close_code == CLOSE_PROTOCOL def test_encode_validates_what_the_server_sends(): text = encode("kick", SERVER_TO_CLIENT, reason="bye") assert json.loads(text) == {"type": "kick", "reason": "bye"} with pytest.raises(FrameError): encode("kick", SERVER_TO_CLIENT) with pytest.raises(FrameError): encode("kick", CLIENT_TO_SERVER, reason="x") with pytest.raises(FrameError): encode("kick", SERVER_TO_CLIENT, reason="x", extra=1) def test_every_frame_in_the_spec_can_be_built_and_decoded(): samples = {"str": "x", "int": 1, "bool": True, "object": {}, "list": []} for spec_frame in SPEC.frames: fields = {item.name: samples[item.type] for item in spec_frame.fields} text = encode(spec_frame.type, spec_frame.direction, **fields) assert decode(text, spec_frame.direction)["type"] == spec_frame.type