comparison test/test_mailgw.py @ 5123:226052e0cc4c

Fixed incorrect header comparisons in compareMessages. It iterated over every header in the new message and didn't detect when the new message was missing a header that was in the reference message. I have fixed that by retrieving all header names from the new and reference (old) messages, lower casing the list and comparing them (with a fixup for the x-roundup-version header). If the list of headers isn't identical, I throw an error. Also the Content-Type mime values are now being compared as well to make sure the new message being generates matches the reference mime content type. Of course this broke some tests. A number of them were using compareMessages when they should have been using assertEqual. That was an easy fix. Also some messages when retrieved from the content property are missing trailing newlines. I think this is part of the signature removal algorithm. This may be a bug or intended. In any case I am fixing the tests to remove trailing newlines. However I have a handful of tests that are not that obvious. Two of are test_mailgw.py:testMultipartCharsetLatin1AttachFile test_mailgw.py:testMultipartCharsetUTF8AttachFile I removed a: Content-Transfer-Encoding: quoted-printable message header from the reference message in both cases. The message content-type was multipart/mixed and it had no content that was outside of a mime part. Sending a message like this using mailx and nmh resulted in an email being delivered that was missing a Content-Transfer-Encoding, so I think I am ok here. The last broken test also started as a missing Content-Transfer-Encoding header. But there was a twist: test-mailgw.py:testMultipartRFC822 had a reference copy with a mime type of text/plain which requires a Content-Transfer-Encoding. However I never looked at the mime type of the mail being generated. The generated mail was of type multipart/mixed. So I changed the reference copy to remove the Content-Transfer-Encoding and changed the reference mime type to multipart/mixed. Now all the mailgw tests are passing. With luck I haven't encoded an invalid test. Also did one minor spelling correction.
author John Rouillard <rouilj@ieee.org>
date Sun, 03 Jul 2016 18:54:18 -0400
parents 14abd0a67207
children a927f9549af0
comparison
equal deleted inserted replaced
5122:1c90f15a177f 5123:226052e0cc4c
62 62
63 class DiffHelper: 63 class DiffHelper:
64 def compareMessages(self, new, old): 64 def compareMessages(self, new, old):
65 """Compare messages for semantic equivalence. 65 """Compare messages for semantic equivalence.
66 66
67 Only use this for full rfc 2822/822/whatever messages with headers.
68
67 Will raise an AssertionError with a diff for inequality. 69 Will raise an AssertionError with a diff for inequality.
68 70
69 Note that header fieldnames are case-insensitive. 71 Note that header fieldnames are case-insensitive.
70 So if a header fieldname appears more than once in different casing 72 So if a header fieldname appears more than once in different casing
71 and the values are not equal, there will be more than one entry 73 and the values are not equal, there will be more than one entry
83 85
84 if not new == old: 86 if not new == old:
85 res = [] 87 res = []
86 88
87 replace = {} 89 replace = {}
90
91 # make sure that all headers are the same between the new
92 # and old (reference) messages. Once we have done this we
93 # can iterate over all the headers in the new message and
94 # compare contents. If we don't do this, we don't know
95 # when the new message has dropped a header that should be
96 # present.
97 # Headers are case insensitive, so smash to lower case
98 new_headers=[x.lower() for x in new.keys()]
99 old_headers=[x.lower() for x in old.keys()]
100
101 if "x-roundup-version" not in old_headers:
102 # add it. it is skipped in most cases and missing from
103 # the test cases in old.
104 old_headers.append("x-roundup-version")
105
106 new_headers.sort() # sort, make comparison easier in error message.
107 old_headers.sort()
108
109 if new_headers != old_headers:
110 res.append('headers differ new vs. reference: %r != %r'%(new_headers, old_headers))
111
88 for key in new.keys(): 112 for key in new.keys():
89 if key.startswith('from '): 113 if key.startswith('from '):
90 # skip the unix from line 114 # skip the unix from line
91 continue 115 continue
92 if key.lower() == 'x-roundup-version': 116 if key.lower() == 'x-roundup-version':
94 if new[key] != __version__: 118 if new[key] != __version__:
95 res.append(' %s: %r != %r' % (key, __version__, 119 res.append(' %s: %r != %r' % (key, __version__,
96 new[key])) 120 new[key]))
97 elif key.lower() == 'content-type' and 'boundary=' in new[key]: 121 elif key.lower() == 'content-type' and 'boundary=' in new[key]:
98 # handle mime messages 122 # handle mime messages
99 newmime = new[key].split('=',1)[-1].strip('"') 123 newmimeboundary = new[key].split('=',1)[-1].strip('"')
100 oldmime = old.get(key, '').split('=',1)[-1].strip('"') 124 oldmimeboundary = old.get(key, '').split('=',1)[-1].strip('"')
101 replace ['--' + newmime] = '--' + oldmime 125 # mime types are not case sensitive rfc 2045
102 replace ['--' + newmime + '--'] = '--' + oldmime + '--' 126 newmimetype = new[key].split(';',1)[0].strip('"').lower()
127 oldmimetype = old.get(key, '').split(';',1)[0].strip('"').lower()
128 # throw an error if we have differeing content types
129 if not newmimetype == oldmimetype:
130 res.append('content-type mime type headers differ new vs. reference: %r != %r'%(newmimetype, oldmimetype))
131 replace ['--' + newmimeboundary] = '--' + oldmimeboundary
132 replace ['--' + newmimeboundary + '--'] = '--' + oldmimeboundary + '--'
103 elif new.get_all(key, '') != old.get_all(key, ''): 133 elif new.get_all(key, '') != old.get_all(key, ''):
104 # check that all other headers are identical, including 134 # check that all other headers are identical, including
105 # headers that appear more than once. 135 # headers that appear more than once.
106 res.append(' %s: %r != %r' % (key, old.get_all(key, ''), 136 res.append(' %s: %r != %r' % (key, old.get_all(key, ''),
107 new.get_all(key, ''))) 137 new.get_all(key, '')))
909 In-Reply-To: <dummy_test_message_id> 939 In-Reply-To: <dummy_test_message_id>
910 X-Roundup-Name: Roundup issue tracker 940 X-Roundup-Name: Roundup issue tracker
911 X-Roundup-Loop: hello 941 X-Roundup-Loop: hello
912 X-Roundup-Issue-Status: chatting 942 X-Roundup-Issue-Status: chatting
913 X-Roundup-Issue-Files: unnamed 943 X-Roundup-Issue-Files: unnamed
914 Content-Transfer-Encoding: quoted-printable
915 944
916 945
917 --utf-8 946 --utf-8
918 MIME-Version: 1.0 947 MIME-Version: 1.0
919 Content-Type: text/plain; charset="utf-8" 948 Content-Type: text/plain; charset="utf-8"
973 In-Reply-To: <dummy_test_message_id> 1002 In-Reply-To: <dummy_test_message_id>
974 X-Roundup-Name: Roundup issue tracker 1003 X-Roundup-Name: Roundup issue tracker
975 X-Roundup-Loop: hello 1004 X-Roundup-Loop: hello
976 X-Roundup-Issue-Status: chatting 1005 X-Roundup-Issue-Status: chatting
977 X-Roundup-Issue-Files: unnamed 1006 X-Roundup-Issue-Files: unnamed
978 Content-Transfer-Encoding: quoted-printable
979 1007
980 1008
981 --utf-8 1009 --utf-8
982 MIME-Version: 1.0 1010 MIME-Version: 1.0
983 Content-Type: text/plain; charset="iso-8859-1" 1011 Content-Type: text/plain; charset="iso-8859-1"
1020 self.assertEqual(f.name, name) 1048 self.assertEqual(f.name, name)
1021 self.assertEqual(msg.content, 'First part: Text') 1049 self.assertEqual(msg.content, 'First part: Text')
1022 self.compareMessages(self._get_mail(), 1050 self.compareMessages(self._get_mail(),
1023 '''FROM: roundup-admin@your.tracker.email.domain.example 1051 '''FROM: roundup-admin@your.tracker.email.domain.example
1024 TO: chef@bork.bork.bork, richard@test.test 1052 TO: chef@bork.bork.bork, richard@test.test
1025 Content-Type: text/plain; charset="utf-8" 1053 Content-Type: multipart/mixed; charset="utf-8"
1026 Subject: [issue1] Testing... 1054 Subject: [issue1] Testing...
1027 To: chef@bork.bork.bork, richard@test.test 1055 To: chef@bork.bork.bork, richard@test.test
1028 From: "Contrary, Mary" <issue_tracker@your.tracker.email.domain.example> 1056 From: "Contrary, Mary" <issue_tracker@your.tracker.email.domain.example>
1029 Reply-To: Roundup issue tracker 1057 Reply-To: Roundup issue tracker
1030 <issue_tracker@your.tracker.email.domain.example> 1058 <issue_tracker@your.tracker.email.domain.example>
1033 In-Reply-To: <dummy_test_message_id> 1061 In-Reply-To: <dummy_test_message_id>
1034 X-Roundup-Name: Roundup issue tracker 1062 X-Roundup-Name: Roundup issue tracker
1035 X-Roundup-Loop: hello 1063 X-Roundup-Loop: hello
1036 X-Roundup-Issue-Status: chatting 1064 X-Roundup-Issue-Status: chatting
1037 X-Roundup-Issue-Files: Fwd: Original email subject.eml 1065 X-Roundup-Issue-Files: Fwd: Original email subject.eml
1038 Content-Transfer-Encoding: quoted-printable
1039 1066
1040 1067
1041 --utf-8 1068 --utf-8
1042 MIME-Version: 1.0 1069 MIME-Version: 1.0
1043 Content-Type: text/plain; charset="utf-8" 1070 Content-Type: text/plain; charset="utf-8"
2118 anonid = self.db.user.lookup('anonymous') 2145 anonid = self.db.user.lookup('anonymous')
2119 self.db.user.set(anonid, roles='Anonymous') 2146 self.db.user.set(anonid, roles='Anonymous')
2120 try: 2147 try:
2121 self._handle_mail(message) 2148 self._handle_mail(message)
2122 except Unauthorized, value: 2149 except Unauthorized, value:
2123 body_diff = self.compareMessages(str(value), """ 2150 body_diff = self.assertEqual(str(value), """
2124 You are not a registered user. 2151 You are not a registered user.
2125 2152
2126 Unknown address: fubar@bork.bork.bork 2153 Unknown address: fubar@bork.bork.bork
2127 """) 2154 """)
2128 assert not body_diff, body_diff 2155 assert not body_diff, body_diff
2129 else: 2156 else:
2130 raise AssertionError, "Unathorized not raised when handling mail" 2157 raise AssertionError, "Unauthorized not raised when handling mail"
2131 2158
2132 # Add Web Access role to anonymous, and try again to make sure 2159 # Add Web Access role to anonymous, and try again to make sure
2133 # we get a "please register at:" message this time. 2160 # we get a "please register at:" message this time.
2134 p = [ 2161 p = [
2135 self.db.security.getPermission('Register', 'user'), 2162 self.db.security.getPermission('Register', 'user'),
2137 ] 2164 ]
2138 self.db.security.role['anonymous'].permissions=p 2165 self.db.security.role['anonymous'].permissions=p
2139 try: 2166 try:
2140 self._handle_mail(message) 2167 self._handle_mail(message)
2141 except Unauthorized, value: 2168 except Unauthorized, value:
2142 body_diff = self.compareMessages(str(value), """ 2169 body_diff = self.assertEqual(str(value), """
2143 You are not a registered user. Please register at: 2170 You are not a registered user. Please register at:
2144 2171
2145 http://tracker.example/cgi-bin/roundup.cgi/bugs/user?@template=register 2172 http://tracker.example/cgi-bin/roundup.cgi/bugs/user?@template=register
2146 2173
2147 ...before sending mail to the tracker. 2174 ...before sending mail to the tracker.
2542 This is a followup 2569 This is a followup
2543 ''' 2570 '''
2544 2571
2545 def testEmailQuoting(self): 2572 def testEmailQuoting(self):
2546 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'no' 2573 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'no'
2547 self.innerTestQuoting(self.firstquotingtest, '''This is a followup 2574 # FIXME possible bug. Messages retreived from content are missing
2548 ''', 'This is a followup') 2575 # trailing newlines. Probably due to signature stripping.
2576 # so nuke all trailing newlines.
2577 self.innerTestQuoting(self.firstquotingtest, '''This is a followup''', 'This is a followup')
2549 2578
2550 def testEmailQuotingNewIsNew(self): 2579 def testEmailQuotingNewIsNew(self):
2551 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'new' 2580 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'new'
2552 # create the message, remove the prefix from subject 2581 # create the message, remove the prefix from subject
2553 testmessage=self.firstquotingtest.replace(" Re: [issue1]", "") 2582 testmessage=self.firstquotingtest.replace(" Re: [issue1]", "")
2616 self.assertEqual(content, '''Blah blah wrote:\n> Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf\n> skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj\n>\n\nThis is a followup''') 2645 self.assertEqual(content, '''Blah blah wrote:\n> Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf\n> skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj\n>\n\nThis is a followup''')
2617 self.assertEqual(summary, '''This is a followup''') 2646 self.assertEqual(summary, '''This is a followup''')
2618 2647
2619 def testEmailQuotingRemove(self): 2648 def testEmailQuotingRemove(self):
2620 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'yes' 2649 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'yes'
2650 # FIXME possible bug. Messages retreived from content are missing
2651 # trailing newlines. Probably due to signature stripping.
2652 # so nuke all trailing newlines.
2621 self.innerTestQuoting(self.firstquotingtest, '''Blah blah wrote: 2653 self.innerTestQuoting(self.firstquotingtest, '''Blah blah wrote:
2622 > Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf 2654 > Blah bklaskdfj sdf asdf jlaskdf skj sdkfjl asdf
2623 > skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj 2655 > skdjlkjsdfalsdkfjasdlfkj dlfksdfalksd fj
2624 > 2656 >
2625 2657
2626 This is a followup 2658 This is a followup''', 'This is a followup')
2627 ''', 'This is a followup')
2628 2659
2629 secondquotingtest = '''Content-Type: text/plain; 2660 secondquotingtest = '''Content-Type: text/plain;
2630 charset="iso-8859-1" 2661 charset="iso-8859-1"
2631 From: richard <richard@test.test> 2662 From: richard <richard@test.test>
2632 To: issue_tracker@your.tracker.email.domain.example 2663 To: issue_tracker@your.tracker.email.domain.example
2672 -- 2703 --
2673 added signature 2704 added signature
2674 ''' 2705 '''
2675 def testEmailQuoting2(self): 2706 def testEmailQuoting2(self):
2676 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'no' 2707 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'no'
2708 # FIXME possible bug. Messages retreived from content are missing
2709 # trailing newlines. Probably due to signature stripping.
2710 # so nuke all trailing newlines.
2677 self.innerTestQuoting(self.secondquotingtest, '''AA: 2711 self.innerTestQuoting(self.secondquotingtest, '''AA:
2678 2712
2679 AA 2713 AA
2680 2714
2681 AA 2715 AA
2687 BB 2721 BB
2688 BB 2722 BB
2689 BB 2723 BB
2690 BB 2724 BB
2691 2725
2692 CC 2726 CC''', 'AA:')
2693 ''', 'AA:')
2694 2727
2695 def testEmailQuotingRemove2(self): 2728 def testEmailQuotingRemove2(self):
2696 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'yes' 2729 self.instance.config.EMAIL_KEEP_QUOTED_TEXT = 'yes'
2730 # FIXME possible bug. Messages retreived from content are missing
2731 # trailing newlines. Probably due to signature stripping.
2732 # so nuke all trailing newlines. That's what the trailing
2733 # [:-1] is doing on the '\n'.join(....)[8:-3]
2697 self.innerTestQuoting(self.secondquotingtest, 2734 self.innerTestQuoting(self.secondquotingtest,
2698 '\n'.join(self.secondquotingtest.split('\n')[8:-3]), 'AA:') 2735 '\n'.join(self.secondquotingtest.split('\n')[8:-3][:-1]), 'AA:')
2699 2736
2700 thirdquotingtest = '''Content-Type: text/plain; 2737 thirdquotingtest = '''Content-Type: text/plain;
2701 charset="iso-8859-1" 2738 charset="iso-8859-1"
2702 From: richard <richard@test.test> 2739 From: richard <richard@test.test>
2703 To: issue_tracker@your.tracker.email.domain.example 2740 To: issue_tracker@your.tracker.email.domain.example
2767 newmessages = self.db.issue.get(nodeid, 'messages') 2804 newmessages = self.db.issue.get(nodeid, 'messages')
2768 for msg in messages: 2805 for msg in messages:
2769 newmessages.remove(msg) 2806 newmessages.remove(msg)
2770 messageid = newmessages[0] 2807 messageid = newmessages[0]
2771 2808
2772 self.compareMessages(self.db.msg.get(messageid, 'content'), expect) 2809 self.assertEqual(self.db.msg.get(messageid, 'content'), expect)
2773 if summary: 2810 if summary:
2774 self.assertEqual (summary, self.db.msg.get(messageid, 'summary')) 2811 self.assertEqual (summary, self.db.msg.get(messageid, 'summary'))
2775 2812
2776 def testUserLookup(self): 2813 def testUserLookup(self):
2777 i = self.db.user.create(username='user1', address='user1@foo.com') 2814 i = self.db.user.create(username='user1', address='user1@foo.com')

Roundup Issue Tracker: http://roundup-tracker.org/