Add cmdline/CLI tests (#258)
This commit is contained in:
parent
1090393774
commit
9588c0d448
4 changed files with 326 additions and 38 deletions
|
@ -8,12 +8,18 @@ from __future__ import print_function
|
||||||
import argparse
|
import argparse
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
import six
|
||||||
|
|
||||||
from click import confirm, prompt
|
from click import confirm
|
||||||
|
|
||||||
import pyicloud
|
from pyicloud import PyiCloudService
|
||||||
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
input = raw_input # pylint: disable=redefined-builtin,invalid-name,undefined-variable
|
||||||
|
else:
|
||||||
|
input = input # pylint: disable=bad-option-value,self-assigning-variable,invalid-name
|
||||||
|
|
||||||
DEVICE_ERROR = (
|
DEVICE_ERROR = (
|
||||||
"Please use the --device switch to indicate which device to use."
|
"Please use the --device switch to indicate which device to use."
|
||||||
|
@ -21,19 +27,20 @@ DEVICE_ERROR = (
|
||||||
|
|
||||||
|
|
||||||
def create_pickled_data(idevice, filename):
|
def create_pickled_data(idevice, filename):
|
||||||
"""This helper will output the idevice to a pickled file named
|
"""
|
||||||
|
This helper will output the idevice to a pickled file named
|
||||||
after the passed filename.
|
after the passed filename.
|
||||||
|
|
||||||
This allows the data to be used without resorting to screen / pipe
|
This allows the data to be used without resorting to screen / pipe
|
||||||
scrapping."""
|
scrapping.
|
||||||
location = filename
|
"""
|
||||||
pickle_file = open(location, 'wb')
|
pickle_file = open(filename, 'wb')
|
||||||
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
pickle_file.close()
|
pickle_file.close()
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
"""Main commandline entrypoint"""
|
"""Main commandline entrypoint."""
|
||||||
if args is None:
|
if args is None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
@ -194,7 +201,7 @@ def main(args=None):
|
||||||
parser.error('No password supplied')
|
parser.error('No password supplied')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api = pyicloud.PyiCloudService(
|
api = PyiCloudService(
|
||||||
username.strip(),
|
username.strip(),
|
||||||
password.strip()
|
password.strip()
|
||||||
)
|
)
|
||||||
|
@ -206,8 +213,8 @@ def main(args=None):
|
||||||
utils.store_password_in_keyring(username, password)
|
utils.store_password_in_keyring(username, password)
|
||||||
|
|
||||||
if api.requires_2sa:
|
if api.requires_2sa:
|
||||||
print("Two-step authentication required.",
|
print("\nTwo-step authentication required.",
|
||||||
"Your trusted devices are:")
|
"\nYour trusted devices are:")
|
||||||
|
|
||||||
devices = api.trusted_devices
|
devices = api.trusted_devices
|
||||||
for i, device in enumerate(devices):
|
for i, device in enumerate(devices):
|
||||||
|
@ -216,20 +223,22 @@ def main(args=None):
|
||||||
'deviceName',
|
'deviceName',
|
||||||
"SMS to %s" % device.get('phoneNumber'))))
|
"SMS to %s" % device.get('phoneNumber'))))
|
||||||
|
|
||||||
device = prompt('Which device would you like to use?',
|
print('\nWhich device would you like to use?')
|
||||||
default=0)
|
device = int(input('(number) --> '))
|
||||||
device = devices[device]
|
device = devices[device]
|
||||||
if not api.send_verification_code(device):
|
if not api.send_verification_code(device):
|
||||||
print("Failed to send verification code")
|
print("Failed to send verification code")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
code = prompt('Please enter validation code')
|
print('\nPlease enter validation code')
|
||||||
|
code = input('(string) --> ')
|
||||||
if not api.validate_verification_code(device, code):
|
if not api.validate_verification_code(device, code):
|
||||||
print("Failed to verify verification code")
|
print("Failed to verify verification code")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
print('')
|
||||||
break
|
break
|
||||||
except pyicloud.exceptions.PyiCloudFailedLoginException:
|
except PyiCloudFailedLoginException:
|
||||||
# If they have a stored password; we just used it and
|
# If they have a stored password; we just used it and
|
||||||
# it did not work; let's delete it if there is one.
|
# it did not work; let's delete it if there is one.
|
||||||
if utils.password_exists_in_keyring(username):
|
if utils.password_exists_in_keyring(username):
|
||||||
|
@ -344,6 +353,7 @@ def main(args=None):
|
||||||
DEVICE_ERROR
|
DEVICE_ERROR
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1 +1,190 @@
|
||||||
"""Library tests."""
|
"""Library tests."""
|
||||||
|
|
||||||
|
from pyicloud import base
|
||||||
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
|
from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager, AppleDevice
|
||||||
|
|
||||||
|
|
||||||
|
AUTHENTICATED_USER = "authenticated_user"
|
||||||
|
REQUIRES_2SA_USER = "requires_2sa_user"
|
||||||
|
VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2SA_USER]
|
||||||
|
|
||||||
|
|
||||||
|
class PyiCloudServiceMock(base.PyiCloudService):
|
||||||
|
"""Mocked PyiCloudService."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
apple_id,
|
||||||
|
password=None,
|
||||||
|
cookie_directory=None,
|
||||||
|
verify=True,
|
||||||
|
client_id=None,
|
||||||
|
with_family=True
|
||||||
|
):
|
||||||
|
base.PyiCloudService.__init__(self, apple_id, password, cookie_directory, verify, client_id, with_family)
|
||||||
|
base.FindMyiPhoneServiceManager = FindMyiPhoneServiceManagerMock
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
if not self.user.get("apple_id") or self.user.get("apple_id") not in VALID_USERS:
|
||||||
|
raise PyiCloudFailedLoginException("Invalid email/password combination.", None)
|
||||||
|
if not self.user.get("password") or self.user.get("password") != "valid_pass":
|
||||||
|
raise PyiCloudFailedLoginException("Invalid email/password combination.", None)
|
||||||
|
|
||||||
|
self.params.update({'dsid': 'ID'})
|
||||||
|
self._webservices = {
|
||||||
|
'account': {
|
||||||
|
'url': 'account_url',
|
||||||
|
},
|
||||||
|
'findme': {
|
||||||
|
'url': 'findme_url',
|
||||||
|
},
|
||||||
|
'calendar': {
|
||||||
|
'url': 'calendar_url',
|
||||||
|
},
|
||||||
|
'contacts': {
|
||||||
|
'url': 'contacts_url',
|
||||||
|
},
|
||||||
|
'reminders': {
|
||||||
|
'url': 'reminders_url',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires_2sa(self):
|
||||||
|
return self.user["apple_id"] is REQUIRES_2SA_USER
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trusted_devices(self):
|
||||||
|
return [
|
||||||
|
{"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"}
|
||||||
|
]
|
||||||
|
|
||||||
|
def send_verification_code(self, device):
|
||||||
|
return device
|
||||||
|
|
||||||
|
def validate_verification_code(self, device, code):
|
||||||
|
if not device or code != 0:
|
||||||
|
self.user["apple_id"] = AUTHENTICATED_USER
|
||||||
|
self.authenticate()
|
||||||
|
return not self.requires_2sa
|
||||||
|
|
||||||
|
|
||||||
|
IPHONE_DEVICE_ID = "X1x/X&x="
|
||||||
|
IPHONE_DEVICE = AppleDevice(
|
||||||
|
{
|
||||||
|
"msg": {
|
||||||
|
"strobe": False,
|
||||||
|
"userText": False,
|
||||||
|
"playSound": True,
|
||||||
|
"vibrate": True,
|
||||||
|
"createTimestamp": 1568031021347,
|
||||||
|
"statusCode": "200"
|
||||||
|
},
|
||||||
|
"canWipeAfterLock": True,
|
||||||
|
"baUUID": "",
|
||||||
|
"wipeInProgress": False,
|
||||||
|
"lostModeEnabled": False,
|
||||||
|
"activationLocked": True,
|
||||||
|
"passcodeLength": 6,
|
||||||
|
"deviceStatus": "200",
|
||||||
|
"deviceColor": "1-6-0",
|
||||||
|
"features": {
|
||||||
|
"MSG": True,
|
||||||
|
"LOC": True,
|
||||||
|
"LLC": False,
|
||||||
|
"CLK": False,
|
||||||
|
"TEU": True,
|
||||||
|
"LMG": False,
|
||||||
|
"SND": True,
|
||||||
|
"CLT": False,
|
||||||
|
"LKL": False,
|
||||||
|
"SVP": False,
|
||||||
|
"LST": True,
|
||||||
|
"LKM": False,
|
||||||
|
"WMG": True,
|
||||||
|
"SPN": False,
|
||||||
|
"XRM": False,
|
||||||
|
"PIN": False,
|
||||||
|
"LCK": True,
|
||||||
|
"REM": False,
|
||||||
|
"MCS": False,
|
||||||
|
"CWP": False,
|
||||||
|
"KEY": False,
|
||||||
|
"KPD": False,
|
||||||
|
"WIP": True
|
||||||
|
},
|
||||||
|
"lowPowerMode": True,
|
||||||
|
"rawDeviceModel": "iPhone11,8",
|
||||||
|
"id": IPHONE_DEVICE_ID,
|
||||||
|
"remoteLock": None,
|
||||||
|
"isLocating": True,
|
||||||
|
"modelDisplayName": "iPhone",
|
||||||
|
"lostTimestamp": "",
|
||||||
|
"batteryLevel": 0.47999998927116394,
|
||||||
|
"mesg": None,
|
||||||
|
"locationEnabled": True,
|
||||||
|
"lockedTimestamp": None,
|
||||||
|
"locFoundEnabled": False,
|
||||||
|
"snd": {
|
||||||
|
"createTimestamp": 1568031021347,
|
||||||
|
"statusCode": "200"
|
||||||
|
},
|
||||||
|
"fmlyShare": False,
|
||||||
|
"lostDevice": {
|
||||||
|
"stopLostMode": False,
|
||||||
|
"emailUpdates": False,
|
||||||
|
"userText": True,
|
||||||
|
"sound": False,
|
||||||
|
"ownerNbr": "",
|
||||||
|
"text": "",
|
||||||
|
"createTimestamp": 1558383841233,
|
||||||
|
"statusCode": "2204"
|
||||||
|
},
|
||||||
|
"lostModeCapable": True,
|
||||||
|
"wipedTimestamp": None,
|
||||||
|
"deviceDisplayName": "iPhone XR",
|
||||||
|
"prsId": None,
|
||||||
|
"audioChannels": [],
|
||||||
|
"locationCapable": True,
|
||||||
|
"batteryStatus": "NotCharging",
|
||||||
|
"trackingInfo": None,
|
||||||
|
"name": "Quentin's iPhone",
|
||||||
|
"isMac": False,
|
||||||
|
"thisDevice": False,
|
||||||
|
"deviceClass": "iPhone",
|
||||||
|
"location": {
|
||||||
|
"isOld": False,
|
||||||
|
"isInaccurate": False,
|
||||||
|
"altitude": 0.0,
|
||||||
|
"positionType": "GPS",
|
||||||
|
"latitude": 46.012345678,
|
||||||
|
"floorLevel": 0,
|
||||||
|
"horizontalAccuracy": 12.012345678,
|
||||||
|
"locationType": "",
|
||||||
|
"timeStamp": 1568827039692,
|
||||||
|
"locationFinished": False,
|
||||||
|
"verticalAccuracy": 0.0,
|
||||||
|
"longitude": 5.012345678
|
||||||
|
},
|
||||||
|
"deviceModel": "iphoneXR-1-6-0",
|
||||||
|
"maxMsgChar": 160,
|
||||||
|
"darkWake": False,
|
||||||
|
"remoteWipe": None
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
DEVICES = {
|
||||||
|
IPHONE_DEVICE_ID: IPHONE_DEVICE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FindMyiPhoneServiceManagerMock(FindMyiPhoneServiceManager):
|
||||||
|
"""Mocked FindMyiPhoneServiceManager."""
|
||||||
|
def __init__(self, service_root, session, params, with_family=False):
|
||||||
|
FindMyiPhoneServiceManager.__init__(self, service_root, session, params, with_family)
|
||||||
|
|
||||||
|
def refresh_client(self):
|
||||||
|
self._devices = DEVICES
|
||||||
|
|
101
tests/test_cmdline.py
Normal file
101
tests/test_cmdline.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
"""Cmdline tests."""
|
||||||
|
from pyicloud import cmdline
|
||||||
|
from . import PyiCloudServiceMock, AUTHENTICATED_USER, REQUIRES_2SA_USER, DEVICES
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pickle
|
||||||
|
import pytest
|
||||||
|
from unittest import TestCase
|
||||||
|
if sys.version_info >= (3, 3):
|
||||||
|
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
|
||||||
|
else:
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
class TestCmdline(TestCase):
|
||||||
|
"""Cmdline test cases."""
|
||||||
|
main = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
cmdline.PyiCloudService = PyiCloudServiceMock
|
||||||
|
self.main = cmdline.main
|
||||||
|
|
||||||
|
def test_no_arg(self):
|
||||||
|
"""Test no args."""
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
self.main()
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
self.main(None)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
self.main([])
|
||||||
|
|
||||||
|
def test_help(self):
|
||||||
|
"""Test the help command."""
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
self.main(['--help'])
|
||||||
|
|
||||||
|
def test_username(self):
|
||||||
|
"""Test the username command."""
|
||||||
|
# No username supplied
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
self.main(['--username'])
|
||||||
|
|
||||||
|
@patch("getpass.getpass")
|
||||||
|
def test_username_password_invalid(self, mock_getpass):
|
||||||
|
"""Test username and password commands."""
|
||||||
|
# No password supplied
|
||||||
|
mock_getpass.return_value = None
|
||||||
|
with pytest.raises(SystemExit, match="2"):
|
||||||
|
self.main(['--username', 'invalid_user'])
|
||||||
|
|
||||||
|
# Bad username or password
|
||||||
|
mock_getpass.return_value = "invalid_pass"
|
||||||
|
with pytest.raises(RuntimeError, match="Bad username or password for invalid_user"):
|
||||||
|
self.main(['--username', 'invalid_user'])
|
||||||
|
|
||||||
|
# We should not use getpass for this one, but we reset the password at login fail
|
||||||
|
with pytest.raises(RuntimeError, match="Bad username or password for invalid_user"):
|
||||||
|
self.main(['--username', 'invalid_user', '--password', 'invalid_pass'])
|
||||||
|
|
||||||
|
|
||||||
|
@patch('pyicloud.cmdline.input')
|
||||||
|
def test_username_password_requires_2sa(self, mock_input):
|
||||||
|
"""Test username and password commands."""
|
||||||
|
# Valid connection for the first time
|
||||||
|
mock_input.return_value = "0"
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
self.main([
|
||||||
|
'--username', REQUIRES_2SA_USER,
|
||||||
|
'--password', 'valid_pass',
|
||||||
|
'--non-interactive',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_device_outputfile(self):
|
||||||
|
"""Test the outputfile command."""
|
||||||
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
self.main([
|
||||||
|
'--username', AUTHENTICATED_USER,
|
||||||
|
'--password', 'valid_pass',
|
||||||
|
'--non-interactive',
|
||||||
|
'--outputfile'
|
||||||
|
])
|
||||||
|
|
||||||
|
for key in DEVICES:
|
||||||
|
file_name = DEVICES[key].content['name'].strip().lower() + ".fmip_snapshot"
|
||||||
|
|
||||||
|
pickle_file = open(file_name, "rb")
|
||||||
|
assert pickle_file
|
||||||
|
|
||||||
|
contents = []
|
||||||
|
with pickle_file as opened_file:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
contents.append(pickle.load(opened_file))
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
assert contents == [DEVICES[key].content]
|
||||||
|
|
||||||
|
pickle_file.close()
|
||||||
|
os.remove(file_name)
|
|
@ -1,12 +0,0 @@
|
||||||
"""Sanity test."""
|
|
||||||
from unittest2 import TestCase
|
|
||||||
|
|
||||||
from pyicloud.cmdline import main
|
|
||||||
|
|
||||||
|
|
||||||
class SanityTestCase(TestCase):
|
|
||||||
"""Sanity test."""
|
|
||||||
def test_basic_sanity(self):
|
|
||||||
"""Sanity test."""
|
|
||||||
with self.assertRaises(SystemExit):
|
|
||||||
main(['--help'])
|
|
Loading…
Reference in a new issue