Skip to content

Commit

Permalink
Merge branch 'feature/mfa' of https://github.com/albertogeniola/Meros…
Browse files Browse the repository at this point in the history
…sIot into development-0.4.X.X
  • Loading branch information
albertogeniola committed Oct 26, 2023
2 parents a45a9c3 + 0bf351f commit 56e123c
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 12 deletions.
13 changes: 11 additions & 2 deletions meross_iot/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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`
"""
Expand All @@ -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,
Expand Down Expand Up @@ -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}")

Expand Down
83 changes: 79 additions & 4 deletions meross_iot/model/http/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand All @@ -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"""

Expand All @@ -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"""
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -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"""
6 changes: 6 additions & 0 deletions meross_iot/model/http/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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.")
Expand Down
46 changes: 42 additions & 4 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
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 tests import async_get_client, _TEST_EMAIL, _TEST_PASSWORD, _TEST_API_BASE_URL
from meross_iot.model.http.error_codes import ErrorCodes
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
Expand Down Expand Up @@ -48,9 +49,46 @@ 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="[email protected]",
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="[email protected]",
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_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()
Expand All @@ -61,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
Expand Down

0 comments on commit 56e123c

Please sign in to comment.