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 = "*"
|
||||
lxml = "*"
|
||||
python-dotenv = "*"
|
||||
requests-mock = "*"
|
||||
|
||||
[scripts]
|
||||
tconnectsync = "python3 main.py"
|
||||
test = "python3 -m unittest discover -vv"
|
||||
test = "python3 -m unittest discover -vv"
|
||||
|
||||
Generated
+9
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "071afba23de7f532f99a0611caed63951c6f7ba968ba877ad5c76c244e78577e"
|
||||
"sha256": "979feca3aba1b7f43890a94862347738158af5847959c14dc1d5f23ce5db8cdc"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -126,6 +126,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-mock": {
|
||||
"hashes": [
|
||||
"sha256:11215c6f4df72702aa357f205cf1e537cffd7392b3e787b58239bde5fb3db53b",
|
||||
"sha256:e68f46844e4cee9d447150343c9ae875f99fa8037c6dcf5f15bf1fe9ab43d226"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
|
||||
@@ -11,7 +11,7 @@ def base_headers():
|
||||
class ApiException(Exception):
|
||||
def __init__(self, status_code, text, *args, **kwargs):
|
||||
self.status_code = status_code
|
||||
super().__init__(text, *args, **kwargs)
|
||||
super().__init__('%s (HTTP %s)' % (text, status_code), *args, **kwargs)
|
||||
|
||||
class ApiLoginException(ApiException):
|
||||
pass
|
||||
@@ -25,18 +25,8 @@ class ControlIQApi:
|
||||
with requests.Session() as s:
|
||||
initial = s.get(self.LOGIN_URL, headers=base_headers())
|
||||
soup = BeautifulSoup(initial.content, features='lxml')
|
||||
data = {
|
||||
"__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)
|
||||
}
|
||||
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)
|
||||
if req.status_code != 302:
|
||||
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']
|
||||
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):
|
||||
diff = (arrow.get(self.accessTokenExpiresAt) - arrow.get())
|
||||
return (diff.seconds <= 5 * 60)
|
||||
|
||||
+111
-3
@@ -3,12 +3,120 @@
|
||||
import unittest
|
||||
import itertools
|
||||
import datetime
|
||||
import json
|
||||
import requests_mock
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
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):
|
||||
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):
|
||||
tries = 0
|
||||
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)
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
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, [
|
||||
('email', 'password')
|
||||
|
||||
Reference in New Issue
Block a user