Repository: xconnio/wampproto-python (language: python)

Issue:
Auth Messages
Need Message for authentication defined

1. Challenge
2. Authenticate


Discussion / hints:

Fix patch:
diff --git a/wamp/messages/authenticate.py b/wamp/messages/authenticate.py
new file mode 100644
index 0000000..e8d7701
--- /dev/null
+++ b/wamp/messages/authenticate.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+from wamp.messages import error, util
+from wamp.messages.message import Message
+
+
+class Authenticate(Message):
+    AUTHENTICATE_TEXT = "CHALLENGE"
+    MESSAGE_TYPE = 5
+
+    def __init__(self, signature: str, extra: dict | None = None):
+        super().__init__()
+        self.signature = signature
+        self.extra = {} if extra is None else extra
+
+    @staticmethod
+    def parse(msg: list) -> Authenticate:
+        util.validate_message_or_raise(msg, Authenticate.AUTHENTICATE_TEXT)
+
+        if msg[0] != Authenticate.MESSAGE_TYPE:
+            raise error.ProtocolError(f"invalid message type for {Authenticate.AUTHENTICATE_TEXT}")
+
+        signature = msg[1]
+        if not isinstance(signature, str):
+            raise error.ProtocolError(
+                f"invalid type {type(signature)} for 'signature' in {Authenticate.AUTHENTICATE_TEXT}"
+            )
+
+        extra = util.validate_details_or_raise(msg[2], Authenticate.AUTHENTICATE_TEXT)
+
+        return Authenticate(signature, extra)
+
+    def marshal(self):
+        return [Authenticate.MESSAGE_TYPE, self.signature, self.extra]
diff --git a/wamp/messages/challenge.py b/wamp/messages/challenge.py
new file mode 100644
index 0000000..4fb6659
--- /dev/null
+++ b/wamp/messages/challenge.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from wamp.messages import error, util
+from wamp.messages.message import Message
+
+
+class Challenge(Message):
+    CHALLENGE_TEXT = "CHALLENGE"
+    MESSAGE_TYPE = 4
+
+    def __init__(self, authmethod: str, extra: dict | None = None):
+        super().__init__()
+        self.authmethod = authmethod
+        self.extra = {} if extra is None else extra
+
+    @staticmethod
+    def parse(msg: list) -> Challenge:
+        util.validate_message_or_raise(msg, Challenge.CHALLENGE_TEXT)
+
+        if msg[0] != Challenge.MESSAGE_TYPE:
+            raise error.ProtocolError(f"invalid message type for {Challenge.CHALLENGE_TEXT}")
+
+        authmethod = msg[1]
+        if not isinstance(authmethod, str):
+            raise error.ProtocolError(f"invalid type {type(authmethod)} for 'authmethod' in {Challenge.CHALLENGE_TEXT}")
+
+        extra = util.validate_details_or_raise(msg[2], Challenge.CHALLENGE_TEXT)
+
+        return Challenge(authmethod, extra)
+
+    def marshal(self):
+        return [Challenge.MESSAGE_TYPE, self.authmethod, self.extra]


Test patch:
diff --git a/tests/messages/test_authenticate.py b/tests/messages/test_authenticate.py
new file mode 100644
index 0000000..1da5d88
--- /dev/null
+++ b/tests/messages/test_authenticate.py
@@ -0,0 +1,105 @@
+import pytest
+
+from wamp.messages import error
+from wamp.messages.authenticate import Authenticate
+
+
+def test_parse_with_invalid_type():
+    message = "msg"
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Authenticate.parse(message)
+
+    assert (
+        str(exc_info.value)
+        == f"invalid message type '{type(message)}' for {Authenticate.AUTHENTICATE_TEXT}, type should be a list"
+    )
+
+
+def test_parse_with_invalid_list_length():
+    message = [5]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Authenticate.parse(message)
+
+    assert (
+        str(exc_info.value) == f"invalid message length '{len(message)}' for {Authenticate.AUTHENTICATE_TEXT}, "
+        f"length should be equal to three"
+    )
+
+
+def test_parse_with_invalid_message_type():
+    message = [4, "signature", {}]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Authenticate.parse(message)
+
+    assert str(exc_info.value) == f"invalid message type for {Authenticate.AUTHENTICATE_TEXT}"
+
+
+def test_parse_with_invalid_signature_type():
+    message = [5, ["signature"], {}]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Authenticate.parse(message)
+
+    assert str(exc_info.value) == f"invalid type {type(message[1])} for 'signature' in {Authenticate.AUTHENTICATE_TEXT}"
+
+
+def test_parse_with_invalid_extra_type():
+    message = [5, "signature", "extra"]
+    with pytest.raises(error.InvalidDetailsError) as exc_info:
+        Authenticate.parse(message)
+
+    assert str(exc_info.value) == f"details must be of type dictionary for {Authenticate.AUTHENTICATE_TEXT}"
+
+
+def test_parse_with_invalid_details_dict_key():
+    message = [5, "signature", {1: "v"}]
+    with pytest.raises(error.InvalidDetailsError) as exc_info:
+        Authenticate.parse(message)
+
+    assert str(exc_info.value) == f"invalid type for key '1' in extra details for {Authenticate.AUTHENTICATE_TEXT}"
+
+
+def test_parse_correctly():
+    signature = "signature"
+    extra = {"channel_binding": "null"}
+    message = [5, signature, extra]
+    authenticate = Authenticate.parse(message)
+
+    assert isinstance(authenticate, Authenticate)
+
+    assert isinstance(authenticate.signature, str)
+    assert authenticate.signature == signature
+
+    assert isinstance(authenticate.extra, dict)
+    assert authenticate.extra == extra
+
+
+def test_marshal_without_extra():
+    signature = "signature"
+    message = Authenticate(signature).marshal()
+
+    assert isinstance(message, list)
+
+    assert isinstance(message[0], int)
+    assert message[0] == Authenticate.MESSAGE_TYPE
+
+    assert isinstance(message[1], str)
+    assert message[1] == signature
+
+    assert message[2] == dict()
+
+
+def test_marshal_with_extra():
+    signature = "signature"
+    extra = {"channel_binding": "null"}
+    message = Authenticate(signature, extra).marshal()
+
+    assert isinstance(message, list)
+
+    assert isinstance(message[0], int)
+    assert message[0] == Authenticate.MESSAGE_TYPE
+
+    assert isinstance(message[1], str)
+    assert message[1] == signature
+
+    assert isinstance(message[2], dict)
+    assert message[2] == extra
diff --git a/tests/messages/test_challenge.py b/tests/messages/test_challenge.py
new file mode 100644
index 0000000..18b03fd
--- /dev/null
+++ b/tests/messages/test_challenge.py
@@ -0,0 +1,105 @@
+import pytest
+
+from wamp.messages import error
+from wamp.messages.challenge import Challenge
+
+
+def test_parse_with_invalid_type():
+    message = "msg"
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Challenge.parse(message)
+
+    assert (
+        str(exc_info.value)
+        == f"invalid message type '{type(message)}' for {Challenge.CHALLENGE_TEXT}, type should be a list"
+    )
+
+
+def test_parse_with_invalid_list_length():
+    message = [1]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Challenge.parse(message)
+
+    assert (
+        str(exc_info.value)
+        == f"invalid message length '{len(message)}' for {Challenge.CHALLENGE_TEXT}, length should be equal to three"
+    )
+
+
+def test_parse_with_invalid_message_type():
+    message = [2, "anonymous", {}]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Challenge.parse(message)
+
+    assert str(exc_info.value) == f"invalid message type for {Challenge.CHALLENGE_TEXT}"
+
+
+def test_parse_with_invalid_authmethod_type():
+    message = [4, ["authmethod"], {}]
+    with pytest.raises(error.ProtocolError) as exc_info:
+        Challenge.parse(message)
+
+    assert str(exc_info.value) == f"invalid type {type(message[1])} for 'authmethod' in {Challenge.CHALLENGE_TEXT}"
+
+
+def test_parse_with_invalid_extra_type():
+    message = [4, "anonymous", "extra"]
+    with pytest.raises(error.InvalidDetailsError) as exc_info:
+        Challenge.parse(message)
+
+    assert str(exc_info.value) == f"details must be of type dictionary for {Challenge.CHALLENGE_TEXT}"
+
+
+def test_parse_with_invalid_details_dict_key():
+    message = [4, "anonymous", {1: "v"}]
+    with pytest.raises(error.InvalidDetailsError) as exc_info:
+        Challenge.parse(message)
+
+    assert str(exc_info.value) == f"invalid type for key '1' in extra details for {Challenge.CHALLENGE_TEXT}"
+
+
+def test_parse_correctly():
+    authmethod = "cryptosign"
+    extra = {"channel_binding": "tls-unique"}
+    message = [4, authmethod, extra]
+    challenge = Challenge.parse(message)
+
+    assert isinstance(challenge, Challenge)
+
+    assert isinstance(challenge.authmethod, str)
+    assert challenge.authmethod == authmethod
+
+    assert isinstance(challenge.extra, dict)
+    assert challenge.extra == extra
+
+
+def test_marshal_without_extra():
+    authmethod = "cryptosign"
+    message = Challenge(authmethod).marshal()
+
+    assert isinstance(message, list)
+
+    assert isinstance(message[0], int)
+    assert message[0] == Challenge.MESSAGE_TYPE
+
+    assert isinstance(message[1], str)
+    assert message[1] == authmethod
+
+    assert message[2] == dict()
+
+
+def test_marshal_with_extra():
+    authmethod = "wamp-scram"
+    extra = {"iterations": 4096}
+    message = Challenge(authmethod, extra).marshal()
+
+    assert isinstance(message, list)
+
+    assert isinstance(message[0], int)
+    assert message[0] == Challenge.MESSAGE_TYPE
+
+    assert isinstance(message[1], str)
+    assert message[1] == authmethod
+
+    assert isinstance(message[2], dict)
+    assert message[2] == extra
diff --git a/tests/messages/test_util.py b/tests/messages/test_util.py
index f003f40..1cce79c 100644
--- a/tests/messages/test_util.py
+++ b/tests/messages/test_util.py
@@ -95,3 +95,26 @@ def test_validate_uri_or_raise_correctly():
     uri = "wamp.close.goodbye_and_out"
     result = util.validate_uri_or_raise(uri, "")
     assert result == uri
+
+
+def test_validate_message_or_raise_with_invalid_type():
+    message = "wamp message"
+    error_msg = "Pie"
+
+    with pytest.raises(error.ProtocolError) as exc_info:
+        util.validate_message_or_raise(message, error_msg)
+
+    assert str(exc_info.value) == f"invalid message type '{type(message)}' for {error_msg}, type should be a list"
+
+
+def test_validate_message_or_raise_with_invalid_list_length():
+    message = ["wamp message"]
+    error_msg = "Pie"
+
+    with pytest.raises(error.ProtocolError) as exc_info:
+        util.validate_message_or_raise(message, error_msg)
+
+    assert (
+        str(exc_info.value)
+        == f"invalid message length '{len(message)}' for {error_msg}, length should be equal to three"
+    )