Skip to content

Commit c8888b8

Browse files
authored
Add dirtiness check for refresh token (#15)
* Add dirtiness check for refresh token * Tox updates * Revert "Tox updates" This reverts commit bf39877. * Updated Makefile
1 parent ddc2bb0 commit c8888b8

4 files changed

Lines changed: 105 additions & 8 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
coverage:
22
pipenv run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=simplipy tests
33
init:
4-
pip install --upgrade pip pipenv
4+
pip install pip==18.0 pipenv
55
pipenv lock
66
pipenv install --dev
77
lint:

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,36 @@ asyncio.get_event_loop().run_until_complete(main())
351351

352352
# Refreshing the Access Token
353353

354+
## General Notes
355+
356+
During usage, `simplipy` will automatically refresh the access token as needed.
357+
At any point, the "dirtiness" of the token can be checked:
358+
359+
```python
360+
from simplipy import API
361+
362+
363+
async def main() -> None:
364+
"""Create the aiohttp session and run."""
365+
async with ClientSession() as websession:
366+
simplisafe = API.login_via_token("<REFRESH TOKEN>", websession)
367+
systems = await simplisafe.get_systems()
368+
primary_system = systems[0]
369+
370+
# Assuming the access token was automatically refreshed:
371+
primary_system.api.refresh_token_dirty
372+
# >>> True
373+
374+
# Once the dirtiness is confirmed, the dirty bit resets:
375+
primary_system.api.refresh_token_dirty
376+
# >>> False
377+
378+
379+
asyncio.get_event_loop().run_until_complete(main())
380+
```
381+
382+
## Restarting with a Refresh Token
383+
354384
It may be desirable to re-authenticate against the SimpliSafe™ API at some
355385
point in the future (and without using a user's email and password). In that
356386
case, it is recommended that you save the `refresh_token` property somewhere;

simplipy/api.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Define a SimpliSafe account."""
2-
# pylint: disable=import-error,protected-access,unused-import
2+
# pylint: disable=import-error,protected-access,too-many-instance-attributes
3+
# pylint: disable=unused-import
34

45
from datetime import datetime, timedelta
56
from typing import List, Type, TypeVar, Union # noqa
@@ -26,14 +27,32 @@ class API:
2627

2728
def __init__(self, websession: ClientSession) -> None:
2829
"""Initialize."""
29-
self._access_token = None
30+
self._access_token = ''
3031
self._access_token_expire = None # type: Union[None, datetime]
3132
self._actively_refreshing = False
3233
self._email = None # type: Union[None, str]
3334
self._websession = websession
34-
self.refresh_token = None
35+
self._refresh_token = ''
36+
self.refresh_token_dirty = False
3537
self.user_id = None
3638

39+
@property
40+
def refresh_token(self) -> str:
41+
"""Return the current refresh_token."""
42+
if self.refresh_token_dirty:
43+
self.refresh_token_dirty = False
44+
45+
return self._refresh_token
46+
47+
@refresh_token.setter
48+
def refresh_token(self, value: str) -> None:
49+
"""Set the refresh token if it has changed."""
50+
if value == self._refresh_token:
51+
return
52+
53+
self._refresh_token = value
54+
self.refresh_token_dirty = True
55+
3756
@classmethod
3857
async def login_via_credentials(
3958
cls: Type[ApiType], email: str, password: str,
@@ -121,8 +140,7 @@ async def request(
121140
and datetime.now() >= self._access_token_expire
122141
and not self._actively_refreshing):
123142
self._actively_refreshing = True
124-
await self._refresh_access_token( # type: ignore
125-
self.refresh_token)
143+
await self._refresh_access_token(self._refresh_token)
126144

127145
url = '{0}/{1}'.format(URL_BASE, endpoint)
128146

tests/test_system.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def test_bad_request(event_loop, v2_server):
4040
@pytest.mark.asyncio
4141
async def test_expired_token_refresh(
4242
api_token_json, auth_check_json, event_loop, v2_server):
43-
"""Test that the correct exception is raised when the token is expired."""
43+
"""Test that a refresh token is used correctly."""
4444
async with v2_server:
4545
v2_server.add(
4646
'api.simplisafe.com', '/v1/api/token', 'post',
@@ -61,6 +61,34 @@ async def test_expired_token_refresh(
6161
await system.api.request('get', 'api/authCheck')
6262

6363

64+
@pytest.mark.asyncio
65+
async def test_refresh_token_dirtiness(
66+
api_token_json, auth_check_json, event_loop, v2_server):
67+
"""Test that the refresh token's dirtiness can be checked."""
68+
async with v2_server:
69+
v2_server.add(
70+
'api.simplisafe.com', '/v1/api/token', 'post',
71+
aresponses.Response(text=json.dumps(api_token_json), status=200))
72+
v2_server.add(
73+
'api.simplisafe.com', '/v1/api/authCheck', 'get',
74+
aresponses.Response(text=json.dumps(auth_check_json), status=200))
75+
v2_server.add(
76+
'api.simplisafe.com', '/v1/api/authCheck', 'get',
77+
aresponses.Response(text=json.dumps(auth_check_json), status=200))
78+
79+
async with aiohttp.ClientSession(loop=event_loop) as websession:
80+
api = await API.login_via_credentials(
81+
TEST_EMAIL, TEST_PASSWORD, websession)
82+
[system] = await api.get_systems()
83+
system.api._access_token_expire = datetime.now() - timedelta(
84+
hours=1)
85+
await system.api.request('get', 'api/authCheck')
86+
87+
assert system.api.refresh_token_dirty
88+
assert system.api.refresh_token == TEST_REFRESH_TOKEN
89+
assert not system.api.refresh_token_dirty
90+
91+
6492
@pytest.mark.asyncio
6593
async def test_get_events(events_json, event_loop, v2_server):
6694
"""Test getting events from a system."""
@@ -76,6 +104,7 @@ async def test_get_events(events_json, event_loop, v2_server):
76104
[system] = await api.get_systems()
77105

78106
events = await system.get_events(1534725051, 2)
107+
79108
assert len(events) == 2
80109

81110

@@ -109,9 +138,11 @@ async def test_get_systems_v2(
109138
credentials_api = await API.login_via_credentials(
110139
TEST_EMAIL, TEST_PASSWORD, websession)
111140
systems = await credentials_api.get_systems()
141+
112142
assert len(systems) == 1
113143

114144
primary_system = systems[0]
145+
115146
assert primary_system.serial == TEST_SYSTEM_SERIAL_NO
116147
assert primary_system.system_id == TEST_SYSTEM_ID
117148
assert primary_system.api._access_token == TEST_ACCESS_TOKEN
@@ -120,9 +151,11 @@ async def test_get_systems_v2(
120151
token_api = await API.login_via_token(
121152
TEST_REFRESH_TOKEN, websession)
122153
systems = await token_api.get_systems()
154+
123155
assert len(systems) == 1
124156

125157
primary_system = systems[0]
158+
126159
assert primary_system.serial == TEST_SYSTEM_SERIAL_NO
127160
assert primary_system.system_id == TEST_SYSTEM_ID
128161
assert primary_system.api._access_token == TEST_ACCESS_TOKEN
@@ -159,9 +192,11 @@ async def test_get_systems_v3(
159192
credentials_api = await API.login_via_credentials(
160193
TEST_EMAIL, TEST_PASSWORD, websession)
161194
systems = await credentials_api.get_systems()
195+
162196
assert len(systems) == 1
163197

164198
primary_system = systems[0]
199+
165200
assert primary_system.serial == TEST_SYSTEM_SERIAL_NO
166201
assert primary_system.system_id == TEST_SYSTEM_ID
167202
assert primary_system.api._access_token == TEST_ACCESS_TOKEN
@@ -170,9 +205,11 @@ async def test_get_systems_v3(
170205
token_api = await API.login_via_token(
171206
TEST_REFRESH_TOKEN, websession)
172207
systems = await token_api.get_systems()
208+
173209
assert len(systems) == 1
174210

175211
primary_system = systems[0]
212+
176213
assert primary_system.serial == TEST_SYSTEM_SERIAL_NO
177214
assert primary_system.system_id == TEST_SYSTEM_ID
178215
assert primary_system.api._access_token == TEST_ACCESS_TOKEN
@@ -187,6 +224,7 @@ async def test_properties_base(event_loop, v2_server):
187224
api = await API.login_via_credentials(
188225
TEST_EMAIL, TEST_PASSWORD, websession)
189226
[system] = await api.get_systems()
227+
190228
assert system.address == TEST_ADDRESS
191229
assert not system.alarm_going_off
192230
assert system.serial == TEST_SYSTEM_SERIAL_NO
@@ -232,15 +270,19 @@ async def test_set_states_v2(
232270
[system] = await api.get_systems()
233271

234272
await system.set_away()
273+
235274
assert system.state == system.SystemStates.away
236275

237276
await system.set_home()
277+
238278
assert system.state == system.SystemStates.home
239279

240280
await system.set_off()
281+
241282
assert system.state == system.SystemStates.off
242283

243284
await system.set_off()
285+
244286
assert system.state == system.SystemStates.off
245287

246288

@@ -280,15 +322,19 @@ async def test_set_states_v3(
280322
[system] = await api.get_systems()
281323

282324
await system.set_away()
325+
283326
assert system.state == system.SystemStates.away
284327

285328
await system.set_home()
329+
286330
assert system.state == system.SystemStates.home
287331

288332
await system.set_off()
333+
289334
assert system.state == system.SystemStates.off
290335

291336
await system.set_off()
337+
292338
assert system.state == system.SystemStates.off
293339

294340

@@ -306,12 +352,13 @@ async def test_unknown_initial_state(caplog, event_loop):
306352

307353
@pytest.mark.asyncio
308354
async def test_unknown_sensor_type(caplog, event_loop, v2_server):
309-
"""Test getting a new access token from a refresh token."""
355+
"""Test whether a message is logged upon finding an unknown sensor type."""
310356
async with v2_server:
311357
async with aiohttp.ClientSession(loop=event_loop) as websession:
312358
api = await API.login_via_credentials(
313359
TEST_EMAIL, TEST_PASSWORD, websession)
314360
_ = await api.get_systems() # noqa
361+
315362
assert any('Unknown' in e.message for e in caplog.records)
316363

317364

@@ -337,6 +384,7 @@ async def test_update_system_data_v2(
337384
[system] = await api.get_systems()
338385

339386
await system.update()
387+
340388
assert system.serial == TEST_SYSTEM_SERIAL_NO
341389
assert system.system_id == TEST_SYSTEM_ID
342390
assert system.api._access_token == TEST_ACCESS_TOKEN
@@ -365,6 +413,7 @@ async def test_update_system_data_v3(
365413
[system] = await api.get_systems()
366414

367415
await system.update()
416+
368417
assert system.serial == TEST_SYSTEM_SERIAL_NO
369418
assert system.system_id == TEST_SYSTEM_ID
370419
assert system.api._access_token == TEST_ACCESS_TOKEN

0 commit comments

Comments
 (0)