From 46cda79f73a1c9ec360e25accc383cdb4daaab51 Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Sun, 22 Oct 2023 11:08:45 +0200 Subject: [PATCH 1/2] Added new error codes --- meross_iot/model/http/error_codes.py | 83 ++++++++++++++++++++++++++-- tests/test_http.py | 27 ++++++++- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/meross_iot/model/http/error_codes.py b/meross_iot/model/http/error_codes.py index 8be9049..1753e87 100644 --- a/meross_iot/model/http/error_codes.py +++ b/meross_iot/model/http/error_codes.py @@ -9,6 +9,9 @@ class ErrorCodes(Enum): CODE_NO_ERROR = 0 """Not an error""" + CODE_MISSING_USER = 1000 + """Wrong or missing user""" + CODE_MISSING_PASSWORD = 1001 """Wrong or missing password""" @@ -27,9 +30,30 @@ class ErrorCodes(Enum): CODE_BAD_PASSWORD_FORMAT = 1006 """Bad password format""" + USER_ALREADY_EXISTS = 1007 + """User already exists""" + CODE_WRONG_EMAIL = 1008 """This email is not registered""" + SEND_EMAIL_FAILED = 1009 + """Email send failed""" + + WRONG_TICKET = 1011 + """Wrong Ticket""" + + CODE_OVERDUE = 1012 + """Code Overdue""" + + WRONG_CODE = 1013 + """Wrong Code""" + + DUPLICATE_PASSWORD = 1014 + """Duplicate password""" + + SAME_EMAIL = 1015 + """Same email when changing account email""" + CODE_TOKEN_INVALID = 1019 """Token expired""" @@ -54,17 +78,38 @@ class ErrorCodes(Enum): CODE_UNKNOWN_FAILURE_1027 = 1027 """Unknown error""" - CODE_UNKNOWN_FAILURE_1028 = 1028 - """Unknown error""" + REQUESTED_TOO_FREQUENTLY = 1028 + """Requested too frequently""" CODE_REDIRECT_REGION = 1030 """Wrong login region""" + USER_NAME_NOT_MATCHING = 1031 + """Username does not match""" + + WRONG_MFA_CODE = 1032 + """Wrong MFA Code""" + + MFA_CODE_REQUIRED = 1033 + """MFA Code required""" + + OPERATION_IS_LOCKED = 1035 + """Operation is locked""" + + REPEAT_CHECK_IN = 1041 + """Repeat checkin""" + + API_TOP_LIMIT_REACHED = 1042 + """API Top limit reached""" + + RESOURCE_ACCESS_DENY = 1043 + """Resource access deny""" + CODE_TOKEN_EXPIRED = 1200 """Token has expired""" - CODE_UNKNOWN_FAILURE_1201 = 1201 - """Unknown error""" + SERVER_UNABLE_GEN_TOKEN = 1201 + """Server was unable to generate token""" CODE_UNKNOWN_FAILURE_1202 = 1202 """Unknown error""" @@ -120,6 +165,12 @@ class ErrorCodes(Enum): CODE_MAX_CONTROL_BOARDS_REACHED = 1255 """The number of remote control boards exceeded the limit""" + CODE_COMPATIBILE_MODE_HAVING = 1256 + """Compatible mode having""" + + CODE_COMPATIBILE_MODE_NOT_HAVING = 1257 + """Compatible mode not having""" + CODE_TOO_MANY_TOKENS = 1301 """Too many tokens have been issued""" @@ -194,3 +245,27 @@ class ErrorCodes(Enum): CODE_IR_RECORD_INVALID = 5022 """Infrared record invalid""" + + SYSTEM_ERROR = 10001 + """System error""" + + UNKNOWN_ERROR = 10002 + """Unknown error""" + + SERIALIZE_ERROR = 10003 + """Serialize error""" + + HTTP_COMMON_ERROR = 10006 + """Http common error""" + + INVALID_PARAMETER = 20101 + """Invalid parameter""" + + RESOURCE_DOES_NOT_EXIST = 20106 + """Not existing resource""" + + UNSUPPORTED = 20112 + """Unsupported""" + + SEND_EMAIL_LIMIT = 20115 + """Send email limit""" diff --git a/tests/test_http.py b/tests/test_http.py index bceb038..1a0b2f4 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -4,7 +4,8 @@ from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from meross_iot.http_api import MerossHttpClient -from meross_iot.model.http.exception import BadLoginException, BadDomainException +from meross_iot.model.http.error_codes import ErrorCodes +from meross_iot.model.http.exception import BadLoginException, BadDomainException, HttpApiError from tests import async_get_client, _TEST_EMAIL, _TEST_PASSWORD, _TEST_API_BASE_URL if os.name == 'nt': @@ -48,9 +49,31 @@ async def test_subdevice_listing(self): async def test_bad_login(self): with self.assertRaises(BadLoginException): return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, - email="albertogeniola@gmail.com", + email=_TEST_EMAIL, password="thisIzWRONG!") + @unittest_run_loop + async def test_not_existing_email(self): + with self.assertRaises(HttpApiError): + try: + return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, + email="thisDoesNotExistIGuess@gmail.com", + password="thisIzWRONG!") + except HttpApiError as e: + self.assertEqual(e.error_code, ErrorCodes.CODE_WRONG_EMAIL) + raise e + + @unittest_run_loop + async def test_bad_email(self): + with self.assertRaises(HttpApiError): + try: + return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, + email="invalidemail", + password="somePassword") + except HttpApiError as e: + self.assertEqual(e.error_code, ErrorCodes.CODE_WRONG_EMAIL) + raise e + @unittest_run_loop async def test_device_listing(self): devices = await self.meross_client.async_list_devices() From 0bf351f5bb5e05349400fd7b070cc9afdc453283 Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Thu, 26 Oct 2023 16:25:19 +0200 Subject: [PATCH 2/2] Added new error codes --- meross_iot/http_api.py | 13 +++++++++++-- meross_iot/model/http/exception.py | 6 ++++++ tests/__init__.py | 5 +++-- tests/test_http.py | 21 ++++++++++++++++++--- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/meross_iot/http_api.py b/meross_iot/http_api.py index 0e861da..b8536ce 100644 --- a/meross_iot/http_api.py +++ b/meross_iot/http_api.py @@ -20,7 +20,7 @@ from meross_iot.model.http.device import HttpDeviceInfo from meross_iot.model.http.error_codes import ErrorCodes from meross_iot.model.http.exception import TooManyTokensException, TokenExpiredException, AuthenticatedPostException, \ - HttpApiError, BadLoginException, BadDomainException + HttpApiError, BadLoginException, BadDomainException, MissingMFA, WrongMFA from meross_iot.model.http.subdevice import HttpSubdeviceInfo from meross_iot.utilities.misc import current_version from meross_iot.utilities.stats import HttpStatsCounter @@ -97,6 +97,7 @@ async def async_from_user_password(cls, app_version: str = _MODULE_VERSION, log_identifier: str = _DEFAULT_LOG_IDENTIFIER, auto_retry_on_bad_domain: bool=True, + mfa_code: string = None, *args, **kwargs) -> MerossHttpClient: """ Builds a MerossHttpClient using username/password combination. @@ -110,6 +111,7 @@ async def async_from_user_password(cls, :param app_version: App Version header parameter to use :param log_identifier: Log identifier to use :param auto_retry_on_bad_domain: when set, it enables auto-retry when BadDomain exception occurs. + :param mfa_code: multi-factor authentication code (optional) :return: an instance of `MerossHttpClient` """ @@ -122,7 +124,8 @@ async def async_from_user_password(cls, app_type=app_type, app_version=app_version, log_identifier=log_identifier, - auto_retry_on_bad_domain=auto_retry_on_bad_domain) + auto_retry_on_bad_domain=auto_retry_on_bad_domain, + mfa_code=mfa_code) # Call log await cls._async_log(creds=creds, api_base_url=api_base_url, @@ -258,6 +261,12 @@ async def async_login(cls, else: _LOGGER.exception(f"Login failed against {api_base_url}") raise e + except HttpApiError as e: + if e.error_code == ErrorCodes.MFA_CODE_REQUIRED: + raise MissingMFA() from e + elif e.error_code == ErrorCodes.WRONG_MFA_CODE: + raise WrongMFA() from e + raise e _LOGGER.info(f"Login successful against {api_base_url}") diff --git a/meross_iot/model/http/exception.py b/meross_iot/model/http/exception.py index d759b13..8bb2696 100644 --- a/meross_iot/model/http/exception.py +++ b/meross_iot/model/http/exception.py @@ -14,6 +14,12 @@ def error_code(self): class BadLoginException(Exception): pass +class MissingMFA(BadLoginException): + pass + +class WrongMFA(BadLoginException): + pass + class BadDomainException(Exception): def __init__(self, msg: str, api_domain: str, mqtt_domain:str): super().__init__(msg) diff --git a/tests/__init__.py b/tests/__init__.py index 3bb9836..4231fa1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,8 +7,9 @@ from meross_iot.http_api import MerossHttpClient from meross_iot.model.credentials import MerossCloudCreds -_TEST_API_BASE_URL = os.environ.get('MEROSS_API_URL', "https://iot.meross.com") +_TEST_API_BASE_URL = os.environ.get('MEROSS_API_URL', "https://iotx-us.meross.com") _TEST_EMAIL = os.environ.get('MEROSS_EMAIL') +_TEST_EMAIL_MFA = os.environ.get('MEROSS_EMAIL_MFA') _TEST_PASSWORD = os.environ.get('MEROSS_PASSWORD') _TEST_CREDS = os.getenv("__MEROSS_CREDS") @@ -24,7 +25,7 @@ async def async_get_client() -> Tuple[MerossHttpClient, bool]: if _TEST_API_BASE_URL is not None: api_base_url = _TEST_API_BASE_URL else: - api_base_url = "https://iotx-eu.meross.com" + api_base_url = "https://iotx-us.meross.com" if _TEST_CREDS is not None: _LOGGER.info("Found cached credentials. Using them.") diff --git a/tests/test_http.py b/tests/test_http.py index 1a0b2f4..e9da328 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -5,8 +5,8 @@ from meross_iot.http_api import MerossHttpClient from meross_iot.model.http.error_codes import ErrorCodes -from meross_iot.model.http.exception import BadLoginException, BadDomainException, HttpApiError -from tests import async_get_client, _TEST_EMAIL, _TEST_PASSWORD, _TEST_API_BASE_URL +from meross_iot.model.http.exception import BadLoginException, BadDomainException, HttpApiError, MissingMFA, WrongMFA +from tests import async_get_client, _TEST_EMAIL, _TEST_PASSWORD, _TEST_API_BASE_URL, _TEST_EMAIL_MFA if os.name == 'nt': import asyncio @@ -74,6 +74,21 @@ async def test_bad_email(self): self.assertEqual(e.error_code, ErrorCodes.CODE_WRONG_EMAIL) raise e + @unittest_run_loop + async def test_missing_mfa(self): + with self.assertRaises(MissingMFA): + return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, + email=_TEST_EMAIL_MFA, + password=_TEST_PASSWORD) + + @unittest_run_loop + async def test_wrong_mfa(self): + with self.assertRaises(WrongMFA): + return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, + email=_TEST_EMAIL_MFA, + password=_TEST_PASSWORD, + mfa_code="invalid") + @unittest_run_loop async def test_device_listing(self): devices = await self.meross_client.async_list_devices() @@ -84,7 +99,7 @@ async def test_device_listing(self): @unittest_run_loop async def test_bad_domain(self): with self.assertRaises(BadDomainException): - return await MerossHttpClient.async_from_user_password(api_base_url=_TEST_API_BASE_URL, email=_TEST_EMAIL, password=_TEST_PASSWORD, auto_retry_on_bad_domain=False) + return await MerossHttpClient.async_from_user_password(api_base_url="iot.meross.com", email=_TEST_EMAIL, password=_TEST_PASSWORD, auto_retry_on_bad_domain=False) @unittest_run_loop