mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
api/controliq: add login tests
This commit is contained in:
@@ -11,7 +11,8 @@ bs4 = "*"
|
|||||||
arrow = "*"
|
arrow = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
python-dotenv = "*"
|
python-dotenv = "*"
|
||||||
|
requests-mock = "*"
|
||||||
|
|
||||||
[scripts]
|
[scripts]
|
||||||
tconnectsync = "python3 main.py"
|
tconnectsync = "python3 main.py"
|
||||||
test = "python3 -m unittest discover -vv"
|
test = "python3 -m unittest discover -vv"
|
||||||
|
|||||||
Generated
+9
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "071afba23de7f532f99a0611caed63951c6f7ba968ba877ad5c76c244e78577e"
|
"sha256": "979feca3aba1b7f43890a94862347738158af5847959c14dc1d5f23ce5db8cdc"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@@ -126,6 +126,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.25.1"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
|
"requests-mock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:11215c6f4df72702aa357f205cf1e537cffd7392b3e787b58239bde5fb3db53b",
|
||||||
|
"sha256:e68f46844e4cee9d447150343c9ae875f99fa8037c6dcf5f15bf1fe9ab43d226"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.8.0"
|
||||||
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def base_headers():
|
|||||||
class ApiException(Exception):
|
class ApiException(Exception):
|
||||||
def __init__(self, status_code, text, *args, **kwargs):
|
def __init__(self, status_code, text, *args, **kwargs):
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
super().__init__(text, *args, **kwargs)
|
super().__init__('%s (HTTP %s)' % (text, status_code), *args, **kwargs)
|
||||||
|
|
||||||
class ApiLoginException(ApiException):
|
class ApiLoginException(ApiException):
|
||||||
pass
|
pass
|
||||||
@@ -25,18 +25,8 @@ class ControlIQApi:
|
|||||||
with requests.Session() as s:
|
with requests.Session() as s:
|
||||||
initial = s.get(self.LOGIN_URL, headers=base_headers())
|
initial = s.get(self.LOGIN_URL, headers=base_headers())
|
||||||
soup = BeautifulSoup(initial.content, features='lxml')
|
soup = BeautifulSoup(initial.content, features='lxml')
|
||||||
data = {
|
data = self._build_login_data(email, password, soup)
|
||||||
"__LASTFOCUS": "",
|
|
||||||
"__EVENTTARGET": "ctl00$ContentBody$LoginControl$linkLogin",
|
|
||||||
"__EVENTARGUMENT": "",
|
|
||||||
"__VIEWSTATE": soup.select_one("#__VIEWSTATE")["value"],
|
|
||||||
"__VIEWSTATEGENERATOR": soup.select_one("#__VIEWSTATEGENERATOR")["value"],
|
|
||||||
"__EVENTVALIDATION": soup.select_one("#__EVENTVALIDATION")["value"],
|
|
||||||
"ctl00$ContentBody$LoginControl$txtLoginEmailAddress": email,
|
|
||||||
"txtLoginEmailAddress_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % (email, email, email),
|
|
||||||
"ctl00$ContentBody$LoginControl$txtLoginPassword": password,
|
|
||||||
"txtLoginPassword_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % (password, password, password)
|
|
||||||
}
|
|
||||||
req = s.post(self.LOGIN_URL, data=data, headers={'Referer': self.LOGIN_URL, **base_headers()}, allow_redirects=False)
|
req = s.post(self.LOGIN_URL, data=data, headers={'Referer': self.LOGIN_URL, **base_headers()}, allow_redirects=False)
|
||||||
if req.status_code != 302:
|
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. Check your login credentials.')
|
||||||
@@ -51,6 +41,20 @@ class ControlIQApi:
|
|||||||
self.accessTokenExpiresAt = req.cookies['accessTokenExpiresAt']
|
self.accessTokenExpiresAt = req.cookies['accessTokenExpiresAt']
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _build_login_data(self, email, password, soup):
|
||||||
|
return {
|
||||||
|
"__LASTFOCUS": "",
|
||||||
|
"__EVENTTARGET": "ctl00$ContentBody$LoginControl$linkLogin",
|
||||||
|
"__EVENTARGUMENT": "",
|
||||||
|
"__VIEWSTATE": soup.select_one("#__VIEWSTATE")["value"],
|
||||||
|
"__VIEWSTATEGENERATOR": soup.select_one("#__VIEWSTATEGENERATOR")["value"],
|
||||||
|
"__EVENTVALIDATION": soup.select_one("#__EVENTVALIDATION")["value"],
|
||||||
|
"ctl00$ContentBody$LoginControl$txtLoginEmailAddress": email,
|
||||||
|
"txtLoginEmailAddress_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % (email, email, email),
|
||||||
|
"ctl00$ContentBody$LoginControl$txtLoginPassword": password,
|
||||||
|
"txtLoginPassword_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % (password, password, password)
|
||||||
|
}
|
||||||
|
|
||||||
def needs_relogin(self):
|
def needs_relogin(self):
|
||||||
diff = (arrow.get(self.accessTokenExpiresAt) - arrow.get())
|
diff = (arrow.get(self.accessTokenExpiresAt) - arrow.get())
|
||||||
return (diff.seconds <= 5 * 60)
|
return (diff.seconds <= 5 * 60)
|
||||||
|
|||||||
+111
-3
@@ -3,12 +3,120 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import itertools
|
import itertools
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
import requests_mock
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from .fake import ControlIQApi
|
from .fake import ControlIQApi
|
||||||
|
|
||||||
from tconnectsync.api.common import ApiException
|
from tconnectsync.api.controliq import ControlIQApi as RealControlIQApi
|
||||||
|
from tconnectsync.api.common import ApiException, ApiLoginException, base_headers
|
||||||
|
|
||||||
class TestControlIQApi(unittest.TestCase):
|
class TestControlIQApi(unittest.TestCase):
|
||||||
|
LOGIN_HTML = """
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form method="post" action="./login.aspx?ReturnUrl=%2f" onsubmit="javascript:return WebForm_OnSubmit();" id="form1">
|
||||||
|
<div class="aspNetHidden">
|
||||||
|
<input type="hidden" name="__LASTFOCUS" id="__LASTFOCUS" value="" />
|
||||||
|
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
|
||||||
|
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
|
||||||
|
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="AAAAA" />
|
||||||
|
</div>
|
||||||
|
<div class="aspNetHidden">
|
||||||
|
<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="BBBBB" />
|
||||||
|
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="CCCCC" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOGIN_POST_DATA = {
|
||||||
|
"__LASTFOCUS": "",
|
||||||
|
"__EVENTTARGET": "ctl00$ContentBody$LoginControl$linkLogin",
|
||||||
|
"__EVENTARGUMENT": "",
|
||||||
|
"__VIEWSTATE": "AAAAA",
|
||||||
|
"__VIEWSTATEGENERATOR": "BBBBB",
|
||||||
|
"__EVENTVALIDATION": "CCCCC",
|
||||||
|
"ctl00$ContentBody$LoginControl$txtLoginEmailAddress": "email@email.com",
|
||||||
|
"txtLoginEmailAddress_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % ("email@email.com", "email@email.com", "email@email.com"),
|
||||||
|
"ctl00$ContentBody$LoginControl$txtLoginPassword": "password",
|
||||||
|
"txtLoginPassword_ClientState": '{"enabled":true,"emptyMessage":"","validationText":"%s","valueAsString":"%s","lastSetTextBoxValue":"%s"}' % ("password", "password", "password")
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_build_login_data(self):
|
||||||
|
ciq = ControlIQApi()
|
||||||
|
soup = BeautifulSoup(self.LOGIN_HTML, features='lxml')
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
ciq._build_login_data('email@email.com', 'password', soup),
|
||||||
|
self.LOGIN_POST_DATA)
|
||||||
|
|
||||||
|
def test_login_successful(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 = 302
|
||||||
|
context.headers['Location'] = '/newlocation'
|
||||||
|
context.cookies['UserGUID'] = 'user_guid'
|
||||||
|
context.cookies['accessToken'] = 'access_tok'
|
||||||
|
context.cookies['accessTokenExpiresAt'] = 'access_tok_expire'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
m.post('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f',
|
||||||
|
request_headers={'Referer': ciq.LOGIN_URL, **base_headers()},
|
||||||
|
|
||||||
|
text=post_callback)
|
||||||
|
|
||||||
|
m.post('https://tconnect.tandemdiabetes.com/newlocation',
|
||||||
|
cookies={'cookie': 'value'},
|
||||||
|
headers=base_headers(),
|
||||||
|
|
||||||
|
status_code=200)
|
||||||
|
|
||||||
|
self.assertTrue(ciq.login('email@email.com', 'password'))
|
||||||
|
|
||||||
|
self.assertEqual(ciq.userGuid, 'user_guid')
|
||||||
|
self.assertEqual(ciq.accessToken, 'access_tok')
|
||||||
|
self.assertEqual(ciq.accessTokenExpiresAt, 'access_tok_expire')
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_invalid_credentials(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 '<html><body>...</body></html>'
|
||||||
|
|
||||||
|
m.post('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f',
|
||||||
|
request_headers={'Referer': ciq.LOGIN_URL, **base_headers()},
|
||||||
|
|
||||||
|
text=post_callback)
|
||||||
|
|
||||||
|
self.assertRaises(ApiLoginException, 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):
|
def fake_get_with_http_code(self, http_code, expected_endpoint, num_times):
|
||||||
tries = 0
|
tries = 0
|
||||||
def fake_get(endpoint, query):
|
def fake_get(endpoint, query):
|
||||||
@@ -42,7 +150,7 @@ class TestControlIQApi(unittest.TestCase):
|
|||||||
|
|
||||||
ciq._get = self.fake_get_with_http_code(500, "therapytimeline/users/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 2)
|
ciq._get = self.fake_get_with_http_code(500, "therapytimeline/users/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 2)
|
||||||
|
|
||||||
ex = self.assertRaises(ApiException, ciq.therapy_timeline, '2021-04-01', '2021-04-02')
|
self.assertRaises(ApiException, ciq.therapy_timeline, '2021-04-01', '2021-04-02')
|
||||||
|
|
||||||
def test_therapy_timeline_triggers_relogin_after_single_http_401(self):
|
def test_therapy_timeline_triggers_relogin_after_single_http_401(self):
|
||||||
ciq = ControlIQApi()
|
ciq = ControlIQApi()
|
||||||
@@ -84,7 +192,7 @@ class TestControlIQApi(unittest.TestCase):
|
|||||||
|
|
||||||
ciq._get = self.fake_get_with_http_code(401, "therapytimeline/users/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 2)
|
ciq._get = self.fake_get_with_http_code(401, "therapytimeline/users/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 2)
|
||||||
|
|
||||||
ex = self.assertRaises(ApiException, ciq.therapy_timeline, '2021-04-01', '2021-04-02')
|
self.assertRaises(ApiException, ciq.therapy_timeline, '2021-04-01', '2021-04-02')
|
||||||
|
|
||||||
self.assertListEqual(hit_login, [
|
self.assertListEqual(hit_login, [
|
||||||
('email', 'password')
|
('email', 'password')
|
||||||
|
|||||||
Reference in New Issue
Block a user