Skip to content

Commit 5dfd1c7

Browse files
authored
Add new exception type for when an endpoint isn't available in the plan (bachya#170)
current plan
1 parent 4a3e9d4 commit 5dfd1c7

6 files changed

Lines changed: 51 additions & 47 deletions

File tree

‎docs/advanced.rst‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,3 @@ At any point, the "dirtiness" of the token can be checked:
6060
# Once the dirtiness is confirmed, the dirty bit resets:
6161
simplisafe.refresh_token_dirty
6262
# >>> False
63-
64-
Errors/Exceptions
65-
*****************
66-
67-
``simplipy`` exposes several useful error types:
68-
69-
* :meth:`SimplipyError <simplipy.errors.SimplipyError>`: a base error that all other
70-
``simplipy`` errors inherit from
71-
* :meth:`RequestError <simplipy.errors.RequestError>`: an error
72-
related to an invalid username/password combo
73-
* :meth:`PinError <simplipy.errors.PinError>`: an error related to an invalid PIN
74-
operation, such as attempting to delete a reserved PIN (e.g., "master"), adding too
75-
many PINs, etc.
76-
* :meth:`RequestError <simplipy.errors.RequestError>`: an error related to HTTP requests
77-
that return something other than a ``200`` response code

‎simplipy/api.py‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from aiohttp.client_exceptions import ClientError
1111

1212
from simplipy.errors import (
13+
EndpointUnavailable,
1314
InvalidCredentialsError,
1415
PendingAuthorizationError,
1516
RequestError,
@@ -302,13 +303,14 @@ async def request( # pylint: disable=too-many-branches
302303
try:
303304
resp.raise_for_status()
304305
except ClientError as err:
305-
if isinstance(data, dict) and data.get("error") == "mfa_required":
306-
# In the case of an MFA token, SimpliSafe's API will return a 401,
307-
# but will include the MFA token in the response body. This
308-
# somewhat-kludgy check handles that case by ignoring the 401 and
309-
# returning the body for use:
306+
if data.get("error") == "mfa_required":
310307
return data
311308

309+
if data.get("type") == "NoRemoteManagement":
310+
raise EndpointUnavailable(
311+
f"Endpoint unavailable in plan: {endpoint}"
312+
) from None
313+
312314
if "401" in str(err):
313315
if self._actively_refreshing:
314316
raise InvalidCredentialsError(
@@ -320,11 +322,6 @@ async def request( # pylint: disable=too-many-branches
320322
return await self.request(method, endpoint, **kwargs)
321323
raise InvalidCredentialsError("Invalid username/password") from None
322324

323-
if "403" in str(err):
324-
raise InvalidCredentialsError(
325-
f"User does not have permission to access {endpoint}"
326-
) from None
327-
328325
raise RequestError(
329326
f"There was an error while requesting /{endpoint}: {err}"
330327
) from None

‎simplipy/errors.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ class SimplipyError(Exception):
77
pass
88

99

10+
class EndpointUnavailable(SimplipyError):
11+
"""An error related to accessing an endpoint that isn't available in the plan."""
12+
13+
pass
14+
15+
1016
class InvalidCredentialsError(SimplipyError):
1117
"""An error related to invalid credentials."""
1218

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "NoRemoteManagement",
3+
"message": "Subscription does not support remote management",
4+
"code": "078",
5+
"statusCode": 403,
6+
"props": {}
7+
}

‎tests/system/test_v3.py‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
import pytz
88

99
from simplipy import API
10-
from simplipy.errors import InvalidCredentialsError, PinError, SimplipyError
10+
from simplipy.errors import (
11+
EndpointUnavailable,
12+
InvalidCredentialsError,
13+
PinError,
14+
SimplipyError,
15+
)
1116
from simplipy.system import SystemStates
1217
from simplipy.system.v3 import VOLUME_HIGH, VOLUME_MEDIUM
1318

@@ -785,6 +790,31 @@ async def test_system_notifications(aresponses, v3_server, v3_subscriptions_resp
785790
assert notification1.link_label == "More Info"
786791

787792

793+
@pytest.mark.asyncio
794+
async def test_unavailable_endpoint(aresponses, v3_server):
795+
"""Test that an unavailable endpoint logs a message."""
796+
async with v3_server:
797+
v3_server.add(
798+
"api.simplisafe.com",
799+
f"/v1/ss3/subscriptions/{TEST_SUBSCRIPTION_ID}/settings/normal",
800+
"get",
801+
aresponses.Response(
802+
text=load_fixture("unavailable_endpoint_response.json"), status=403
803+
),
804+
)
805+
806+
async with aiohttp.ClientSession() as session:
807+
simplisafe = await API.login_via_credentials(
808+
TEST_EMAIL, TEST_PASSWORD, client_id=TEST_CLIENT_ID, session=session
809+
)
810+
811+
systems = await simplisafe.get_systems()
812+
system = systems[TEST_SYSTEM_ID]
813+
814+
with pytest.raises(EndpointUnavailable):
815+
await system.update(include_system=False, include_entities=False)
816+
817+
788818
@pytest.mark.asyncio
789819
@pytest.mark.parametrize(
790820
"v3_subscriptions_response",

‎tests/test_api.py‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Define tests for the System object."""
22
# pylint: disable=protected-access
33
from datetime import datetime, timedelta
4-
import logging
54

65
import aiohttp
76
from aresponses import ResponsesMockServer
@@ -191,26 +190,6 @@ async def test_expired_token_refresh(aresponses, v2_server):
191190
await simplisafe.request("post", "api/token")
192191

193192

194-
@pytest.mark.asyncio
195-
async def test_invalid_credentials(aresponses, v2_server):
196-
"""Test that invalid credentials throw the correct exception."""
197-
async with ResponsesMockServer() as v2_server:
198-
v2_server.add(
199-
"api.simplisafe.com",
200-
"/v1/api/token",
201-
"post",
202-
aresponses.Response(
203-
text=load_fixture("invalid_credentials_response.json"), status=403,
204-
),
205-
)
206-
207-
async with aiohttp.ClientSession() as session:
208-
with pytest.raises(InvalidCredentialsError):
209-
await API.login_via_credentials(
210-
TEST_EMAIL, TEST_PASSWORD, client_id=TEST_CLIENT_ID, session=session
211-
)
212-
213-
214193
@pytest.mark.asyncio
215194
async def test_mfa(aresponses):
216195
"""Test that a successful MFA flow throws the correct exception."""

0 commit comments

Comments
 (0)