diff --git a/README.md b/README.md index 4e0bc9a..fc66845 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ If you are not sure which encryption method to use, you can leave it empty or pa This function will throw a `LoginTimeoutException` when no match is found, since this is still a HTTP Time Out. This could caused by the wrong encryption method, but also by trying to connect to an inaccessible host. +It will throw a `LoginRetryErrorException` when the router has locked logins after too many attempts. Wait for the lock to expire before trying again. + ### Handle exceptions Some functions may cause an error when an attempt is made to execute it. These exceptions are thrown by the client and need to be [handled in your Python program](https://docs.python.org/3/tutorial/errors.html#handling-exceptions). Best practice is to catch some specific exceptions and handle them gracefully. diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index e2c6081..6c10ded 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -364,7 +364,11 @@ async def logout(self): self._request_id = -1 async def get_encryption_method(self): - """Determine which encryption method to use for authentication and set it directly.""" + """Determine which encryption method to use for authentication and set it directly. + + :raises LoginRetryErrorException: the router has locked logins after too many attempts. + Probing further would only extend the lock, so detection stops here. + """ for encryption_method in EncryptionMethod: try: self.authentication_method = encryption_method @@ -380,7 +384,6 @@ async def get_encryption_method(self): except ( LoginTimeoutException, AuthenticationException, - LoginRetryErrorException, ): pass diff --git a/tests/conftest.py b/tests/conftest.py index 5b39653..edf39a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,12 @@ def login_auth_error_response() -> dict[str, Any]: return load_fixture("login_auth_error.json") +@pytest.fixture +def login_retry_error_response() -> dict[str, Any]: + """Mock response for a login locked after too many attempts.""" + return load_fixture("login_retry_error.json") + + @pytest.fixture def login_invalid_session_response() -> dict[str, Any]: """Mock response for invalid session error.""" diff --git a/tests/fixtures/login_retry_error.json b/tests/fixtures/login_retry_error.json new file mode 100644 index 0000000..f0e0697 --- /dev/null +++ b/tests/fixtures/login_retry_error.json @@ -0,0 +1,22 @@ +{ + "reply": { + "uid": 0, + "id": 0, + "error": { + "code": 16777236, + "description": "XMO_REQUEST_ACTION_ERR" + }, + "actions": [ + { + "uid": 1, + "id": 0, + "error": { + "code": 16777224, + "description": "XMO_LOGIN_RETRY_ERR" + }, + "callbacks": [] + } + ], + "events": [] + } +} diff --git a/tests/unit/test_encryption_method.py b/tests/unit/test_encryption_method.py new file mode 100644 index 0000000..32ec1d8 --- /dev/null +++ b/tests/unit/test_encryption_method.py @@ -0,0 +1,31 @@ +"""Tests for automatic encryption method detection.""" + +import pytest + +from sagemcom_api.client import SagemcomClient +from sagemcom_api.enums import EncryptionMethod +from sagemcom_api.exceptions import LoginRetryErrorException + + +@pytest.mark.asyncio +async def test_detection_stops_when_router_locks_login(mock_session_factory, login_retry_error_response): + """XMO_LOGIN_RETRY_ERR means the router has locked logins; probing further only extends the lock.""" + mock_session = mock_session_factory([login_retry_error_response] * 3) + client = SagemcomClient("192.168.1.1", "admin", "admin", session=mock_session) + + with pytest.raises(LoginRetryErrorException): + await client.get_encryption_method() + + assert mock_session.post.call_count == 1 + + +@pytest.mark.asyncio +async def test_detection_moves_on_after_authentication_error(mock_session_factory, login_auth_error_response, login_success_response): + """A plain authentication error still means "wrong method", so the next method is tried.""" + mock_session = mock_session_factory([login_auth_error_response, login_success_response]) + client = SagemcomClient("192.168.1.1", "admin", "admin", session=mock_session) + + method = await client.get_encryption_method() + + assert method == EncryptionMethod.MD5_NONCE + assert mock_session.post.call_count == 2