Skip to content

Commit e893895

Browse files
authored
Test rework + add account & fmi device test (picklepete#266)
* Rework tests * Add account test * Add Find My iPhone devices test * Remove logger * Working with Python 3.4 * Make test working in more setups @patch("keyring.get_password", return_value=None) * Fix Python 2.7 ASCII * Pylint * Self reviewed
1 parent db9dfca commit e893895

9 files changed

+1868
-194
lines changed

pyicloud/base.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ class PyiCloudSession(Session):
5656

5757
def __init__(self, service):
5858
self.service = service
59-
super(PyiCloudSession, self).__init__()
59+
Session.__init__(self)
6060

61-
def request(self, *args, **kwargs): # pylint: disable=arguments-differ
61+
def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
6262

6363
# Charge logging to the right service endpoint
6464
callee = inspect.stack()[2]
@@ -67,10 +67,10 @@ def request(self, *args, **kwargs): # pylint: disable=arguments-differ
6767
if self.service.password_filter not in request_logger.filters:
6868
request_logger.addFilter(self.service.password_filter)
6969

70-
request_logger.debug("%s %s %s", args[0], args[1], kwargs.get("data", ""))
70+
request_logger.debug("%s %s %s", method, url, kwargs.get("data", ""))
7171

7272
kwargs.pop("retried", None)
73-
response = super(PyiCloudSession, self).request(*args, **kwargs)
73+
response = super(PyiCloudSession, self).request(method, url, **kwargs)
7474

7575
content_type = response.headers.get("Content-Type", "").split(";")[0]
7676
json_mimetypes = ["application/json", "text/json"]
@@ -82,7 +82,7 @@ def request(self, *args, **kwargs): # pylint: disable=arguments-differ
8282
)
8383
request_logger.warn(api_error)
8484
kwargs["retried"] = True
85-
return self.request(*args, **kwargs)
85+
return self.request(method, url, **kwargs)
8686
self._raise_error(response.status_code, response.reason)
8787

8888
if content_type not in json_mimetypes:
@@ -150,6 +150,9 @@ class PyiCloudService(object):
150150
pyicloud.iphone.location()
151151
"""
152152

153+
HOME_ENDPOINT = "https://www.icloud.com"
154+
SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1"
155+
153156
def __init__(
154157
self,
155158
apple_id,
@@ -170,10 +173,7 @@ def __init__(
170173
self.password_filter = PyiCloudPasswordFilter(password)
171174
LOGGER.addFilter(self.password_filter)
172175

173-
self._home_endpoint = "https://www.icloud.com"
174-
self._setup_endpoint = "https://setup.icloud.com/setup/ws/1"
175-
176-
self._base_login_url = "%s/login" % self._setup_endpoint
176+
self._base_login_url = "%s/login" % self.SETUP_ENDPOINT
177177

178178
if cookie_directory:
179179
self._cookie_directory = os.path.expanduser(
@@ -186,8 +186,8 @@ def __init__(
186186
self.session.verify = verify
187187
self.session.headers.update(
188188
{
189-
"Origin": self._home_endpoint,
190-
"Referer": "%s/" % self._home_endpoint,
189+
"Origin": self.HOME_ENDPOINT,
190+
"Referer": "%s/" % self.HOME_ENDPOINT,
191191
"User-Agent": "Opera/9.52 (X11; Linux i686; U; en)",
192192
}
193193
)
@@ -270,15 +270,15 @@ def requires_2sa(self):
270270
def trusted_devices(self):
271271
"""Returns devices trusted for two-step authentication."""
272272
request = self.session.get(
273-
"%s/listDevices" % self._setup_endpoint, params=self.params
273+
"%s/listDevices" % self.SETUP_ENDPOINT, params=self.params
274274
)
275275
return request.json().get("devices")
276276

277277
def send_verification_code(self, device):
278278
"""Requests that a verification code is sent to the given device."""
279279
data = json.dumps(device)
280280
request = self.session.post(
281-
"%s/sendVerificationCode" % self._setup_endpoint,
281+
"%s/sendVerificationCode" % self.SETUP_ENDPOINT,
282282
params=self.params,
283283
data=data,
284284
)
@@ -291,7 +291,7 @@ def validate_verification_code(self, device, code):
291291

292292
try:
293293
self.session.post(
294-
"%s/validateVerificationCode" % self._setup_endpoint,
294+
"%s/validateVerificationCode" % self.SETUP_ENDPOINT,
295295
params=self.params,
296296
data=data,
297297
)

tests/__init__.py

+78-171
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,87 @@
11
"""Library tests."""
2+
import json
3+
from requests import Session, Response
24

35
from pyicloud import base
46
from pyicloud.exceptions import PyiCloudFailedLoginException
57
from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager, AppleDevice
68

9+
from .const import (
10+
AUTHENTICATED_USER,
11+
REQUIRES_2SA_USER,
12+
VALID_USERS,
13+
VALID_PASSWORD,
14+
)
15+
from .const_login import (
16+
LOGIN_WORKING,
17+
LOGIN_2SA,
18+
TRUSTED_DEVICES,
19+
TRUSTED_DEVICE_1,
20+
VERIFICATION_CODE_OK,
21+
VERIFICATION_CODE_KO,
22+
)
23+
from .const_account import ACCOUNT_DEVICES_WORKING
24+
from .const_findmyiphone import FMI_FMLY_WORKING
25+
26+
27+
class ResponseMock(Response):
28+
"""Mocked Response."""
29+
30+
def __init__(self, result, status_code=200):
31+
Response.__init__(self)
32+
self.result = result
33+
self.status_code = status_code
34+
35+
@property
36+
def text(self):
37+
return json.dumps(self.result)
38+
39+
40+
class PyiCloudSessionMock(base.PyiCloudSession):
41+
"""Mocked PyiCloudSession."""
42+
43+
def request(self, method, url, **kwargs):
44+
data = json.loads(kwargs.get("data", "{}"))
45+
46+
# Login
47+
if self.service.SETUP_ENDPOINT in url:
48+
if "login" in url and method == "POST":
49+
if (
50+
data.get("apple_id") not in VALID_USERS
51+
or data.get("password") != VALID_PASSWORD
52+
):
53+
self._raise_error(None, "Unknown reason")
54+
if (
55+
data.get("apple_id") == REQUIRES_2SA_USER
56+
and data.get("password") == VALID_PASSWORD
57+
):
58+
return ResponseMock(LOGIN_2SA)
59+
return ResponseMock(LOGIN_WORKING)
60+
61+
if "listDevices" in url and method == "GET":
62+
return ResponseMock(TRUSTED_DEVICES)
63+
64+
if "sendVerificationCode" in url and method == "POST":
65+
if data == TRUSTED_DEVICE_1:
66+
return ResponseMock(VERIFICATION_CODE_OK)
67+
return ResponseMock(VERIFICATION_CODE_KO)
68+
69+
if "validateVerificationCode" in url and method == "POST":
70+
TRUSTED_DEVICE_1.update({"verificationCode": "0", "trustBrowser": True})
71+
if data == TRUSTED_DEVICE_1:
72+
self.service.user["apple_id"] = AUTHENTICATED_USER
73+
return ResponseMock(VERIFICATION_CODE_OK)
74+
self._raise_error(None, "FOUND_CODE")
775

8-
AUTHENTICATED_USER = "authenticated_user"
9-
REQUIRES_2SA_USER = "requires_2sa_user"
10-
VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2SA_USER]
76+
# Account
77+
if "device/getDevices" in url and method == "GET":
78+
return ResponseMock(ACCOUNT_DEVICES_WORKING)
79+
80+
# Find My iPhone
81+
if "fmi" in url and method == "POST":
82+
return ResponseMock(FMI_FMLY_WORKING)
83+
84+
return None
1185

1286

1387
class PyiCloudServiceMock(base.PyiCloudService):
@@ -22,174 +96,7 @@ def __init__(
2296
client_id=None,
2397
with_family=True,
2498
):
99+
base.PyiCloudSession = PyiCloudSessionMock
25100
base.PyiCloudService.__init__(
26101
self, apple_id, password, cookie_directory, verify, client_id, with_family
27102
)
28-
base.FindMyiPhoneServiceManager = FindMyiPhoneServiceManagerMock
29-
30-
def authenticate(self):
31-
if (
32-
not self.user.get("apple_id")
33-
or self.user.get("apple_id") not in VALID_USERS
34-
):
35-
raise PyiCloudFailedLoginException(
36-
"Invalid email/password combination.", None
37-
)
38-
if not self.user.get("password") or self.user.get("password") != "valid_pass":
39-
raise PyiCloudFailedLoginException(
40-
"Invalid email/password combination.", None
41-
)
42-
43-
self.params.update({"dsid": "ID"})
44-
self._webservices = {
45-
"account": {"url": "account_url",},
46-
"findme": {"url": "findme_url",},
47-
"calendar": {"url": "calendar_url",},
48-
"contacts": {"url": "contacts_url",},
49-
"reminders": {"url": "reminders_url",},
50-
}
51-
52-
@property
53-
def requires_2sa(self):
54-
return self.user["apple_id"] is REQUIRES_2SA_USER
55-
56-
@property
57-
def trusted_devices(self):
58-
return [
59-
{
60-
"deviceType": "SMS",
61-
"areaCode": "",
62-
"phoneNumber": "*******58",
63-
"deviceId": "1",
64-
}
65-
]
66-
67-
def send_verification_code(self, device):
68-
return device
69-
70-
def validate_verification_code(self, device, code):
71-
if not device or code != 0:
72-
self.user["apple_id"] = AUTHENTICATED_USER
73-
self.authenticate()
74-
return not self.requires_2sa
75-
76-
77-
IPHONE_DEVICE_ID = "X1x/X&x="
78-
IPHONE_DEVICE = AppleDevice(
79-
{
80-
"msg": {
81-
"strobe": False,
82-
"userText": False,
83-
"playSound": True,
84-
"vibrate": True,
85-
"createTimestamp": 1568031021347,
86-
"statusCode": "200",
87-
},
88-
"canWipeAfterLock": True,
89-
"baUUID": "",
90-
"wipeInProgress": False,
91-
"lostModeEnabled": False,
92-
"activationLocked": True,
93-
"passcodeLength": 6,
94-
"deviceStatus": "200",
95-
"deviceColor": "1-6-0",
96-
"features": {
97-
"MSG": True,
98-
"LOC": True,
99-
"LLC": False,
100-
"CLK": False,
101-
"TEU": True,
102-
"LMG": False,
103-
"SND": True,
104-
"CLT": False,
105-
"LKL": False,
106-
"SVP": False,
107-
"LST": True,
108-
"LKM": False,
109-
"WMG": True,
110-
"SPN": False,
111-
"XRM": False,
112-
"PIN": False,
113-
"LCK": True,
114-
"REM": False,
115-
"MCS": False,
116-
"CWP": False,
117-
"KEY": False,
118-
"KPD": False,
119-
"WIP": True,
120-
},
121-
"lowPowerMode": True,
122-
"rawDeviceModel": "iPhone11,8",
123-
"id": IPHONE_DEVICE_ID,
124-
"remoteLock": None,
125-
"isLocating": True,
126-
"modelDisplayName": "iPhone",
127-
"lostTimestamp": "",
128-
"batteryLevel": 0.47999998927116394,
129-
"mesg": None,
130-
"locationEnabled": True,
131-
"lockedTimestamp": None,
132-
"locFoundEnabled": False,
133-
"snd": {"createTimestamp": 1568031021347, "statusCode": "200"},
134-
"fmlyShare": False,
135-
"lostDevice": {
136-
"stopLostMode": False,
137-
"emailUpdates": False,
138-
"userText": True,
139-
"sound": False,
140-
"ownerNbr": "",
141-
"text": "",
142-
"createTimestamp": 1558383841233,
143-
"statusCode": "2204",
144-
},
145-
"lostModeCapable": True,
146-
"wipedTimestamp": None,
147-
"deviceDisplayName": "iPhone XR",
148-
"prsId": None,
149-
"audioChannels": [],
150-
"locationCapable": True,
151-
"batteryStatus": "NotCharging",
152-
"trackingInfo": None,
153-
"name": "Quentin's iPhone",
154-
"isMac": False,
155-
"thisDevice": False,
156-
"deviceClass": "iPhone",
157-
"location": {
158-
"isOld": False,
159-
"isInaccurate": False,
160-
"altitude": 0.0,
161-
"positionType": "GPS",
162-
"latitude": 46.012345678,
163-
"floorLevel": 0,
164-
"horizontalAccuracy": 12.012345678,
165-
"locationType": "",
166-
"timeStamp": 1568827039692,
167-
"locationFinished": False,
168-
"verticalAccuracy": 0.0,
169-
"longitude": 5.012345678,
170-
},
171-
"deviceModel": "iphoneXR-1-6-0",
172-
"maxMsgChar": 160,
173-
"darkWake": False,
174-
"remoteWipe": None,
175-
},
176-
None,
177-
None,
178-
None,
179-
)
180-
181-
DEVICES = {
182-
IPHONE_DEVICE_ID: IPHONE_DEVICE,
183-
}
184-
185-
186-
class FindMyiPhoneServiceManagerMock(FindMyiPhoneServiceManager):
187-
"""Mocked FindMyiPhoneServiceManager."""
188-
189-
def __init__(self, service_root, session, params, with_family=False):
190-
FindMyiPhoneServiceManager.__init__(
191-
self, service_root, session, params, with_family
192-
)
193-
194-
def refresh_client(self):
195-
self._devices = DEVICES

tests/const.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Test constants."""
2+
from .const_login import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL
3+
4+
# Base
5+
AUTHENTICATED_USER = PRIMARY_EMAIL
6+
REQUIRES_2SA_USER = "requires_2sa_user"
7+
VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2SA_USER, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL]
8+
VALID_PASSWORD = "valid_password"
9+
10+
CLIENT_ID = "client_id"

0 commit comments

Comments
 (0)