From 22006f7c0f92160eac4d9cfdd5b5d93f785ff93a Mon Sep 17 00:00:00 2001 From: James Woglom Date: Sun, 11 Jun 2023 00:13:45 -0400 Subject: [PATCH] Bump supported software version and display login error message --- tconnectsync/api/common.py | 2 +- tconnectsync/api/controliq.py | 22 ++++++++++++-- tconnectsync/util/__init__.py | 7 ++++- tests/api/test_controliq.py | 55 ++++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/tconnectsync/api/common.py b/tconnectsync/api/common.py index 2ee889d..f92dd36 100644 --- a/tconnectsync/api/common.py +++ b/tconnectsync/api/common.py @@ -127,7 +127,7 @@ def split_days_range(start_a, end_a, days: int = 5) -> List[Tuple[str, str]]: class ApiException(Exception): def __init__(self, status_code, text, *args, **kwargs): self.status_code = status_code - super().__init__('%s (HTTP %s)' % (text, status_code), *args, **kwargs) + super().__init__('%s%s' % (text, ' (HTTP %s)' % status_code if status_code else ''), *args, **kwargs) class ApiLoginException(ApiException): pass \ No newline at end of file diff --git a/tconnectsync/api/controliq.py b/tconnectsync/api/controliq.py index 10527c2..029ab94 100644 --- a/tconnectsync/api/controliq.py +++ b/tconnectsync/api/controliq.py @@ -5,7 +5,7 @@ import logging from bs4 import BeautifulSoup -from ..util import timeago +from ..util import timeago, cap_length from .common import parse_date, base_headers, base_session, ApiException, ApiLoginException logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ class ControlIQApi: BASE_URL = 'https://tdcservices.tandemdiabetes.com/' LOGIN_URL = 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f' - LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.14.0.1' + LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.15.0.1' userGuid = None accessToken = None @@ -34,12 +34,20 @@ class ControlIQApi: data = self._build_login_data(email, password, soup) req = s.post(self.LOGIN_URL, data=data, headers={'Referer': self.LOGIN_URL, **base_headers()}, allow_redirects=False) + # HTTP 200 is reported when credentials are incorrect + if req.status_code == 200: + login_error = self._find_login_error(req.text) + if not login_error: + login_error = 'Check your login credentials.' + raise ApiLoginException(None, 'Error logging in to t:connect: %s' % login_error) + if req.status_code != 302: - raise ApiLoginException(req.status_code, 'Error logging in to t:connect. Check your login credentials.') + raise ApiLoginException(req.status_code, 'Error logging in to t:connect') fwd = s.post(urllib.parse.urljoin(self.LOGIN_URL, req.headers['Location']), cookies=req.cookies, headers=base_headers()) if fwd.status_code != 200: + logger.warn("Received non-HTTP 200: %s" % req.text) raise ApiException(fwd.status_code, 'Error retrieving t:connect login cookies.') self.userGuid = req.cookies['UserGUID'] @@ -79,6 +87,14 @@ class ControlIQApi: "txtLoginPassword_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % (password, password, password) } + def _find_login_error(self, text): + try: + soup = BeautifulSoup(text, features='lxml') + notice_error = soup.select_one(".notice_error").text.strip() + return notice_error + except Exception: + return None + def needs_relogin(self): diff = (arrow.get(self.accessTokenExpiresAt) - arrow.get()) return (diff.seconds <= 5 * 60) diff --git a/tconnectsync/util/__init__.py b/tconnectsync/util/__init__.py index 4f2f8b8..8758c22 100644 --- a/tconnectsync/util/__init__.py +++ b/tconnectsync/util/__init__.py @@ -28,4 +28,9 @@ def removesuffix(input_string, suffix): def removeprefix(input_string, prefix): if prefix and input_string.startswith(prefix): return input_string[len(prefix):] - return input_string \ No newline at end of file + return input_string + +def cap_length(text, maxlen): + if not text or len(text) <= maxlen: + return text + return '%s[...]%s' % (text[:maxlen//2], text[maxlen//-2:]) \ No newline at end of file diff --git a/tests/api/test_controliq.py b/tests/api/test_controliq.py index 23de473..2c05ed3 100644 --- a/tests/api/test_controliq.py +++ b/tests/api/test_controliq.py @@ -111,12 +111,65 @@ class TestControlIQApi(unittest.TestCase): text=post_callback) - self.assertRaises(ApiLoginException, ciq.login, 'email@email.com', 'password') + self.assertRaisesRegex(ApiLoginException, 'Error logging in to t:connect: Check your login credentials.', ciq.login, 'email@email.com', 'password') self.assertIsNone(ciq.userGuid) self.assertIsNone(ciq.accessToken) self.assertIsNone(ciq.accessTokenExpiresAt) + def test_login_invalid_credentials_parsed_message(self): + ciq = ControlIQApi() + ciq.LOGIN_URL = RealControlIQApi.LOGIN_URL + ciq.login = lambda email, password: RealControlIQApi.login(ciq, email, password) + + with requests_mock.Mocker() as m: + m.get('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f', + request_headers=base_headers(), + + text=self.LOGIN_HTML) + + def post_callback(request, context): + context.status_code = 200 + return '
The email address or password you entered is invalid. Please re-enter and try again.
' + + m.post('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f', + request_headers={'Referer': ciq.LOGIN_URL, **base_headers()}, + + text=post_callback) + + self.assertRaisesRegex(ApiLoginException, 'Error logging in to t:connect: The email address or password you entered is invalid. Please re-enter and try again.', ciq.login, 'email@email.com', 'password') + + self.assertIsNone(ciq.userGuid) + self.assertIsNone(ciq.accessToken) + self.assertIsNone(ciq.accessTokenExpiresAt) + + def test_login_unexpected_http_code(self): + ciq = ControlIQApi() + ciq.LOGIN_URL = RealControlIQApi.LOGIN_URL + ciq.login = lambda email, password: RealControlIQApi.login(ciq, email, password) + + with requests_mock.Mocker() as m: + m.get('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f', + request_headers=base_headers(), + + text=self.LOGIN_HTML) + + def post_callback(request, context): + context.status_code = 500 + return '...' + + m.post('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f', + request_headers={'Referer': ciq.LOGIN_URL, **base_headers()}, + + text=post_callback) + + self.assertRaisesRegex(ApiLoginException, 'Error logging in to t:connect \(HTTP 500\)', ciq.login, 'email@email.com', 'password') + + self.assertIsNone(ciq.userGuid) + self.assertIsNone(ciq.accessToken) + self.assertIsNone(ciq.accessTokenExpiresAt) + + def fake_get_with_http_code(self, http_code, expected_endpoint, num_times): tries = 0 def fake_get(endpoint, query):