Mercurial > p > roundup > code
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') |
