# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # Copyright (c) 2015-2018 Digi International Inc. import copy import datetime import unittest from dateutil.tz import tzutc from devicecloud import DeviceCloudHttpException from devicecloud.devicecore import dev_mac, group_id from devicecloud.test.unit.test_utilities import HttpTestBase import httpretty from devicecloud.devicecore import ADD_GROUP_TEMPLATE, TAGS_TEMPLATE import six import mock from xml.sax.saxutils import escape EXAMPLE_GET_DEVICES = { "resultTotalRows": "2", "requestedStartRow": "0", "resultSize": "2", "requestedSize": "1000", "remainingSize": "0", "items": [ { "id": { "devId": "702077", "devVersion": "6" }, "devRecordStartDate": "2013-02-28T19:54:00.000Z", "devMac": "00:40:9D:58:17:5B", "devCellularModemId": "354374042391400", "devConnectwareId": "00000000-00000000-00409DFF-FF58175B", "cstId": "1872", "grpId": "2331", "devEffectiveStartDate": "2013-02-28T19:53:00.000Z", "devTerminated": "false", "dvVendorId": "4261412864", "dpDeviceType": "ConnectPort X5 R", "dpFirmwareLevel": "34537482", "dpFirmwareLevelDesc": "2.15.0.10", "dpRestrictedStatus": "0", "dpLastKnownIp": "10.35.1.107", "dpGlobalIp": "204.182.3.237", "dpConnectionStatus": "0", "dpLastConnectTime": "2013-04-08T04:01:20.633Z", "dpContact": "", "dpDescription": "", "dpLocation": "", "dpMapLat": "34.964465", "dpMapLong": "40.268198", "dpServerId": "", "dpZigbeeCapabilities": "0", "dpCapabilities": "6707", "grpPath": "", "dpLastDisconnectTime": "2013-04-16T19:46:06.557Z" }, { "id": { "devId": "714038", "devVersion": "7" }, "devRecordStartDate": "2013-07-16T18:05:00.000Z", "devMac": "00:1d:09:2b:7d:8c", "devConnectwareId": "00000000-00000000-001D09FF-FF2B7D8C", "cstId": "1872", "grpId": "2331", "devEffectiveStartDate": "2013-07-16T18:05:00.000Z", "devTerminated": "false", "dvVendorId": "50331982", "dpDeviceType": "IntelligentSystem", "dpFirmwareLevel": "0", "dpRestrictedStatus": "0", "dpLastKnownIp": "10.35.1.113", "dpGlobalIp": "204.182.3.238", "dpConnectionStatus": "0", "dpLastConnectTime": "2013-07-24T00:40:20.363Z", "dpServerId": "", "dpCapabilities": "66114", "grpPath": "", "dpLastDisconnectTime": "2013-07-24T00:40:36.537Z" } ] } GET_DEVICES_PAGE1 = """\ { "resultTotalRows": "2", "requestedStartRow": "0", "resultSize": "1", "requestedSize": "1", "remainingSize": "1", "items": [ {"id": {"devId": "702077","devVersion": "6"},"devRecordStartDate": "2013-02-28T19:54:00.000Z","devMac": "00:40:9D:58:17:5B","devCellularModemId": "354374042391400","devConnectwareId": "00000000-00000000-00409DFF-FF58175B","cstId": "1872","grpId": "2331","devEffectiveStartDate": "2013-02-28T19:53:00.000Z","devTerminated": "false","dvVendorId": "4261412864","dpDeviceType": "ConnectPort X5 R","dpFirmwareLevel": "34537482","dpFirmwareLevelDesc": "2.15.0.10","dpRestrictedStatus": "0","dpLastKnownIp": "10.35.1.107","dpGlobalIp": "204.182.3.237","dpConnectionStatus": "0","dpLastConnectTime": "2013-04-08T04:01:20.633Z","dpContact": "","dpDescription": "","dpLocation": "","dpMapLat": "34.964465","dpMapLong": "40.268198","dpServerId": "","dpZigbeeCapabilities": "0","dpCapabilities": "6707","grpPath": "","dpLastDisconnectTime": "2013-04-16T19:46:06.557Z"} ] } """ GET_DEVICES_PAGE2 = """\ { "resultTotalRows": "2", "requestedStartRow": "1", "resultSize": "1", "requestedSize": "1", "remainingSize": "0", "items": [ {"id": {"devId": "702078","devVersion": "6"},"devRecordStartDate": "2013-02-28T19:54:00.000Z","devMac": "00:40:9D:58:17:5B","devCellularModemId": "354374042391400","devConnectwareId": "00000000-00000000-00409DFF-FF58175B","cstId": "1872","grpId": "2331","devEffectiveStartDate": "2013-02-28T19:53:00.000Z","devTerminated": "false","dvVendorId": "4261412864","dpDeviceType": "ConnectPort X5 R","dpFirmwareLevel": "34537482","dpFirmwareLevelDesc": "2.15.0.10","dpRestrictedStatus": "0","dpLastKnownIp": "10.35.1.107","dpGlobalIp": "204.182.3.237","dpConnectionStatus": "0","dpLastConnectTime": "2013-04-08T04:01:20.633Z","dpContact": "","dpDescription": "","dpLocation": "","dpMapLat": "34.964465","dpMapLong": "40.268198","dpServerId": "","dpZigbeeCapabilities": "0","dpCapabilities": "6707","grpPath": "","dpLastDisconnectTime": "2013-04-16T19:46:06.557Z"} ] } """ EXAMPLE_GET_GROUPS = """\ { "resultTotalRows": "2", "requestedStartRow": "0", "resultSize": "2", "requestedSize": "1000", "remainingSize": "0", "items": [ { "grpId": "11817", "grpName": "7603_Digi", "grpDescription": "7603_Digi root group", "grpPath": "\/7603_Digi\/", "grpParentId": "1"}, { "grpId": "13542", "grpName": "Demo", "grpPath": "\/7603_Digi\/Demo\/", "grpParentId": "11817"} ] } """ EXAMPLE_GET_GROUPS_EXTENDED = """\ { "resultTotalRows": "4", "requestedStartRow": "0", "resultSize": "4", "requestedSize": "1000", "remainingSize": "0", "items": [ { "grpId": "11817", "grpName": "7603_Digi", "grpDescription": "7603_Digi root group", "grpPath": "\/7603_Digi\/", "grpParentId": "1"}, { "grpId": "13542", "grpName": "Demo", "grpPath": "\/7603_Digi\/Demo\/", "grpParentId": "11817"}, { "grpId": "13544", "grpName": "SubDir2", "grpPath": "\/7603_Digi\/Demo\/SubDir2\/", "grpParentId": "13542"}, { "grpId": "13545", "grpName": "Another Second Level", "grpDescription": "Another Second Level", "grpPath": "\/7603_Digi\/Another Second Level\/", "grpParentId": "11817"} ] } """ PROVISION_SUCCESS_RESPONSE1 = """\ <?xml version="1.0" encoding="ISO-8859-1"?> <result> <location>DeviceCore/946246/0</location> </result> """ PROVISION_MULTIPLE_SUCCESS_RESPONSE1 = """\ <?xml version="1.0" encoding="ISO-8859-1"?> <result> <location>DeviceCore/1397876/0</location> <location>DeviceCore/946246/0</location> </result> """ PROVISION_ERROR1 = """\ <?xml version="1.0" encoding="ISO-8859-1"?> <result> <error>The device 00000000-00000000-BC5FF4FF-FFF7908A is already provisioned.</error> </result> """ PROVISION_MIXED_RESULT_RESPONSE = """\ <?xml version="1.0" encoding="ISO-8859-1"?> <result> <location>DeviceCore/1397876/0</location> <error>The device 00000000-00000000-D48564FF-FF9D4FEE is already provisioned.</error> </result> """ class TestDeviceCoreGroups(HttpTestBase): def test_get_groups(self): self.prepare_response("GET", "/ws/Group", EXAMPLE_GET_GROUPS) it = self.dc.devicecore.get_groups() grp = six.next(it) self.assertEqual(grp.is_root(), True) self.assertEqual(grp.get_id(), "11817") self.assertEqual(grp.get_name(), "7603_Digi") self.assertEqual(grp.get_description(), "7603_Digi root group") self.assertEqual(grp.get_path(), "/7603_Digi/") self.assertEqual(grp.get_parent_id(), "1") grp = six.next(it) self.assertEqual(grp.is_root(), False) self.assertEqual(grp.get_id(), "13542") self.assertEqual(grp.get_name(), "Demo") self.assertEqual(grp.get_description(), "") self.assertEqual(grp.get_path(), "/7603_Digi/Demo/") self.assertEqual(grp.get_parent_id(), "11817") def test_repr_and_tree_print(self): self.prepare_response("GET", "/ws/Group", EXAMPLE_GET_GROUPS_EXTENDED) fobj = six.StringIO() root = self.dc.devicecore.get_group_tree_root() root.print_subtree(fobj) # the order of the traversal can vary, so just assert on the length if six.PY2: self.assertEqual(len(fobj.getvalue()), 489) elif six.PY3: self.assertEqual(len(fobj.getvalue()), 471) # no u'' on repr for strings def test_get_groups_condition(self): self.prepare_response("GET", "/ws/Group", EXAMPLE_GET_GROUPS) list(self.dc.devicecore.get_groups(group_id == "123")) params = self._get_last_request_params() self.assertEqual(params["condition"], "grpId='123'") class TestDeviceCoreProvisioning(HttpTestBase): def test_provision_one_simple_device_id(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_SUCCESS_RESPONSE1, status=207) res = self.dc.devicecore.provision_device(device_id='00000000-00000000-0000DEFF-FFADBEEFF') req = self._get_last_request() self.assertEqual(req.body, six.b( "<list>" "<DeviceCore>" "<devConnectwareId>00000000-00000000-0000DEFF-FFADBEEFF</devConnectwareId>" "</DeviceCore>" "</list>")) self.assertDictEqual(res, {"error": False, "error_msg": None, "location": "DeviceCore/946246/0"}) def test_provision_one_simple_mac(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_SUCCESS_RESPONSE1, status=207) res = self.dc.devicecore.provision_device(mac_address="DE:AD:BE:EF:00:00") req = self._get_last_request() self.assertEqual(req.body, six.b( "<list>" "<DeviceCore>" "<devMac>DE:AD:BE:EF:00:00</devMac>" "</DeviceCore>" "</list>")) self.assertDictEqual(res, {"error": False, "error_msg": None, "location": "DeviceCore/946246/0"}) def test_provision_imei(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_SUCCESS_RESPONSE1, status=207) res = self.dc.devicecore.provision_device(imei="990000862471854") req = self._get_last_request() self.assertEqual(req.body, six.b( "<list>" "<DeviceCore>" "<devCellularModemId>990000862471854</devCellularModemId>" "</DeviceCore>" "</list>")) self.assertDictEqual(res, {"error": False, "error_msg": None, "location": "DeviceCore/946246/0"}) def test_provision_all_the_fixins(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_SUCCESS_RESPONSE1, status=207) res = self.dc.devicecore.provision_device( mac_address="DE:AD:BE:EF:00:00", group_path="/group/path", metadata="Sweet, sweet metadata", map_lat=44.9807496, map_long=-93.1397815, contact="Saint Paul Parks Department", description="Buried Treasure", ) req = self._get_last_request() self.assertEqual(req.body, six.b( '<list>' '<DeviceCore>' '<devMac>DE:AD:BE:EF:00:00</devMac>' '<grpPath>/group/path</grpPath>' '<dpUserMetaData>Sweet, sweet metadata</dpUserMetaData>' '<dpMapLong>-93.1397815</dpMapLong>' '<dpMapLat>44.9807496</dpMapLat>' '<dpContact>Saint Paul Parks Department</dpContact>' '<dpDescription>Buried Treasure</dpDescription>' '</DeviceCore>' '</list>')) self.assertDictEqual(res, {"error": False, "error_msg": None, "location": "DeviceCore/946246/0"}) def test_provision_multiple_simple(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_MULTIPLE_SUCCESS_RESPONSE1, status=207) res = self.dc.devicecore.provision_devices([ {'device_id': "00000000-00000000-0000DEFF-FFADBEEFF"}, {'mac_address': 'DE:AD:BE:EF:00:00'} ]) req = self._get_last_request() self.assertEqual(req.body, six.b( '<list>' '<DeviceCore>' '<devConnectwareId>00000000-00000000-0000DEFF-FFADBEEFF</devConnectwareId>' '</DeviceCore>' '' '<DeviceCore>' '<devMac>DE:AD:BE:EF:00:00</devMac>' '</DeviceCore>' '</list>')) self.assertTrue(len(res), 2) self.assertDictEqual(res[0], {"error": False, "error_msg": None, "location": "DeviceCore/1397876/0"}) self.assertDictEqual(res[1], {"error": False, "error_msg": None, "location": "DeviceCore/946246/0"}) def test_without_required_param(self): self.assertRaises(ValueError, self.dc.devicecore.provision_device, description="I should not work") def test_bad_request_400(self): self.prepare_response('POST', '/ws/DeviceCore', 'Bad Request', status=400) self.assertRaises(DeviceCloudHttpException, self.dc.devicecore.provision_device, mac_address="DE:AD:BE:EF:00:00") def test_bad_request_500(self): self.prepare_response('POST', '/ws/DeviceCore', 'Bad Request', status=500) self.assertRaises(DeviceCloudHttpException, self.dc.devicecore.provision_device, mac_address="DE:AD:BE:EF:00:00") def test_error_response(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_ERROR1, status=207) res = self.dc.devicecore.provision_device(imei="990000862471854") self.assertDictEqual(res, { "error": True, "error_msg": 'The device 00000000-00000000-BC5FF4FF-FFF7908A is already provisioned.', "location": None} ) def test_mixed_error_success_response(self): self.prepare_response("POST", "/ws/DeviceCore", PROVISION_MIXED_RESULT_RESPONSE, status=207) res = self.dc.devicecore.provision_devices([ {'device_id': "00000000-00000000-0000DEFF-FFADBEEFF"}, {'mac_address': 'DE:AD:BE:EF:00:00'} ]) self.assertTrue(len(res), 2) self.assertDictEqual(res[0], { 'error': False, 'error_msg': None, 'location': 'DeviceCore/1397876/0', }) self.assertDictEqual(res[1], { 'error': True, 'error_msg': 'The device 00000000-00000000-D48564FF-FF9D4FEE is already provisioned.', 'location': None, }) class TestDeviceCoreDeleting(HttpTestBase): def test_delete_device_good(self): fake_device = mock.MagicMock() fake_device.get_device_id.return_value = '1234' self.prepare_response("DELETE", "/ws/DeviceCore/1234", "<result><message>1 items deleted</message></result>", status=200) self.dc.devicecore.delete_device(fake_device) req = self._get_last_request() self.assertEqual(req.path, "/ws/DeviceCore/1234") def test_delete_device_not_exist(self): fake_device = mock.MagicMock() fake_device.get_device_id.return_value = '1234' self.prepare_response("DELETE", "/ws/DeviceCore/1234", "<result><message>0 items deleted</message></result>", status=200) self.dc.devicecore.delete_device(fake_device) req = self._get_last_request() self.assertEqual(req.path, "/ws/DeviceCore/1234") def test_delete_device_bad_status(self): fake_device = mock.MagicMock() fake_device.get_device_id.return_value = '1234' self.prepare_response("DELETE", "/ws/DeviceCore/1234", "<result><error>I pity da foo' who don' know about API changes.</error></result>", status=400) try: self.dc.devicecore.delete_device(fake_device) except DeviceCloudHttpException: pass else: assert False, "should have thrown exception" req = self._get_last_request() self.assertEqual(req.path, "/ws/DeviceCore/1234") class TestDeviceCoreDevices(HttpTestBase): def test_dc_get_devices(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) devices = self.dc.devicecore.get_devices() dev1 = six.next(devices) dev2 = six.next(devices) self.assertRaises(StopIteration, six.next, devices) self.assertEqual(dev1.get_mac(), "00:40:9D:58:17:5B") self.assertEqual(dev1.get_mac_last4(), "175B") self.assertEqual(dev1.get_device_id(), "702077") self.assertEqual(dev1.get_connectware_id(), "00000000-00000000-00409DFF-FF58175B") self.assertEqual(dev1.get_ip(), "10.35.1.107") self.assertEqual(dev1.get_tags(), []) self.assertEqual(dev1.get_registration_dt(), datetime.datetime(2013, 2, 28, 19, 54, tzinfo=tzutc())) self.assertEqual(dev1.get_meid(), '354374042391400') self.assertEqual(dev1.get_customer_id(), '1872') self.assertEqual(dev1.get_group_id(), '2331') self.assertEqual(dev1.get_group_path(), '') self.assertEqual(dev1.get_vendor_id(), '4261412864') self.assertEqual(dev1.get_device_type(), 'ConnectPort X5 R') self.assertEqual(dev1.get_firmware_level(), '34537482') self.assertEqual(dev1.get_firmware_level_description(), '2.15.0.10') self.assertEqual(dev1.get_restricted_status(), "0") self.assertEqual(dev1.get_last_known_ip(), '10.35.1.107') self.assertEqual(dev1.get_global_ip(), '204.182.3.237') self.assertEqual(dev1.get_last_connected_dt(), datetime.datetime(2013, 4, 8, 4, 1, 20, 633000, tzinfo=tzutc())) self.assertEqual(dev1.get_contact(), '') self.assertEqual(dev1.get_description(), '') self.assertEqual(dev1.get_location(), '') self.assertEqual(dev1.get_latlon(), (34.964465, 40.268198)) self.assertEqual(dev1.get_user_metadata(), None) self.assertEqual(dev1.get_zb_pan_id(), None) self.assertEqual(dev1.get_zb_extended_address(), None) self.assertEqual(dev1.get_server_id(), '') self.assertEqual(dev1.get_provision_id(), None) self.assertEqual(dev1.get_current_connect_pw(), None) def test_dc_get_devices_paged(self): self.prepare_response("GET", "/ws/DeviceCore", GET_DEVICES_PAGE1) gen = self.dc.devicecore.get_devices(page_size=1) dev1 = six.next(gen) self.prepare_response("GET", "/ws/DeviceCore", GET_DEVICES_PAGE2) dev2 = six.next(gen) self.assertRaises(StopIteration, six.next, gen) self.assertEqual(dev1.get_device_id(), '702077') self.assertEqual(dev2.get_device_id(), '702078') def test_dc_get_devices_with_condition(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) gen = self.dc.devicecore.get_devices(dev_mac == 'xx:xx:xx:xx:xx', page_size=1) six.next(gen) qs = httpretty.last_request().querystring self.assertEqual(qs['condition'][0], "devMac='xx:xx:xx:xx:xx'") self.assertEqual(qs['size'][0], "1") self.assertEqual(qs['embed'][0], "true") self.assertEqual(qs['start'][0], "0") def test_refresh_from_cache(self): get_devices_update = copy.deepcopy(EXAMPLE_GET_DEVICES) get_devices_update["items"][0]["dpDeviceType"] = "Turboencabulator" del get_devices_update["items"][1] # remove the other item... close enough self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) devices = self.dc.devicecore.get_devices() device = six.next(devices) self.prepare_json_response("GET", "/ws/DeviceCore/702077", get_devices_update) self.assertEqual(device.get_device_type(), "ConnectPort X5 R") self.assertEqual(device.get_device_type(False), "Turboencabulator") self.assertEqual(device.get_device_type(), "Turboencabulator") # make sure cache updated def test_add_device_to_group(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) expected = ADD_GROUP_TEMPLATE.format(connectware_id=dev.get_connectware_id(), group_path='testgrp') dev.add_to_group('testgrp') self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_remove_device_from_group(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) dev.get_group_path = lambda: 'something other than empty string' expected = ADD_GROUP_TEMPLATE.format(connectware_id=dev.get_connectware_id(), group_path='') dev.remove_from_group() self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_add_device_tag(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) expected = TAGS_TEMPLATE.format(connectware_id=dev.get_connectware_id(), tags='test') dev.add_tag('test') self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_add_multiple_tags(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) expected = TAGS_TEMPLATE.format(connectware_id=dev.get_connectware_id(), tags='test,test2,test3') dev.add_tag('test,test2,test3') self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_add_tag_list(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) tags = ['test', 'test2', 'test3'] expected = TAGS_TEMPLATE.format(connectware_id=dev.get_connectware_id(), tags="{}".format(",".join(tags))) dev.add_tag(tags) self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_add_tags_with_spaces(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) tags = 'test, test2, test3, compound tag' clean_tags = [t.strip() for t in tags.split(',')] expected = TAGS_TEMPLATE.format(connectware_id=dev.get_connectware_id(), tags="{}".format(",".join(clean_tags))) dev.add_tag(tags) self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_add_tags_with_special_chars(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) tags = 'test, test2, test3, this & that, < more >' clean_tags = [t.strip() for t in tags.split(',')] expected = TAGS_TEMPLATE.format(connectware_id=dev.get_connectware_id(), tags=escape("{}".format(",".join(clean_tags)))) dev.add_tag(tags) self.assertIsNone(dev._device_json) self.assertEqual(six.b(expected), httpretty.last_request().body) def test_remove_device_tag(self): self.prepare_json_response("GET", "/ws/DeviceCore", EXAMPLE_GET_DEVICES) self.prepare_response("PUT", "/ws/DeviceCore", '') gen = self.dc.devicecore.get_devices(page_size=1) dev = six.next(gen) try: dev.remove_tag('test') except ValueError: pass else: assert False, "should have thrown exception" if __name__ == '__main__': unittest.main()