浏览代码

Drop the login assert; validate credentials in _open_client

The assert demanded a login even for the documented CHATTO_TOKEN-only
path, and AssertionError was not in the except tuple — it escaped
_get_chatto_client as a crash instead of the promised client-or-None.
It also vanished under python -O.

_open_client now raises ValueError when neither token nor
login+password is configured, which upstream already catches and logs
as a normal creation failure. Token-only auth works; three tests pin
the contract.
Paul Klumpp 1 周之前
父节点
当前提交
1b92c4ea39
共有 2 个文件被更改,包括 49 次插入5 次删除
  1. 9 5
      adapter.py
  2. 40 0
      test_adapter.py

+ 9 - 5
adapter.py

@@ -336,10 +336,6 @@ class ChattoAdapter(BasePlatformAdapter):
                 return self._chatto_client
 
             try:
-                assert (
-                    self.chatto_config.login.value
-                )  # now we can assume, _login is available.
-
                 client = await self._open_client(
                     base_url=self.chatto_config.base_url.value,
                     login=self.chatto_config.login.value,
@@ -395,9 +391,17 @@ class ChattoAdapter(BasePlatformAdapter):
         password: str,
         token: str | None = None,
     ) -> ChattoClient:
-        """Return a connected ``ChattoClient`` using token or login/password."""
+        """Return a connected ``ChattoClient`` using token or login/password.
+
+        Token wins when both are configured. Raises ValueError when neither a
+        token nor login+password is set — caught upstream as a normal
+        creation failure, so misconfiguration reads as a logged error instead
+        of a crash.
+        """
         if token:
             return ChattoClient(token=token, base_url=base_url)
+        if not login or not password:
+            raise ValueError("Chatto: neither token nor login/password configured")
         return await ChattoClient.login(login, password, base_url=base_url)
 
     async def connect(self, *, is_reconnect: bool = False) -> bool:

+ 40 - 0
test_adapter.py

@@ -38,6 +38,7 @@ from vendor_path import setup_vendor_path
 
 setup_vendor_path()
 
+from chattolib.client import ChattoClient
 from chattolib.realtime_types import ReactionPayload
 from chattolib.types import (
     Asset,
@@ -1909,3 +1910,42 @@ class TestConstants:
 
     def test_seen_cap(self):
         assert _SEEN_CAP == 500
+
+
+# -- Client creation credentials --
+
+
+class TestOpenClientCredentials:
+    """Token wins; nothing configured raises as a normal creation failure."""
+
+    def _adapter(self):
+        return _make_adapter()
+
+    async def test_token_only_connects_without_login(self):
+        """The documented CHATTO_TOKEN path must not demand a login."""
+        adapter = self._adapter()
+        client = await adapter._open_client(
+            base_url="https://chat.example.com", login="", password="", token="t"
+        )
+        assert isinstance(client, ChattoClient)
+
+    async def test_no_credentials_raises_value_error(self):
+        """Misconfiguration reads as a logged creation failure, not a crash."""
+        adapter = self._adapter()
+        with pytest.raises(ValueError, match="neither token nor login"):
+            await adapter._open_client(
+                base_url="https://chat.example.com", login="", password=""
+            )
+
+    async def test_login_password_path_is_used_without_token(self):
+        adapter = self._adapter()
+        with patch("adapter.ChattoClient") as client_cls:
+            client_cls.login = AsyncMock(return_value=MagicMock())
+
+            await adapter._open_client(
+                base_url="https://chat.example.com", login="u", password="p"
+            )
+
+            client_cls.login.assert_awaited_once_with(
+                "u", "p", base_url="https://chat.example.com"
+            )