diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..3aa487dd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,97 @@ +import re +from xml.dom.minidom import Node, parseString + + +def strip_quotes(want, got): + """ + Strip quotes of doctests output values: + >>> strip_quotes("'foo'") + "foo" + >>> strip_quotes('"foo"') + "foo" + """ + def is_quoted_string(s): + s = s.strip() + return (len(s) >= 2 + and s[0] == s[-1] + and s[0] in ('"', "'")) + + def is_quoted_unicode(s): + s = s.strip() + return (len(s) >= 3 + and s[0] == 'u' + and s[1] == s[-1] + and s[1] in ('"', "'")) + + if is_quoted_string(want) and is_quoted_string(got): + want = want.strip()[1:-1] + got = got.strip()[1:-1] + elif is_quoted_unicode(want) and is_quoted_unicode(got): + want = want.strip()[2:-1] + got = got.strip()[2:-1] + return want, got + + +def compare_xml(want, got): + """Tries to do a 'xml-comparison' of want and got. Plain string + comparison doesn't always work because, for example, attribute + ordering should not be important. Comment nodes are not considered in the + comparison. + Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py + """ + _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') + + def norm_whitespace(v): + return _norm_whitespace_re.sub(' ', v) + + def child_text(element): + return ''.join(c.data for c in element.childNodes + if c.nodeType == Node.TEXT_NODE) + + def children(element): + return [c for c in element.childNodes + if c.nodeType == Node.ELEMENT_NODE] + + def norm_child_text(element): + return norm_whitespace(child_text(element)) + + def attrs_dict(element): + return dict(element.attributes.items()) + + def check_element(want_element, got_element): + if want_element.tagName != got_element.tagName: + return False + if norm_child_text(want_element) != norm_child_text(got_element): + return False + if attrs_dict(want_element) != attrs_dict(got_element): + return False + want_children = children(want_element) + got_children = children(got_element) + if len(want_children) != len(got_children): + return False + for want, got in zip(want_children, got_children): + if not check_element(want, got): + return False + return True + + def first_node(document): + for node in document.childNodes: + if node.nodeType != Node.COMMENT_NODE: + return node + + want, got = strip_quotes(want, got) + want = want.replace('\\n', '\n') + got = got.replace('\\n', '\n') + + # If the string is not a complete xml document, we may need to add a + # root element. This allow us to compare fragments, like "" + if not want.startswith('%s' + want = wrapper % want + got = wrapper % got + + # Parse the want and got strings, and compare the parsings. + want_root = first_node(parseString(want)) + got_root = first_node(parseString(got)) + + return check_element(want_root, got_root) diff --git a/tests/manager.py b/tests/manager.py index d364a0d0..02d3ead6 100644 --- a/tests/manager.py +++ b/tests/manager.py @@ -16,9 +16,38 @@ from xero import Xero from xero.manager import Manager from tests import mock_data +from . import compare_xml +from unittest.util import safe_repr +import difflib +import six class ManagerTest(unittest.TestCase): + maxDiff = None + + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + diff = ('\n' + '\n'.join( + difflib.ndiff( + six.text_type(xml1).splitlines(), + six.text_type(xml2).splitlines(), + ) + )) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + def test_serializer(self): credentials = Mock(base_url="") manager = Manager('contacts', credentials) @@ -80,12 +109,87 @@ def test_serializer(self): 2015-07-06 16:25:02.711136 """ - # @todo Need a py2/3 way to compare XML easily. - # self.assertEqual( - # resultant_xml, - # expected_xml, - # "Failed to serialize data to XML correctly." - # ) + self.assertXMLEqual( + resultant_xml, + expected_xml, + ) + + + def test_serializer_phones_addresses(self): + credentials = Mock(base_url="") + manager = Manager('contacts', credentials) + + example_contact_input = { + 'ContactID': '565acaa9-e7f3-4fbf-80c3-16b081ddae10', + 'ContactStatus': 'ACTIVE', + 'Name': 'Southside Office Supplies', + 'Addresses': [ + { + 'AddressType': 'POBOX', + }, + { + 'AddressType': 'STREET', + }, + ], + 'Phones': [ + { + 'PhoneType': 'DDI', + }, + { + 'PhoneType': 'DEFAULT', + }, + { + 'PhoneType': 'FAX', + }, + { + 'PhoneType': 'MOBILE', + }, + ], + 'UpdatedDateUTC': datetime.datetime(2015, 9, 18, 5, 6, 56, 893), + 'IsSupplier': False, + 'IsCustomer': False, + 'HasAttachments': False, + } + resultant_xml = manager._prepare_data_for_save(example_contact_input) + + expected_xml = """ + + 565acaa9-e7f3-4fbf-80c3-16b081ddae10 + ACTIVE + Southside Office Supplies + +
+ POBOX +
+
+ STREET +
+
+ + + DDI + + + DEFAULT + + + FAX + + + MOBILE + + + 2015-09-18T05:06:56.893 + false + false + false +
+ """ + + self.assertXMLEqual( + resultant_xml, + expected_xml, + ) def test_serializer_nested_singular(self):