def common_tests(class_, xml_, dict_, parent_id): """Common tests for the MS classes.""" xml_content = XML.fromstring(xml_.encode('utf8')) # MusicServiceItem.from_xml and MusicServiceItem.to_dict item_from_xml = class_.from_xml(xml_content, FAKE_MUSIC_SERVICE, parent_id) assert item_from_xml.to_dict == dict_ # MusicServiceItem.from_dict and MusicServiceItem.to_dict item_from_dict = class_.from_dict(dict_) assert item_from_dict.to_dict == dict_ # MusicServiceItem.didl_metadata # NOTE! These tests is reliant on the attributes being put in the same # order by ElementTree and so it might fail if that changes if item_from_xml.can_play: dict_encoded = {} for key, value in dict_.items(): try: is_str = isinstance(value, unicode) except NameError: is_str = isinstance(value, str) if is_str: dict_encoded[key] = escape(value).\ encode('ascii', 'xmlcharrefreplace').decode('ascii') else: dict_encoded[key] = value didl = DIDL_TEMPLATE.format(item_class=class_.item_class, **dict_encoded) assert XML.tostring(item_from_xml.didl_metadata).decode('ascii') == \ didl assert XML.tostring(item_from_dict.didl_metadata).decode('ascii') == \ didl else: with pytest.raises(DIDLMetadataError): # pylint: disable=pointless-statement item_from_xml.didl_metadata # Text attributes with mandatory content for name in ['item_id', 'extended_id', 'title', 'service_id']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_[name]) # Text attributes with voluntary content for name in ['parent_id', 'album_art_uri']: getter_attributes_test(name, item_from_xml, item_from_dict, dict_.get(name)) # Boolean attribute getter_attributes_test('can_play', item_from_xml, item_from_dict, bool(dict_.get('can_play'))) return item_from_xml, item_from_dict
def test_add_item_to_sonos_playlist(self, moco): moco.contentDirectory.reset_mock() playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco.contentDirectory.Browse.return_value = { 'NumberReturned': '0', 'Result': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>', 'TotalMatches': '0', 'UpdateID': update_id } moco.add_item_to_sonos_playlist(track, playlist) moco.contentDirectory.Browse.assert_called_once_with([ ('ObjectID', playlist.item_id), ('BrowseFlag', 'BrowseDirectChildren'), ('Filter', '*'), ('StartingIndex', 0), ('RequestedCount', 1), ('SortCriteria', '') ]) moco.avTransport.AddURIToSavedQueue.assert_called_once_with([ ('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295) ])
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint="http://endpoint.example.com", method="getData", parameters=[("one", "1")], http_headers={"user-agent": "sonos"}, soap_action="ACTION", soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4, ) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch("requests.post", return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( "http://endpoint.example.com", headers={"SOAPACTION": '"ACTION"', "Content-Type": 'text/xml; charset="utf-8"', "user-agent": "sonos"}, data=mock.ANY, other_arg=4, )
def test_create_didl_resource_to_from_element(self): res = data_structures.DidlResource('a%20uri', 'a:protocol:info:xx', bitrate=3) elt = res.to_element() assert XML.tostring(elt) == ( b'<res bitrate="3" protocolInfo="a:protocol:info:xx">a%20uri</res>') assert data_structures.DidlResource.from_element(elt) == res
def test_call(): """ Calling a command should result in an http request""" s = SoapMessage( endpoint='http://endpoint.example.com', method='getData', parameters=[('one', '1')], http_headers={'user-agent': 'sonos'}, soap_action='ACTION', soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch('requests.post', return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( 'http://endpoint.example.com', headers={'SOAPACTION': '"ACTION"', 'Content-Type': 'text/xml; charset="utf-8"', 'user-agent': 'sonos'}, data=mock.ANY, other_arg=4)
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint="http://endpoint.example.com", method="getData", parameters=[("one", "1")], http_headers={"user-agent": "sonos"}, soap_action="ACTION", soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4, ) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch("requests.post", return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( "http://endpoint.example.com", headers={ "SOAPACTION": '"ACTION"', "Content-Type": 'text/xml; charset="utf-8"', "user-agent": "sonos", }, data=mock.ANY, other_arg=4, )
def test_call(): """Calling a command should result in an http request.""" s = SoapMessage( endpoint='http://endpoint.example.com', method='getData', parameters=[('one', '1')], http_headers={'user-agent': 'sonos'}, soap_action='ACTION', soap_header="<a_header>data</a_header>", namespace="http://namespace.com", other_arg=4) response = mock.MagicMock() response.headers = {} response.status_code = 200 response.content = DUMMY_VALID_RESPONSE with mock.patch('requests.post', return_value=response) as fake_post: result = s.call() assert XML.tostring(result) fake_post.assert_called_once_with( 'http://endpoint.example.com', headers={'SOAPACTION': '"ACTION"', 'Content-Type': 'text/xml; charset="utf-8"', 'user-agent': 'sonos'}, data=mock.ANY, other_arg=4)
def call(self, method, args=None): http_headers = { 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Linux UPnP/1.0 Sonos/26.99-12345' } message = SoapMessage(endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace='http://www.sonos.com/Services/1.1', timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: raise MusicServiceException(exc.faultstring, exc.faultcode) result = list( parse(XML.tostring(result_elt), process_namespaces=True, namespaces={ 'http://www.sonos.com/Services/1.1': None }).values())[0] return result if result is not None else {}
def __init__(self, faultcode, faultstring, detail=None): """ Args: faultcode (str): The SOAP faultcode faultstring (str): The SOAP faultstring detail (Element): The SOAP fault detail, as an ElementTree Element. Default, None """ self.faultcode = faultcode self.faultstring = faultstring self.detail = detail self.detail_string = XML.tostring(detail) if detail is not None else '' super(SoapFault, self).__init__(faultcode, faultstring)
def get_soap_header(self): """ Generate the SOAP authentication header for the related service. This header contains all the necessary authentication details. Returns: (str): A string representation of the XML content of the SOAP header """ # According to the SONOS SMAPI, this header must be sent with all # SOAP requests. Building this is an expensive operation (though # occasionally necessary), so f we have a cached value, return it if self._cached_soap_header is not None: return self._cached_soap_header music_service = self.music_service credentials_header = XML.Element( "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"}) device_id = XML.SubElement(credentials_header, 'deviceId') device_id.text = self._device_id device_provider = XML.SubElement(credentials_header, 'deviceProvider') device_provider.text = 'Sonos' if music_service.account.oa_device_id: # OAuth account credentials are present. We must use them to # authenticate. login_token = XML.Element('loginToken') token = XML.SubElement(login_token, 'token') token.text = music_service.account.oa_device_id key = XML.SubElement(login_token, 'key') key.text = music_service.account.key household_id = XML.SubElement(login_token, 'householdId') household_id.text = self._device.household_id credentials_header.append(login_token) # otherwise, perhaps use DeviceLink or UserId auth elif music_service.auth_type in ['DeviceLink', 'UserId']: # We need a session ID from Sonos session_id = self._device.musicServices.GetSessionId([ ('ServiceId', music_service.service_id), ('Username', music_service.account.username) ])['SessionId'] session_elt = XML.Element('sessionId') session_elt.text = session_id credentials_header.append(session_elt) # Anonymous auth. No need for anything further. self._cached_soap_header = XML.tostring(credentials_header) return self._cached_soap_header
def test_didl_object_to_element(self): didl_object = data_structures.DidlObject(title='a_title', parent_id='pid', item_id='iid', creator='a_creator') # we seem to have to go through this to get ElementTree to deal # with namespaces properly! elt = XML.fromstring(XML.tostring(didl_object.to_element(True))) elt2 = XML.fromstring('<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' + 'xmlns:dc="http://purl.org/dc/elements/1.1/" ' + 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' + '<item id="iid" parentID="pid" restricted="true">' + '<dc:title>a_title</dc:title>' + '<dc:creator>a_creator</dc:creator>' + '<upnp:class>object</upnp:class><desc id="cdudn" '+ 'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' + 'RINCON_AssociatedZPUDN</desc></item></dummy>')[0] assert_xml_equal(elt2, elt)
def test_didl_object_to_element(self): didl_object = data_structures.DidlObject( title='a_title', parent_id='pid', item_id='iid', creator='a_creator') # we seem to have to go through this to get ElementTree to deal # with namespaces properly! elt = XML.fromstring(XML.tostring(didl_object.to_element(True))) elt2 = XML.fromstring( '<dummy xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" ' + 'xmlns:dc="http://purl.org/dc/elements/1.1/" ' + 'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">' + '<item id="iid" parentID="pid" restricted="true">' + '<dc:title>a_title</dc:title>' + '<dc:creator>a_creator</dc:creator>' + '<upnp:class>object</upnp:class><desc id="cdudn" ' + 'nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">' + 'RINCON_AssociatedZPUDN</desc></item></dummy>')[0] assert_xml_equal(elt2, elt)
def get_soap_header(self): if self._session_id is None: return '' if self._cached_soap_header is not None: return self._cached_soap_header credentials_header = XML.Element( "credentials", {'xmlns': "http://www.sonos.com/Services/1.1"}) if self.music_service.auth_type in ['UserId']: session_elt = XML.Element('sessionId') session_elt.text = self._session_id credentials_header.append(session_elt) else: raise Exception('unknown auth_type = ' + self.music_service.auth_type) self._cached_soap_header = XML.tostring( credentials_header, encoding='utf-8').decode(encoding='utf-8') return self._cached_soap_header
def test_add_item_to_sonos_playlist(self, moco): playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco._music_lib_search = mock.Mock(return_value=( {'UpdateID': update_id}, None)) moco.add_item_to_sonos_playlist(track, playlist) moco._music_lib_search.assert_called_once_with(playlist.item_id, 0, 1) moco.avTransport.AddURIToSavedQueue.assert_called_once_with( [('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295)] )
def test_add_item_to_sonos_playlist(self, moco): moco.contentDirectory.reset_mock() playlist = mock.Mock() playlist.item_id = 7 track = mock.Mock() track.uri = 'fake_uri' track.didl_metadata = XML.Element('a') update_id = 100 moco.contentDirectory.Browse.return_value = { 'NumberReturned': '0', 'Result': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"></DIDL-Lite>', 'TotalMatches': '0', 'UpdateID': update_id } moco.add_item_to_sonos_playlist(track, playlist) moco.contentDirectory.Browse.assert_called_once_with([ ('ObjectID', playlist.item_id), ('BrowseFlag', 'BrowseDirectChildren'), ('Filter', '*'), ('StartingIndex', 0), ('RequestedCount', 1), ('SortCriteria', '') ]) moco.avTransport.AddURIToSavedQueue.assert_called_once_with( [('InstanceID', 0), ('UpdateID', update_id), ('ObjectID', playlist.item_id), ('EnqueuedURI', track.uri), ('EnqueuedURIMetaData', XML.tostring(track.didl_metadata)), ('AddAtIndex', 4294967295)] )
def call(self, method, args=None): """ Call a method on the server Args: method (str): The name of the method to call. args (list): A list of (parameter, value) pairs representing the parameters of the method. Default None. Returns: (OrderedDict): An OrderedDict representing the response Raises: MusicServiceException """ message = SoapMessage(endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: if 'Client.TokenRefreshRequired' in exc.faultcode: log.debug('Token refresh required. Trying again') # Remove any cached value for the SOAP header self._cached_soap_header = None # <detail> # <refreshAuthTokenResult> # <authToken>xxxxxxx</authToken> # <privateKey>zzzzzz</privateKey> # </refreshAuthTokenResult> # </detail> auth_token = exc.detail.findtext('.//authToken') private_key = exc.detail.findtext('.//privateKey') # We have new details - update the account self.music_service.account.oa_device_id = auth_token self.music_service.account.key = private_key message = SoapMessage( endpoint=self.endpoint, method=method, parameters=args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) result_elt = message.call() else: raise MusicServiceException(exc.faultstring, exc.faultcode) # The top key in the OrderedDict will be the methodResult. Its # value may be None if no results were returned. result = parse(XML.tostring(result_elt), process_namespaces=True, namespaces={ 'http://www.sonos.com/Services/1.1': None }).values()[0] return result if result is not None else {}
def call(self, method, args=None): """ Call a method on the server Args: method (str): The name of the method to call. args (list): A list of (parameter, value) pairs representing the parameters of the method. Default None. Returns: (OrderedDict): An OrderedDict representing the response Raises: MusicServiceException """ message = SoapMessage( endpoint=self.endpoint, method=method, parameters=[] if args is None else args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) try: result_elt = message.call() except SoapFault as exc: if 'Client.TokenRefreshRequired' in exc.faultcode: log.debug('Token refresh required. Trying again') # Remove any cached value for the SOAP header self._cached_soap_header = None # <detail> # <refreshAuthTokenResult> # <authToken>xxxxxxx</authToken> # <privateKey>zzzzzz</privateKey> # </refreshAuthTokenResult> # </detail> auth_token = exc.detail.findtext('.//authToken') private_key = exc.detail.findtext('.//privateKey') # We have new details - update the account self.music_service.account.oa_device_id = auth_token self.music_service.account.key = private_key message = SoapMessage( endpoint=self.endpoint, method=method, parameters=args, http_headers=self.http_headers, soap_action="http://www.sonos.com/Services/1" ".1#{0}".format(method), soap_header=self.get_soap_header(), namespace=self.namespace, timeout=self.timeout) result_elt = message.call() else: raise MusicServiceException(exc.faultstring, exc.faultcode) # The top key in the OrderedDict will be the methodResult. Its # value may be None if no results were returned. result = parse( XML.tostring(result_elt), process_namespaces=True, namespaces={'http://www.sonos.com/Services/1.1': None} ).values()[0] return result if result is not None else {}