Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/model.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/model.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/model.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/model.py 
Sat Nov 15 01:14:46 2014
@@ -1,23 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2013 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+
 from __future__ import with_statement
 
 from datetime import datetime, timedelta
-import os.path
 from StringIO import StringIO
 import tempfile
 import shutil
 import unittest
 
+import trac.tests.compat
 from trac import core
 from trac.attachment import Attachment
 from trac.core import TracError, implements
 from trac.resource import ResourceNotFound
+from trac.test import EnvironmentStub
 from trac.ticket.model import (
     Ticket, Component, Milestone, Priority, Type, Version
 )
+from trac.ticket.roadmap import MilestoneModule
 from trac.ticket.api import (
     IMilestoneChangeListener, ITicketChangeListener, TicketSystem
 )
-from trac.test import EnvironmentStub
 from trac.tests.resource import TestResourceChangeListener
 from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
 
@@ -41,6 +55,36 @@ class TestTicketChangeListener(core.Comp
         self.action = 'deleted'
         self.ticket = ticket
 
+    # the listener has no ticket_comment_modified and ticket_change_deleted
+
+
+class TestTicketChangeListener_2(core.Component):
+    implements(ITicketChangeListener)
+
+    def ticket_created(self, ticket):
+        pass
+
+    def ticket_changed(self, ticket, comment, author, old_values):
+        pass
+
+    def ticket_deleted(self, ticket):
+        pass
+
+    def ticket_comment_modified(self, ticket, cdate, author, comment,
+                                old_comment):
+        self.action = 'comment_modified'
+        self.ticket = ticket
+        self.cdate = cdate
+        self.author = author
+        self.comment = comment
+        self.old_comment = old_comment
+
+    def ticket_change_deleted(self, ticket, cdate, changes):
+        self.action = 'change_deleted'
+        self.ticket = ticket
+        self.cdate = cdate
+        self.changes = changes
+
 
 class TicketTestCase(unittest.TestCase):
 
@@ -118,9 +162,9 @@ class TicketTestCase(unittest.TestCase):
         log = ticket3.get_changelog()
         self.assertEqual(len(log), 3)
         ok_vals = ['foo', 'summary', 'comment']
-        self.failUnless(log[0][2] in ok_vals)
-        self.failUnless(log[1][2] in ok_vals)
-        self.failUnless(log[2][2] in ok_vals)
+        self.assertIn(log[0][2], ok_vals)
+        self.assertIn(log[1][2], ok_vals)
+        self.assertIn(log[2][2], ok_vals)
 
     def test_create_ticket_5(self):
         ticket3 = self._modify_a_ticket()
@@ -157,7 +201,7 @@ class TicketTestCase(unittest.TestCase):
         ticket.save_changes()
 
         for change in ticket.get_changelog():
-            self.assertEqual(None, change[1])
+            self.assertIsNone(change[1])
 
     def test_comment_with_whitespace_only_is_not_saved(self):
         ticket = Ticket(self.env)
@@ -310,7 +354,7 @@ class TicketTestCase(unittest.TestCase):
         self.assertEqual('john', ticket['reporter'])
 
         # An unknown field
-        assert ticket['bar'] is None
+        self.assertIsNone(ticket['bar'])
 
         # Custom field
         self.assertEqual('bar', ticket['foo'])
@@ -599,6 +643,19 @@ class TicketCommentEditTestCase(TicketCo
             self.assertEqual((i, t[i], 'joe (%d)' % i,
                              'Comment 1 (%d)' % i), history[i])
 
+    def test_change_listener_comment_modified(self):
+        listener = TestTicketChangeListener_2(self.env)
+        ticket = Ticket(self.env, self.id)
+        ticket.modify_comment(cdate=self.t2, author='jack',
+                              comment='New Comment 2', when=datetime.now(utc))
+
+        self.assertEqual('comment_modified', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t2, listener.cdate)
+        self.assertEqual('jack', listener.author)
+        self.assertEqual('New Comment 2', listener.comment)
+        self.assertEqual('Comment 2', listener.old_comment)
+
 
 class TicketCommentDeleteTestCase(TicketCommentTestCase):
 
@@ -632,8 +689,8 @@ class TicketCommentDeleteTestCase(Ticket
         ticket.delete_change(cnum=4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         self.assertEqual('change3', ticket['foo'])
-        self.assertEqual(None, ticket.get_change(cnum=4))
-        self.assertNotEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=4))
+        self.assertIsNotNone(ticket.get_change(cnum=3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_last_comment_when_custom_field_gone(self):
@@ -651,13 +708,13 @@ class TicketCommentDeleteTestCase(Ticket
         ticket.delete_change(cnum=4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         # 'foo' is no longer defined for the ticket
-        self.assertEqual(None, ticket['foo'])
+        self.assertIsNone(ticket['foo'])
         # however, 'foo=change3' is still in the database
         self.assertEqual([('change3',)], self.env.db_query("""
             SELECT value FROM ticket_custom WHERE ticket=%s AND name='foo'
             """, (self.id,)))
-        self.assertEqual(None, ticket.get_change(cnum=4))
-        self.assertNotEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=4))
+        self.assertIsNotNone(ticket.get_change(cnum=3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_last_comment_by_date(self):
@@ -668,8 +725,8 @@ class TicketCommentDeleteTestCase(Ticket
         ticket.delete_change(cdate=self.t4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         self.assertEqual('change3', ticket['foo'])
-        self.assertEqual(None, ticket.get_change(cdate=self.t4))
-        self.assertNotEqual(None, ticket.get_change(cdate=self.t3))
+        self.assertIsNone(ticket.get_change(cdate=self.t4))
+        self.assertIsNotNone(ticket.get_change(cdate=self.t3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_mid_comment(self):
@@ -680,7 +737,7 @@ class TicketCommentDeleteTestCase(Ticket
             foo=dict(author='joe', old='change3', new='change4'))
         t = datetime.now(utc)
         ticket.delete_change(cnum=3, when=t)
-        self.assertEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -696,7 +753,7 @@ class TicketCommentDeleteTestCase(Ticket
             foo=dict(author='joe', old='change3', new='change4'))
         t = datetime.now(utc)
         ticket.delete_change(cdate=self.t3, when=t)
-        self.assertEqual(None, ticket.get_change(cdate=self.t3))
+        self.assertIsNone(ticket.get_change(cdate=self.t3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -718,7 +775,7 @@ class TicketCommentDeleteTestCase(Ticket
             keywords=dict(author='joe', old='1, 2', new='a'),
             foo=dict(author='joe', old='change3', new='change4'))
         ticket.delete_change(3)
-        self.assertEqual(None, ticket.get_change(3))
+        self.assertIsNone(ticket.get_change(3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -734,6 +791,25 @@ class TicketCommentDeleteTestCase(Ticket
         ticket.delete_change(1, when=t)
         self.assertEqual(t, ticket.time_changed)
 
+    def test_ticket_change_deleted(self):
+        listener = TestTicketChangeListener_2(self.env)
+        ticket = Ticket(self.env, self.id)
+
+        ticket.delete_change(cdate=self.t3, when=datetime.now(utc))
+        self.assertEqual('change_deleted', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t3, listener.cdate)
+        self.assertEqual(dict(keywords=('a, b, c', 'a, b'),
+                              foo=('change2', 'change3')),
+                         listener.changes)
+
+        ticket.delete_change(cnum=2, when=datetime.now(utc))
+        self.assertEqual('change_deleted', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t2, listener.cdate)
+        self.assertEqual(dict(owner=('john', 'jack'),
+                              foo=('change 1', 'change2')),
+                         listener.changes)
 
 class EnumTestCase(unittest.TestCase):
 
@@ -752,14 +828,14 @@ class EnumTestCase(unittest.TestCase):
         prio = Priority(self.env)
         prio.name = 'foo'
         prio.insert()
-        self.assertEqual(True, prio.exists)
+        self.assertTrue(prio.exists)
 
     def test_priority_insert_with_value(self):
         prio = Priority(self.env)
         prio.name = 'bar'
         prio.value = 100
         prio.insert()
-        self.assertEqual(True, prio.exists)
+        self.assertTrue(prio.exists)
 
     def test_priority_update(self):
         prio = Priority(self.env, 'major')
@@ -772,7 +848,7 @@ class EnumTestCase(unittest.TestCase):
         prio = Priority(self.env, 'major')
         self.assertEqual('3', prio.value)
         prio.delete()
-        self.assertEqual(False, prio.exists)
+        self.assertFalse(prio.exists)
         self.assertRaises(TracError, Priority, self.env, 'major')
         prio = Priority(self.env, 'minor')
         self.assertEqual('3', prio.value)
@@ -807,8 +883,9 @@ class MilestoneTestCase(unittest.TestCas
 
     def setUp(self):
         self.env = EnvironmentStub(default_data=True)
-        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
-        os.mkdir(self.env.path)
+        self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+        self.created_at = datetime(2001, 1, 1, tzinfo=utc)
+        self.updated_at = self.created_at + timedelta(seconds=1)
 
     def tearDown(self):
         shutil.rmtree(self.env.path)
@@ -820,12 +897,25 @@ class MilestoneTestCase(unittest.TestCas
             setattr(milestone, k, v)
         return milestone
 
+    def _insert_ticket(self, when=None, **kwargs):
+        ticket = Ticket(self.env)
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        ticket.insert(when or self.created_at)
+        return ticket
+
+    def _update_ticket(self, ticket, author=None, comment=None, when=None,
+                       **kwargs):
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        ticket.save_changes(author, comment, when or self.updated_at)
+
     def test_new_milestone(self):
         milestone = Milestone(self.env)
-        self.assertEqual(False, milestone.exists)
-        self.assertEqual(None, milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertFalse(milestone.exists)
+        self.assertIsNone(milestone.name)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_new_milestone_empty_name(self):
@@ -834,20 +924,20 @@ class MilestoneTestCase(unittest.TestCas
         milestone being correctly detected as non-existent.
         """
         milestone = Milestone(self.env, '')
-        self.assertEqual(False, milestone.exists)
-        self.assertEqual(None, milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertFalse(milestone.exists)
+        self.assertIsNone(milestone.name)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_existing_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
 
         milestone = Milestone(self.env, 'Test')
-        self.assertEqual(True, milestone.exists)
+        self.assertTrue(milestone.exists)
         self.assertEqual('Test', milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_create_and_update_milestone(self):
@@ -868,35 +958,119 @@ class MilestoneTestCase(unittest.TestCas
             WHERE name='Test'
             """))
 
+    def test_move_tickets(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Testing',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.move_tickets('Testing', 'anonymous', 'Move tickets')
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Testing', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+
+    def test_move_tickets_exclude_closed(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Testing',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.move_tickets('Testing', 'anonymous', 'Move tickets',
+                               exclude_closed=True)
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Test', tkt2['milestone'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+        self.assertEqual(self.updated_at, tkt2['changetime'])
+
+    def test_move_tickets_target_doesnt_exist(self):
+        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        milestone = Milestone(self.env, 'Test')
+        self.assertRaises(ResourceNotFound, milestone.move_tickets,
+                          'Testing', 'anonymous')
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Test', tkt1['milestone'])
+        self.assertEqual('Test', tkt2['milestone'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+        self.assertNotEqual(self.updated_at, tkt2['changetime'])
+
     def test_create_milestone_without_name(self):
         milestone = Milestone(self.env)
         self.assertRaises(TracError, milestone.insert)
 
     def test_delete_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
-
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
         milestone = Milestone(self.env, 'Test')
         milestone.delete()
-        self.assertEqual(False, milestone.exists)
+        self.assertFalse(milestone.exists)
         self.assertEqual([],
             self.env.db_query("SELECT * FROM milestone WHERE name='Test'"))
 
-    def test_delete_milestone_retarget_tickets(self):
-        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('', tkt1['milestone'])
+        self.assertEqual('', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+
+    def test_delete_milestone_with_attachment(self):
+        milestone = Milestone(self.env)
+        milestone.name = 'MilestoneWithAttachment'
+        milestone.insert()
+        
+        attachment = Attachment(self.env, 'milestone', milestone.name)
+        attachment.insert('foo.txt', StringIO(), 0, 1)
 
-        tkt1 = Ticket(self.env)
-        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
-        tkt1.insert()
-        tkt2 = Ticket(self.env)
-        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
-        tkt2.insert()
+        milestone.delete()
+        self.assertEqual(False, milestone.exists)
+
+        attachments = Attachment.select(self.env, 'milestone', milestone.name)
+        self.assertRaises(StopIteration, attachments.next)
 
+    def test_delete_milestone_retarget_tickets(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Other',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
         milestone = Milestone(self.env, 'Test')
         milestone.delete(retarget_to='Other')
-        self.assertEqual(False, milestone.exists)
+        self.assertFalse(milestone.exists)
 
-        self.assertEqual('Other', Ticket(self.env, tkt1.id)['milestone'])
-        self.assertEqual('Other', Ticket(self.env, tkt2.id)['milestone'])
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Other', tkt1['milestone'])
+        self.assertEqual('Other', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
 
     def test_update_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
@@ -920,23 +1094,6 @@ class MilestoneTestCase(unittest.TestCas
         milestone.name = None
         self.assertRaises(TracError, milestone.update)
 
-    def test_update_milestone_update_tickets(self):
-        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
-
-        tkt1 = Ticket(self.env)
-        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
-        tkt1.insert()
-        tkt2 = Ticket(self.env)
-        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
-        tkt2.insert()
-
-        milestone = Milestone(self.env, 'Test')
-        milestone.name = 'Testing'
-        milestone.update()
-
-        self.assertEqual('Testing', Ticket(self.env, tkt1.id)['milestone'])
-        self.assertEqual('Testing', Ticket(self.env, tkt2.id)['milestone'])
-
     def test_rename_milestone(self):
         milestone = Milestone(self.env)
         milestone.name = 'OldName'
@@ -958,6 +1115,24 @@ class MilestoneTestCase(unittest.TestCas
         self.assertEqual('foo.txt', attachments.next().filename)
         self.assertRaises(StopIteration, attachments.next)
 
+    def test_rename_milestone_retarget_tickets(self):
+        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.name = 'Testing'
+        milestone.update()
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Testing', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+
     def test_select_milestones(self):
         self.env.db_transaction.executemany(
             "INSERT INTO milestone (name) VALUES (%s)",
@@ -965,9 +1140,9 @@ class MilestoneTestCase(unittest.TestCas
 
         milestones = list(Milestone.select(self.env))
         self.assertEqual('1.0', milestones[0].name)
-        assert milestones[0].exists
+        self.assertTrue(milestones[0].exists)
         self.assertEqual('2.0', milestones[1].name)
-        assert milestones[1].exists
+        self.assertTrue(milestones[1].exists)
 
     def test_change_listener_created(self):
         listener = TestMilestoneChangeListener(self.env)
@@ -999,10 +1174,10 @@ class MilestoneTestCase(unittest.TestCas
         listener = TestMilestoneChangeListener(self.env)
         milestone = self._create_milestone(name='Milestone 1')
         milestone.insert()
-        self.assertEqual(True, milestone.exists)
+        self.assertTrue(milestone.exists)
         milestone.delete()
         self.assertEqual('Milestone 1', milestone.name)
-        self.assertEqual(False, milestone.exists)
+        self.assertFalse(milestone.exists)
         self.assertEqual('deleted', listener.action)
         self.assertEqual(milestone, listener.milestone)
 
@@ -1180,13 +1355,13 @@ class TicketResourceChangeListenerTestCa
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TicketCommentEditTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TicketCommentDeleteTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(EnumTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(VersionTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(TicketTestCase))
+    suite.addTest(unittest.makeSuite(TicketCommentEditTestCase))
+    suite.addTest(unittest.makeSuite(TicketCommentDeleteTestCase))
+    suite.addTest(unittest.makeSuite(EnumTestCase))
+    suite.addTest(unittest.makeSuite(MilestoneTestCase))
+    suite.addTest(unittest.makeSuite(ComponentTestCase))
+    suite.addTest(unittest.makeSuite(VersionTestCase))
     suite.addTest(unittest.makeSuite(
         ComponentResourceChangeListenerTestCase, 'test'))
     suite.addTest(unittest.makeSuite(

Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/notification.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/notification.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/notification.py
 (original)
+++ 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/notification.py
 Sat Nov 15 01:14:46 2014
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.b...@free.fr>
 # All rights reserved.
 #
@@ -16,126 +16,244 @@
 # (lsmith...@open-networks.co.uk) extensible Python SMTP Server
 #
 
-from trac.util.datefmt import utc
-from trac.ticket.model import Ticket
-from trac.ticket.notification import TicketNotifyEmail
-from trac.test import EnvironmentStub, Mock, MockPerm
-from trac.tests.notification import SMTPThreadedServer, parse_smtp_message, \
-                                    smtp_address
-
 import base64
-from datetime import datetime
 import os
 import quopri
 import re
 import unittest
+from datetime import datetime
+
+import trac.tests.compat
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.tests.notification import SMTP_TEST_PORT, SMTPThreadedServer,\
+                                    parse_smtp_message
+from trac.ticket.model import Ticket
+from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket.web_ui import TicketModule
+from trac.util.datefmt import utc
 
-SMTP_TEST_PORT = 7000 + os.getpid() % 1000
 MAXBODYWIDTH = 76
 notifysuite = None
 
 
-class NotificationTestCase(unittest.TestCase):
-    """Notification test cases that send email over SMTP"""
+class RecipientTestCase(unittest.TestCase):
+    """Notification test cases for email recipients."""
 
     def setUp(self):
         self.env = EnvironmentStub(default_data=True)
         self.env.config.set('project', 'name', 'TracTest')
         self.env.config.set('notification', 'smtp_enabled', 'true')
-        self.env.config.set('notification', 'always_notify_owner', 'true')
-        self.env.config.set('notification', 'always_notify_reporter', 'true')
-        self.env.config.set('notification', 'smtp_always_cc',
-                            'joe.u...@example.net, joe....@example.net')
-        self.env.config.set('notification', 'use_public_cc', 'true')
         self.env.config.set('notification', 'smtp_port', str(SMTP_TEST_PORT))
-        self.env.config.set('notification', 'smtp_server','localhost')
-        self.req = Mock(href=self.env.href, abs_href=self.env.abs_href, tz=utc,
-                        perm=MockPerm())
 
     def tearDown(self):
-        """Signal the notification test suite that a test is over"""
         notifysuite.tear_down()
         self.env.reset_db()
 
-    def test_recipients(self):
-        """To/Cc recipients"""
+    def test_no_recipients(self):
+        """No recipient case"""
         ticket = Ticket(self.env)
-        ticket['reporter'] = '"Joe User" < joe.u...@example.org >'
-        ticket['owner']    = 'joe.u...@example.net'
-        ticket['cc']       = 'joe.u...@example.com, joe....@example.org, ' \
-                             'joe....@example.net'
+        ticket['reporter'] = 'anonymous'
         ticket['summary'] = 'Foo'
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         recipients = notifysuite.smtpd.get_recipients()
-        # checks there is no duplicate in the recipient list
-        rcpts = []
-        for r in recipients:
-            self.failIf(r in rcpts)
-            rcpts.append(r)
-        # checks that all cc recipients have been notified
-        cc_list = self.env.config.get('notification', 'smtp_always_cc')
-        cc_list = "%s, %s" % (cc_list, ticket['cc'])
-        for r in cc_list.replace(',', ' ').split():
-            self.failIf(r not in recipients)
-        # checks that owner has been notified
-        self.failIf(smtp_address(ticket['owner']) not in recipients)
-        # checks that reporter has been notified
-        self.failIf(smtp_address(ticket['reporter']) not in recipients)
-
-    def test_no_recipient(self):
-        """No recipient case"""
-        self.env.config.set('notification', 'smtp_always_cc', '')
+        sender = notifysuite.smtpd.get_sender()
+        message = notifysuite.smtpd.get_message()
+        self.assertEqual(0, len(recipients))
+        self.assertIsNone(sender)
+        self.assertIsNone(message)
+
+    def test_new_ticket_recipients(self):
+        """Report and CC list should be in recipient list for new tickets."""
+        always_cc = ('joe.u...@example.net', 'joe....@example.net')
+        ticket_cc = ('joe.u...@example.com', 'joe....@example.org')
+        self.env.config.set('notification', 'smtp_always_cc',
+                            ', '.join(always_cc))
         ticket = Ticket(self.env)
-        ticket['reporter'] = 'anonymous'
-        ticket['summary'] = 'Foo'
+        ticket['reporter'] = 'joe....@example.org'
+        ticket['owner'] = 'joe.u...@example.net'
+        ticket['cc'] = ' '.join(ticket_cc)
+        ticket['summary'] = 'New ticket recipients'
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
-        sender = notifysuite.smtpd.get_sender()
         recipients = notifysuite.smtpd.get_recipients()
-        message = notifysuite.smtpd.get_message()
-        # checks that no message has been sent
-        self.failIf(recipients)
-        self.failIf(sender)
-        self.failIf(message)
+        for r in always_cc + ticket_cc + \
+                (ticket['owner'], ticket['reporter']):
+            self.assertIn(r, recipients)
 
     def test_cc_only(self):
         """Notification w/o explicit recipients but Cc: (#3101)"""
+        always_cc = ('joe.u...@example.net', 'joe....@example.net')
+        self.env.config.set('notification', 'smtp_always_cc',
+                            ', '.join(always_cc))
         ticket = Ticket(self.env)
         ticket['summary'] = 'Foo'
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         recipients = notifysuite.smtpd.get_recipients()
-        # checks that all cc recipients have been notified
-        cc_list = self.env.config.get('notification', 'smtp_always_cc')
-        for r in cc_list.replace(',', ' ').split():
-            self.failIf(r not in recipients)
+        for r in always_cc:
+            self.assertIn(r, recipients)
+
+    def test_always_notify_updater(self):
+        """The `always_notify_updater` option."""
+        def _test_updater(enabled):
+            self.env.config.set('notification', 'always_notify_updater',
+                                enabled)
+            ticket = Ticket(self.env)
+            ticket['reporter'] = 'joe.u...@example.org'
+            ticket['summary'] = u'This is a súmmäry'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('joe.b...@example.com', 'This is a change',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=False, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertIn('joe.b...@example.com', recipients)
+            else:
+                self.assertEqual(0, len(recipients))
+                self.assertNotIn('joe.b...@example.com', recipients)
+
+        # Validate with and without a default domain
+        for enable in False, True:
+            _test_updater(enable)
+
+    def test_always_notify_owner(self):
+        """The `always_notify_owner` option."""
+        def _test_reporter(enabled):
+            self.env.config.set('notification', 'always_notify_owner',
+                                enabled)
+            self.env.config.set('notification', 'always_notify_updater',
+                                'false')
+            ticket = Ticket(self.env)
+            ticket['summary'] = 'Foo'
+            ticket['reporter'] = u'j...@example.org'
+            ticket['owner'] = u'j...@example.org'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('j...@example.org', 'this is my comment',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=True, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertEqual('j...@example.org', recipients[0])
+            else:
+                self.assertEqual(0, len(recipients))
+
+        for enable in False, True:
+            _test_reporter(enable)
+
+    def test_always_notify_reporter(self):
+        """Notification to reporter w/ updater option disabled (#3780)"""
+        def _test_reporter(enabled):
+            self.env.config.set('notification', 'always_notify_updater',
+                                'false')
+            self.env.config.set('notification', 'always_notify_reporter',
+                                enabled)
+            ticket = Ticket(self.env)
+            ticket['summary'] = 'Foo'
+            ticket['reporter'] = u'j...@example.org'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('j...@example.org', 'this is my comment',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=True, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertEqual('j...@example.org', recipients[0])
+            else:
+                self.assertEqual(0, len(recipients))
+
+        for enable in False, True:
+            _test_reporter(enable)
+
+    def test_no_duplicates(self):
+        """Email addresses should be found only once in the recipient list."""
+        self.env.config.set('notification', 'smtp_always_cc',
+                            'joe.u...@example.com')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.u...@example.com'
+        ticket['owner'] = 'joe.u...@example.com'
+        ticket['cc'] = 'joe.u...@example.com'
+        ticket['summary'] = 'No duplicates'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertEqual(1, len(recipients))
+        self.assertIn('joe.u...@example.com', recipients)
+
+    def test_long_forms(self):
+        """Long forms of SMTP email addresses 'Display Name <address>'"""
+        self.env.config.set('notification', 'always_notify_owner', True)
+        ticket = Ticket(self.env)
+        ticket['reporter'] = '"Joe" <joe.u...@example.com>'
+        ticket['owner'] = 'Joe <joe.u...@example.net>'
+        ticket['cc'] = 'Joe < joe.u...@example.org >'
+        ticket['summary'] = 'Long form'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertEqual(3, len(recipients))
+        self.assertIn('joe.u...@example.com', recipients)
+        self.assertIn('joe.u...@example.net', recipients)
+        self.assertIn('joe.u...@example.org', recipients)
+
+
+class NotificationTestCase(unittest.TestCase):
+    """Notification test cases that send email over SMTP"""
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.env.config.set('project', 'name', 'TracTest')
+        self.env.config.set('notification', 'smtp_enabled', 'true')
+        self.env.config.set('notification', 'always_notify_owner', 'true')
+        self.env.config.set('notification', 'always_notify_reporter', 'true')
+        self.env.config.set('notification', 'smtp_always_cc',
+                            'joe.u...@example.net, joe....@example.net')
+        self.env.config.set('notification', 'use_public_cc', 'true')
+        self.env.config.set('notification', 'smtp_port', str(SMTP_TEST_PORT))
+        self.env.config.set('notification', 'smtp_server', 'localhost')
+        self.req = Mock(href=self.env.href, abs_href=self.env.abs_href, tz=utc,
+                        perm=MockPerm())
+
+    def tearDown(self):
+        """Signal the notification test suite that a test is over"""
+        notifysuite.tear_down()
+        self.env.reset_db()
 
     def test_structure(self):
         """Basic SMTP message structure (headers, body)"""
         ticket = Ticket(self.env)
         ticket['reporter'] = '"Joe User" <joe.u...@example.org>'
-        ticket['owner']    = 'joe.u...@example.net'
-        ticket['cc']       = 'joe.u...@example.com, joe....@example.org, ' \
-                             'joe....@example.net'
+        ticket['owner'] = 'joe.u...@example.net'
+        ticket['cc'] = 'joe.u...@example.com, joe....@example.org, ' \
+                       'joe....@example.net'
         ticket['summary'] = 'This is a summary'
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # checks for header existence
-        self.failIf(not headers)
-        # checks for body existance
-        self.failIf(not body)
+        self.assertTrue(headers)
+        # checks for body existence
+        self.assertTrue(body)
         # checks for expected headers
-        self.failIf('Date' not in headers)
-        self.failIf('Subject' not in headers)
-        self.failIf('Message-ID' not in headers)
-        self.failIf('From' not in headers)
+        self.assertIn('Date', headers)
+        self.assertIn('Subject', headers)
+        self.assertIn('Message-ID', headers)
+        self.assertIn('From', headers)
 
     def test_date(self):
         """Date format compliance (RFC822)
@@ -148,7 +266,7 @@ class NotificationTestCase(unittest.Test
         date_re = re.compile(date_str)
         # python time module does not detect incorrect time values
         days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', \
+        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                   'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
         tz = ['UT', 'GMT', 'EST', 'EDT', 'CST', 'CDT', 'MST', 'MDT',
               'PST', 'PDT']
@@ -159,24 +277,23 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
-        self.failIf('Date' not in headers)
+        headers, body = parse_smtp_message(message)
+        self.assertIn('Date', headers)
         mo = date_re.match(headers['Date'])
-        self.failIf(not mo)
+        self.assertTrue(mo)
         if mo.group('day'):
-            self.failIf(mo.group('day') not in days)
-        self.failIf(int(mo.group('dm')) not in range(1, 32))
-        self.failIf(mo.group('month') not in months)
-        self.failIf(int(mo.group('hour')) not in range(0, 24))
+            self.assertIn(mo.group('day'), days)
+        self.assertIn(int(mo.group('dm')), range(1, 32))
+        self.assertIn(mo.group('month'), months)
+        self.assertIn(int(mo.group('hour')), range(0, 24))
         if mo.group('tz'):
-            self.failIf(mo.group('tz') not in tz)
+            self.assertIn(mo.group('tz'), tz)
 
     def test_bcc_privacy(self):
         """Visibility of recipients"""
-        def run_bcc_feature(public):
+        def run_bcc_feature(public_cc):
             # CC list should be private
-            self.env.config.set('notification', 'use_public_cc',
-                                'true' if public else 'false')
+            self.env.config.set('notification', 'use_public_cc', public_cc)
             self.env.config.set('notification', 'smtp_always_bcc',
                                 'joe.foo...@example.net')
             ticket = Ticket(self.env)
@@ -186,15 +303,15 @@ class NotificationTestCase(unittest.Test
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
-            if public:
+            headers, body = parse_smtp_message(message)
+            if public_cc:
                 # Msg should have a To list
-                self.failIf('To' not in headers)
+                self.assertIn('To', headers)
                 # Extract the list of 'To' recipients from the message
                 to = [rcpt.strip() for rcpt in headers['To'].split(',')]
             else:
                 # Msg should not have a To list
-                self.failIf('To' in headers)
+                self.assertNotIn('To', headers)
                 # Extract the list of 'To' recipients from the message
                 to = []
             # Extract the list of 'Cc' recipients from the message
@@ -207,20 +324,20 @@ class NotificationTestCase(unittest.Test
             for rcpt in cclist:
                 # Each recipient of the 'Cc' list should appear
                 # in the 'Cc' header
-                self.failIf(rcpt not in cc)
+                self.assertIn(rcpt, cc)
                 # Check the message has actually been sent to the recipients
-                self.failIf(rcpt not in rcptlist)
+                self.assertIn(rcpt, rcptlist)
             # Build the list of the expected 'Bcc' recipients
             bccrcpt = self.env.config.get('notification', 'smtp_always_bcc')
             bcclist = [bccr.strip() for bccr in bccrcpt.split(',')]
             for rcpt in bcclist:
                 # Check none of the 'Bcc' recipients appears
                 # in the 'To' header
-                self.failIf(rcpt in to)
+                self.assertNotIn(rcpt, to)
                 # Check the message has actually been sent to the recipients
-                self.failIf(rcpt not in rcptlist)
-        run_bcc_feature(True)
-        run_bcc_feature(False)
+                self.assertIn(rcpt, rcptlist)
+        for public in False, True:
+            run_bcc_feature(public)
 
     def test_short_login(self):
         """Email addresses without a FQDN"""
@@ -233,31 +350,30 @@ class NotificationTestCase(unittest.Test
             # send a notification even if other addresses are not valid
             self.env.config.set('notification', 'smtp_always_cc',
                                 'joe....@example.net')
-            if enabled:
-                self.env.config.set('notification', 'use_short_addr', 'true')
+            self.env.config.set('notification', 'use_short_addr', enabled)
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
+            headers, body = parse_smtp_message(message)
             # Msg should not have a 'To' header
             if not enabled:
-                self.failIf('To' in headers)
+                self.assertNotIn('To', headers)
             else:
                 tolist = [addr.strip() for addr in headers['To'].split(',')]
             # Msg should have a 'Cc' field
-            self.failIf('Cc' not in headers)
+            self.assertIn('Cc', headers)
             cclist = [addr.strip() for addr in headers['Cc'].split(',')]
             if enabled:
                 # Msg should be delivered to the reporter
-                self.failIf(ticket['reporter'] not in tolist)
+                self.assertIn(ticket['reporter'], tolist)
             else:
                 # Msg should not be delivered to joeuser
-                self.failIf(ticket['reporter'] in cclist)
+                self.assertNotIn(ticket['reporter'], cclist)
             # Msg should still be delivered to the always_cc list
-            self.failIf(self.env.config.get('notification',
-                        'smtp_always_cc') not in cclist)
+            self.assertIn(self.env.config.get('notification',
+                                              'smtp_always_cc'), cclist)
         # Validate with and without the short addr option enabled
-        for enable in [False, True]:
+        for enable in False, True:
             _test_short_login(enable)
 
     def test_default_domain(self):
@@ -282,21 +398,21 @@ class NotificationTestCase(unittest.Test
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
+            headers, body = parse_smtp_message(message)
             # Msg should always have a 'Cc' field
-            self.failIf('Cc' not in headers)
+            self.assertIn('Cc', headers)
             cclist = [addr.strip() for addr in headers['Cc'].split(',')]
-            self.failIf('joewith...@example.com' not in cclist)
-            self.failIf('joe....@example.net' not in cclist)
-            if not enabled:
-                self.failIf(len(cclist) != 2)
-                self.failIf('joenodom' in cclist)
+            self.assertIn('joewith...@example.com', cclist)
+            self.assertIn('joe....@example.net', cclist)
+            if enabled:
+                self.assertEqual(3, len(cclist))
+                self.assertIn('joeno...@example.org', cclist)
             else:
-                self.failIf(len(cclist) != 3)
-                self.failIf('joeno...@example.org' not in cclist)
+                self.assertEqual(2, len(cclist))
+                self.assertNotIn('joenodom', cclist)
 
         # Validate with and without a default domain
-        for enable in [False, True]:
+        for enable in False, True:
             _test_default_domain(enable)
 
     def test_email_map(self):
@@ -317,15 +433,15 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('To' not in headers)
+        self.assertIn('To', headers)
         tolist = [addr.strip() for addr in headers['To'].split(',')]
         # 'To' list should have been resolved to the real email address
-        self.failIf('user-...@example.com' not in tolist)
-        self.failIf('user-...@example.com' not in tolist)
-        self.failIf('joeuser' in tolist)
-        self.failIf('jim@domain' in tolist)
+        self.assertIn('user-...@example.com', tolist)
+        self.assertIn('user-...@example.com', tolist)
+        self.assertNotIn('joeuser', tolist)
+        self.assertNotIn('jim@domain', tolist)
 
     def test_from_author(self):
         """Using the reporter or change author as the notification sender"""
@@ -346,7 +462,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Joe User" <user-...@example.com>', headers['From'])
         # Ticket change uses the change author
         ticket['summary'] = 'Modified summary'
@@ -354,7 +470,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Jim User" <user-...@example.com>', headers['From'])
         # Known author without name uses e-mail address only
         ticket['summary'] = 'Final summary'
@@ -362,7 +478,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('user-non...@example.com', headers['From'])
         # Known author without e-mail uses smtp_from and smtp_from_name
         ticket['summary'] = 'Other summary'
@@ -370,7 +486,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"My Trac" <t...@example.com>', headers['From'])
         # Unknown author with name and e-mail address
         ticket['summary'] = 'Some summary'
@@ -378,7 +494,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Test User" <t...@example.com>', headers['From'])
         # Unknown author with e-mail address only
         ticket['summary'] = 'Some summary'
@@ -386,7 +502,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('t...@example.com', headers['From'])
         # Unknown author uses smtp_from and smtp_from_name
         ticket['summary'] = 'Better summary'
@@ -394,7 +510,7 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"My Trac" <t...@example.com>', headers['From'])
 
     def test_ignore_domains(self):
@@ -412,16 +528,16 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('To' not in headers)
+        self.assertIn('To', headers)
         tolist = [addr.strip() for addr in headers['To'].split(',')]
         # 'To' list should not contain addresses with non-SMTP domains
-        self.failIf('kerbe...@example.com' in tolist)
-        self.failIf('kerbe...@example.org' in tolist)
+        self.assertNotIn('kerbe...@example.com', tolist)
+        self.assertNotIn('kerbe...@example.org', tolist)
         # 'To' list should have been resolved to the actual email address
-        self.failIf('k...@example.net' not in tolist)
-        self.failIf(len(tolist) != 1)
+        self.assertIn('k...@example.net', tolist)
+        self.assertEqual(1, len(tolist))
 
     def test_admit_domains(self):
         """SMTP domain inclusion"""
@@ -436,16 +552,16 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('Cc' not in headers)
+        self.assertIn('Cc', headers)
         cclist = [addr.strip() for addr in headers['Cc'].split(',')]
         # 'Cc' list should contain addresses with SMTP included domains
-        self.failIf('joe.user@localdomain' not in cclist)
-        self.failIf('joe.user@server' not in cclist)
+        self.assertIn('joe.user@localdomain', cclist)
+        self.assertIn('joe.user@server', cclist)
         # 'Cc' list should not contain non-FQDN domains
-        self.failIf('joe.user@unknown' in cclist)
-        self.failIf(len(cclist) != 2+2)
+        self.assertNotIn('joe.user@unknown', cclist)
+        self.assertEqual(4, len(cclist))
 
     def test_multiline_header(self):
         """Encoded headers split into multiple lines"""
@@ -458,11 +574,11 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Discards the project name & ticket number
         subject = headers['Subject']
         summary = subject[subject.find(':')+2:]
-        self.failIf(ticket['summary'] != summary)
+        self.assertEqual(ticket['summary'], summary)
 
     def test_mimebody_b64(self):
         """MIME Base64/utf-8 encoding"""
@@ -472,8 +588,7 @@ class NotificationTestCase(unittest.Test
         ticket['summary'] = u'This is a long enough summary to cause Trac ' \
                             u'to generate a multi-line (2 lines) súmmäry'
         ticket.insert()
-        self._validate_mimebody((base64, 'base64', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((base64, 'base64', 'utf-8'), ticket, True)
 
     def test_mimebody_qp(self):
         """MIME QP/utf-8 encoding"""
@@ -493,8 +608,7 @@ class NotificationTestCase(unittest.Test
         ticket['reporter'] = 'joe.user'
         ticket['summary'] = u'This is a summary'
         ticket.insert()
-        self._validate_mimebody((None, '7bit', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((None, '7bit', 'utf-8'), ticket, True)
 
     def test_mimebody_none_8bit(self):
         """MIME None encoding resulting in 8bit"""
@@ -503,8 +617,7 @@ class NotificationTestCase(unittest.Test
         ticket['reporter'] = 'joe.user'
         ticket['summary'] = u'This is a summary for Jöe Usèr'
         ticket.insert()
-        self._validate_mimebody((None, '8bit', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((None, '8bit', 'utf-8'), ticket, True)
 
     def test_md5_digest(self):
         """MD5 digest w/ non-ASCII recipient address (#3491)"""
@@ -518,118 +631,77 @@ class NotificationTestCase(unittest.Test
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
+        self.assertEqual('joe.u...@example.org', headers['To'])
+
+    def test_previous_cc_list(self):
+        """Members removed from CC list receive notifications"""
+        ticket = Ticket(self.env)
+        ticket['summary'] = 'Foo'
+        ticket['cc'] = 'joe.us...@example.net'
+        ticket.insert()
+        ticket['cc'] = 'joe.us...@example.net'
+        now = datetime.now(utc)
+        ticket.save_changes('joe....@example.com', 'Removed from cc', now)
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=False, modtime=now)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertIn('joe.us...@example.net', recipients)
+        self.assertIn('joe.us...@example.net', recipients)
 
-    def test_updater(self):
-        """No-self-notification option"""
-        def _test_updater(disable):
-            if disable:
-                self.env.config.set('notification', 'always_notify_updater',
-                                    'false')
+    def test_previous_owner(self):
+        """Previous owner is notified when ticket is reassigned (#2311)
+           if always_notify_owner is set to True"""
+        def _test_owner(enabled):
+            self.env.config.set('notification', 'always_notify_owner', enabled)
             ticket = Ticket(self.env)
-            ticket['reporter'] = 'joe.u...@example.org'
-            ticket['summary'] = u'This is a súmmäry'
-            ticket['cc'] = 'joe....@example.com'
+            ticket['summary'] = 'Foo'
+            ticket['owner'] = prev_owner = 'joe.us...@example.net'
             ticket.insert()
-            ticket['component'] = 'dummy'
+            ticket['owner'] = new_owner = 'joe.us...@example.net'
             now = datetime.now(utc)
-            ticket.save_changes('joe.b...@example.com', 'This is a change',
-                                when=now)
+            ticket.save_changes('joe....@example.com', 'Changed owner', now)
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=False, modtime=now)
-            message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
-            # checks for header existence
-            self.failIf(not headers)
-            # checks for updater in the 'To' recipient list
-            self.failIf('To' not in headers)
-            tolist = [addr.strip() for addr in headers['To'].split(',')]
-            if disable:
-                self.failIf('joe.b...@example.com' in tolist)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertIn(prev_owner, recipients)
+                self.assertIn(new_owner, recipients)
             else:
-                self.failIf('joe.b...@example.com' not in tolist)
-
-        # Validate with and without a default domain
-        for disable in [False, True]:
-            _test_updater(disable)
+                self.assertNotIn(prev_owner, recipients)
+                self.assertNotIn(new_owner, recipients)
 
-    def test_updater_only(self):
-        """Notification w/ updater, w/o any other recipient (#4188)"""
-        self.env.config.set('notification', 'always_notify_owner', 'false')
-        self.env.config.set('notification', 'always_notify_reporter', 'false')
-        self.env.config.set('notification', 'always_notify_updater', 'true')
-        self.env.config.set('notification', 'smtp_always_cc', '')
-        self.env.config.set('notification', 'smtp_always_bcc', '')
-        self.env.config.set('notification', 'use_public_cc', 'false')
-        self.env.config.set('notification', 'use_short_addr', 'false')
-        self.env.config.set('notification', 'smtp_replyto',
-                            'joeu...@example.net')
-        ticket = Ticket(self.env)
-        ticket['summary'] = 'Foo'
-        ticket.insert()
-        ticket['summary'] = 'Bar'
-        ticket['component'] = 'New value'
-        ticket.save_changes('j...@example.com', 'this is my comment')
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        self.failIf(recipients is None)
-        self.failIf(len(recipients) != 1)
-        self.failIf(recipients[0] != 'j...@example.com')
-
-    def test_updater_is_reporter(self):
-        """Notification to reporter w/ updater option disabled (#3780)"""
-        self.env.config.set('notification', 'always_notify_owner', 'false')
-        self.env.config.set('notification', 'always_notify_reporter', 'true')
-        self.env.config.set('notification', 'always_notify_updater', 'false')
-        self.env.config.set('notification', 'smtp_always_cc', '')
-        self.env.config.set('notification', 'smtp_always_bcc', '')
-        self.env.config.set('notification', 'use_public_cc', 'false')
-        self.env.config.set('notification', 'use_short_addr', 'false')
-        self.env.config.set('notification', 'smtp_replyto',
-                            'joeu...@example.net')
-        ticket = Ticket(self.env)
-        ticket['summary'] = 'Foo'
-        ticket['reporter'] = u'j...@example.org'
-        ticket.insert()
-        ticket['summary'] = 'Bar'
-        ticket['component'] = 'New value'
-        ticket.save_changes('j...@example.org', 'this is my comment')
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        self.failIf(recipients is None)
-        self.failIf(len(recipients) != 1)
-        self.failIf(recipients[0] != 'j...@example.org')
+        for enable in False, True:
+            _test_owner(enable)
 
     def _validate_mimebody(self, mime, ticket, newtk):
         """Body of a ticket notification message"""
-        (mime_decoder, mime_name, mime_charset) = mime
+        mime_decoder, mime_name, mime_charset = mime
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=newtk)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
-        self.failIf('MIME-Version' not in headers)
-        self.failIf('Content-Type' not in headers)
-        self.failIf('Content-Transfer-Encoding' not in headers)
-        self.failIf(not re.compile(r"1.\d").match(headers['MIME-Version']))
+        headers, body = parse_smtp_message(message)
+        self.assertIn('MIME-Version', headers)
+        self.assertIn('Content-Type', headers)
+        self.assertIn('Content-Transfer-Encoding', headers)
+        self.assertTrue(re.compile(r"1.\d").match(headers['MIME-Version']))
         type_re = re.compile(r'^text/plain;\scharset="([\w\-\d]+)"$')
         charset = type_re.match(headers['Content-Type'])
-        self.failIf(not charset)
+        self.assertTrue(charset)
         charset = charset.group(1)
-        self.assertEqual(charset, mime_charset)
+        self.assertEqual(mime_charset, charset)
         self.assertEqual(headers['Content-Transfer-Encoding'], mime_name)
         # checks the width of each body line
         for line in body.splitlines():
-            self.failIf(len(line) > MAXBODYWIDTH)
-        # attempts to decode the body, following the specified MIME endoding
+            self.assertTrue(len(line) <= MAXBODYWIDTH)
+        # attempts to decode the body, following the specified MIME encoding
         # and charset
         try:
             if mime_decoder:
                 body = mime_decoder.decodestring(body)
             body = unicode(body, charset)
         except Exception, e:
-            raise AssertionError, e
+            raise AssertionError(e)
         # now processes each line of the body
         bodylines = body.splitlines()
         # body starts with one of more summary lines, first line is prefixed
@@ -637,25 +709,25 @@ class NotificationTestCase(unittest.Test
         # finds the banner after the summary
         banner_delim_re = re.compile(r'^\-+\+\-+$')
         bodyheader = []
-        while ( not banner_delim_re.match(bodylines[0]) ):
+        while not banner_delim_re.match(bodylines[0]):
             bodyheader.append(bodylines.pop(0))
         # summary should be present
-        self.failIf(not bodyheader)
+        self.assertTrue(bodyheader)
         # banner should not be empty
-        self.failIf(not bodylines)
+        self.assertTrue(bodylines)
         # extracts the ticket ID from the first line
-        (tknum, bodyheader[0]) = bodyheader[0].split(' ', 1)
-        self.assertEqual(tknum[0], '#')
+        tknum, bodyheader[0] = bodyheader[0].split(' ', 1)
+        self.assertEqual('#', tknum[0])
         try:
             tkid = int(tknum[1:-1])
-            self.assertEqual(tkid, 1)
+            self.assertEqual(1, tkid)
         except ValueError:
-            raise AssertionError, "invalid ticket number"
-        self.assertEqual(tknum[-1], ':')
+            raise AssertionError("invalid ticket number")
+        self.assertEqual(':', tknum[-1])
         summary = ' '.join(bodyheader)
         self.assertEqual(summary, ticket['summary'])
         # now checks the banner contents
-        self.failIf(not banner_delim_re.match(bodylines[0]))
+        self.assertTrue(banner_delim_re.match(bodylines[0]))
         banner = True
         footer = None
         props = {}
@@ -667,11 +739,11 @@ class NotificationTestCase(unittest.Test
             if banner:
                 # parse banner and fill in a property dict
                 properties = line.split('|')
-                self.assertEqual(len(properties), 2)
+                self.assertEqual(2, len(properties))
                 for prop in properties:
                     if prop.strip() == '':
                         continue
-                    (k, v) = prop.split(':')
+                    k, v = prop.split(':')
                     props[k.strip().lower()] = v.strip()
             # detect footer marker (weak detection)
             if not footer:
@@ -679,10 +751,10 @@ class NotificationTestCase(unittest.Test
                     footer = 0
                     continue
             # check footer
-            if footer != None:
+            if footer is not None:
                 footer += 1
                 # invalid footer detection
-                self.failIf(footer > 3)
+                self.assertTrue(footer <= 3)
                 # check ticket link
                 if line[:11] == 'Ticket URL:':
                     ticket_link = self.env.abs_href.ticket(ticket.id)
@@ -693,12 +765,13 @@ class NotificationTestCase(unittest.Test
         xlist = ['summary', 'description', 'comment', 'time', 'changetime']
         # check banner content (field exists, msg value matches ticket value)
         for p in [prop for prop in ticket.values.keys() if prop not in xlist]:
-            self.failIf(not props.has_key(p))
+            self.assertIn(p, props)
             # Email addresses might be obfuscated
             if '@' in ticket[p] and '@' in props[p]:
-                self.failIf(props[p].split('@')[0] != ticket[p].split('@')[0])
+                self.assertEqual(props[p].split('@')[0],
+                                 ticket[p].split('@')[0])
             else:
-                self.failIf(props[p] != ticket[p])
+                self.assertEqual(props[p], ticket[p])
 
     def test_props_format_ambiwidth_single(self):
         self.env.config.set('notification', 'mime_encoding', 'none')
@@ -827,7 +900,7 @@ Resolution:  fixed                   |  
       Type:  defect                              |     Status:  new
   Priority:  major                               |  Milestone:  milestone1
  Component:  Lorem ipsum dolor sit amet,         |    Version:  2.0
-  consectetur adipisicing elit, sed do eiusmod   |   Keywords:
+  consectetur adipisicing elit, sed do eiusmod   |
   tempor incididunt ut labore et dolore magna    |
   aliqua. Ut enim ad minim veniam, quis nostrud  |
   exercitation ullamco laboris nisi ut aliquip   |
@@ -837,7 +910,7 @@ Resolution:  fixed                   |  
   Excepteur sint occaecat cupidatat non          |
   proident, sunt in culpa qui officia deserunt   |
   mollit anim id est laborum.                    |
-Resolution:  fixed                               |"""
+Resolution:  fixed                               |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_leftside_unicode(self):
@@ -865,10 +938,10 @@ Resolution:  fixed                      
       Type:  defect                              |     Status:  new
   Priority:  major                               |  Milestone:  milestone1
  Component:  Trac は BSD ライセンスのもとで配布  |    Version:  2.0
-  されています。[1:]このライセンスの全文は、配   |   
Keywords:
+  されています。[1:]このライセンスの全文は、配   |
   布ファイルに含まれている [3:COPYING] ファイル  |
   と同じものが[2:オンライン]で参照できます。     |
-Resolution:  fixed                               |"""
+Resolution:  fixed                               |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_rightside(self):
@@ -893,7 +966,7 @@ Resolution:  fixed                      
                               u'culpa qui officia deserunt mollit anim id ' \
                               u'est laborum.'
         ticket['component'] = u'component1'
-        ticket['version'] = u'2.0'
+        ticket['version'] = u'2.0 Standard and International Edition'
         ticket['resolution'] = u'fixed'
         ticket['keywords'] = u''
         ticket.insert()
@@ -901,8 +974,8 @@ Resolution:  fixed                      
   Reporter:  anonymous   |      Owner:  somebody
       Type:  defect      |     Status:  new
   Priority:  major       |  Milestone:  Lorem ipsum dolor sit amet,
- Component:  component1  |  consectetur adipisicing elit, sed do eiusmod
-Resolution:  fixed       |  tempor incididunt ut labore et dolore magna
+                         |  consectetur adipisicing elit, sed do eiusmod
+                         |  tempor incididunt ut labore et dolore magna
                          |  aliqua. Ut enim ad minim veniam, quis nostrud
                          |  exercitation ullamco laboris nisi ut aliquip ex
                          |  ea commodo consequat. Duis aute irure dolor in
@@ -911,8 +984,9 @@ Resolution:  fixed       |  tempor incid
                          |  occaecat cupidatat non proident, sunt in culpa
                          |  qui officia deserunt mollit anim id est
                          |  laborum.
-                         |    Version:  2.0
-                         |   Keywords:"""
+ Component:  component1  |    Version:  2.0 Standard and International
+                         |  Edition
+Resolution:  fixed       |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_rightside_unicode(self):
@@ -937,10 +1011,10 @@ Resolution:  fixed       |  tempor incid
   Reporter:  anonymous   |      Owner:  somebody
       Type:  defect      |     Status:  new
   Priority:  major       |  Milestone:  Trac 
在经过修改的BSD协议下发布。
- Component:  component1  |  
[1:]协议的完整文本可以[2:在线查看]也可在发布版
-Resolution:  fixed       |  的 [3:COPYING] 文件中找到。
-                         |    Version:  2.0
-                         |   Keywords:"""
+                         |  
[1:]协议的完整文本可以[2:在线查看]也可在发布版
+                         |  的 [3:COPYING] 文件中找到。
+ Component:  component1  |    Version:  2.0
+Resolution:  fixed       |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_bothsides(self):
@@ -964,31 +1038,33 @@ Resolution:  fixed       |  的 [3:CO
                               u'occaecat cupidatat non proident, sunt in ' \
                               u'culpa qui officia deserunt mollit anim id ' \
                               u'est laborum.'
-        ticket['component'] = ticket['milestone']
+        ticket['component'] = (u'Lorem ipsum dolor sit amet, consectetur '
+                               u'adipisicing elit, sed do eiusmod tempor '
+                               u'incididunt ut labore et dolore magna aliqua.')
         ticket['version'] = u'2.0'
         ticket['resolution'] = u'fixed'
-        ticket['keywords'] = u''
+        ticket['keywords'] = u'Ut enim ad minim veniam, ....'
         ticket.insert()
         formatted = """\
   Reporter:  anonymous               |      Owner:  somebody
       Type:  defect                  |     Status:  new
   Priority:  major                   |  Milestone:  Lorem ipsum dolor sit
- Component:  Lorem ipsum dolor sit   |  amet, consectetur adipisicing elit,
-  amet, consectetur adipisicing      |  sed do eiusmod tempor incididunt ut
-  elit, sed do eiusmod tempor        |  labore et dolore magna aliqua. Ut
-  incididunt ut labore et dolore     |  enim ad minim veniam, quis nostrud
-  magna aliqua. Ut enim ad minim     |  exercitation ullamco laboris nisi
-  veniam, quis nostrud exercitation  |  ut aliquip ex ea commodo consequat.
-  ullamco laboris nisi ut aliquip    |  Duis aute irure dolor in
-  ex ea commodo consequat. Duis      |  reprehenderit in voluptate velit
-  aute irure dolor in reprehenderit  |  esse cillum dolore eu fugiat nulla
-  in voluptate velit esse cillum     |  pariatur. Excepteur sint occaecat
-  dolore eu fugiat nulla pariatur.   |  cupidatat non proident, sunt in
-  Excepteur sint occaecat cupidatat  |  culpa qui officia deserunt mollit
-  non proident, sunt in culpa qui    |  anim id est laborum.
-  officia deserunt mollit anim id    |    Version:  2.0
-  est laborum.                       |   Keywords:
-Resolution:  fixed                   |"""
+                                     |  amet, consectetur adipisicing elit,
+                                     |  sed do eiusmod tempor incididunt ut
+                                     |  labore et dolore magna aliqua. Ut
+                                     |  enim ad minim veniam, quis nostrud
+                                     |  exercitation ullamco laboris nisi
+                                     |  ut aliquip ex ea commodo consequat.
+                                     |  Duis aute irure dolor in
+                                     |  reprehenderit in voluptate velit
+                                     |  esse cillum dolore eu fugiat nulla
+ Component:  Lorem ipsum dolor sit   |  pariatur. Excepteur sint occaecat
+  amet, consectetur adipisicing      |  cupidatat non proident, sunt in
+  elit, sed do eiusmod tempor        |  culpa qui officia deserunt mollit
+  incididunt ut labore et dolore     |  anim id est laborum.
+  magna aliqua.                      |    Version:  2.0
+Resolution:  fixed                   |   Keywords:  Ut enim ad minim
+                                     |  veniam, ...."""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_bothsides_unicode(self):
@@ -1011,7 +1087,7 @@ Resolution:  fixed                   |""
                               u'に含まれている[3:CОPYING]ファイ' \
                               u'ルと同じものが[2:オンライン]で' \
                               u'参照できます。'
-        ticket['version'] = u'2.0'
+        ticket['version'] = u'2.0 International Edition'
         ticket['resolution'] = u'fixed'
         ticket['keywords'] = u''
         ticket.insert()
@@ -1022,17 +1098,74 @@ Resolution:  fixed                   |""
  Component:  Trac は BSD ライセンス  |  
议下发布。[1:]协议的完整文本可以[2:
   のもとで配布されています。[1:]こ   |  
在线查看]也可在发布版的 [3:COPYING]
   のライセンスの全文は、※配布ファ   |  文件中找到。
-  イルに含まれている[3:CОPYING]フ   |    Version:  2.0
-  ァイルと同じものが[2:オンライン]   |   Keywords:
+  イルに含まれている[3:CОPYING]フ   |    Version:  2.0 International
+  ァイルと同じものが[2:オンライン]   |  Edition
   で参照できます。                   |
-Resolution:  fixed                   |"""
+Resolution:  fixed                   |   Keywords:"""
+        self._validate_props_format(formatted, ticket)
+
+    def test_props_format_wrap_ticket_10283(self):
+        self.env.config.set('notification', 'mime_encoding', 'none')
+        for name, value in (('blockedby', 'text'),
+                            ('blockedby.label', 'Blocked by'),
+                            ('blockedby.order', '6'),
+                            ('blocking', 'text'),
+                            ('blocking.label', 'Blocking'),
+                            ('blocking.order', '5'),
+                            ('deployment', 'text'),
+                            ('deployment.label', 'Deployment state'),
+                            ('deployment.order', '1'),
+                            ('nodes', 'text'),
+                            ('nodes.label', 'Related nodes'),
+                            ('nodes.order', '3'),
+                            ('privacy', 'text'),
+                            ('privacy.label', 'Privacy sensitive'),
+                            ('privacy.order', '2'),
+                            ('sensitive', 'text'),
+                            ('sensitive.label', 'Security sensitive'),
+                            ('sensitive.order', '4')):
+            self.env.config.set('ticket-custom', name, value)
+
+        ticket = Ticket(self.env)
+        ticket['summary'] = u'This is a summary'
+        ticket['reporter'] = u'anonymous'
+        ticket['owner'] = u'somebody'
+        ticket['type'] = u'defect'
+        ticket['status'] = u'closed'
+        ticket['priority'] = u'normal'
+        ticket['milestone'] = u'iter_01'
+        ticket['component'] = u'XXXXXXXXXXXXXXXXXXXXXXXXXX'
+        ticket['resolution'] = u'fixed'
+        ticket['keywords'] = u''
+        ticket['deployment'] = ''
+        ticket['privacy'] = '0'
+        ticket['nodes'] = 'XXXXXXXXXX'
+        ticket['sensitive'] = '0'
+        ticket['blocking'] = ''
+        ticket['blockedby'] = ''
+        ticket.insert()
+
+        formatted = """\
+          Reporter:  anonymous                   |             Owner:
+                                                 |  somebody
+              Type:  defect                      |            Status:
+                                                 |  closed
+          Priority:  normal                      |         Milestone:
+                                                 |  iter_01
+         Component:  XXXXXXXXXXXXXXXXXXXXXXXXXX  |        Resolution:
+                                                 |  fixed
+          Keywords:                              |  Deployment state:
+ Privacy sensitive:  0                           |     Related nodes:
+                                                 |  XXXXXXXXXX
+Security sensitive:  0                           |          Blocking:
+        Blocked by:                              |"""
         self._validate_props_format(formatted, ticket)
 
     def _validate_props_format(self, expected, ticket):
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         bodylines = body.splitlines()
         # Extract ticket properties
         delim_re = re.compile(r'^\-+\+\-+$')
@@ -1052,7 +1185,7 @@ Resolution:  fixed                   |""
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
-        self.assertNotEqual(None, notifysuite.smtpd.get_message())
+        self.assertIsNotNone(notifysuite.smtpd.get_message())
         self.assertEqual('My Summary', ticket['summary'])
         self.assertEqual('Some description', ticket['description'])
         valid_fieldnames = set([f['name'] for f in ticket.fields])
@@ -1069,6 +1202,52 @@ Resolution:  fixed                   |""
         tn.ticket = ticket
         tn.get_message_id('foo')
 
+    def test_mime_meta_characters_in_from_header(self):
+        """MIME encoding with meta characters in From header"""
+
+        self.env.config.set('notification', 'smtp_from', 't...@example.com')
+        self.env.config.set('notification', 'mime_encoding', 'base64')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joeuser'
+        ticket['summary'] = 'This is a summary'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+
+        def notify(from_name):
+            self.env.config.set('notification', 'smtp_from_name', from_name)
+            tn.notify(ticket, newticket=True)
+            message = notifysuite.smtpd.get_message()
+            headers, body = parse_smtp_message(message)
+            return message, headers, body
+
+        message, headers, body = notify(u'Träc')
+        self.assertEqual(r'"=?utf-8?b?VHLDpGM=?=" <t...@example.com>',
+                         headers['From'])
+        message, headers, body = notify(u'Trac\\')
+        self.assertEqual(r'"Trac\\" <t...@example.com>', headers['From'])
+        message, headers, body = notify(u'Trac"')
+        self.assertEqual(r'"Trac\"" <t...@example.com>', headers['From'])
+        message, headers, body = notify(u'=?utf-8?b?****?=')
+        self.assertEqual('"=?utf-8?b?PT91dGYtOD9iPyoqKio/PQ==?=" '
+                         '<t...@example.com>', headers['From'])
+
+    def test_mime_meta_characters_in_subject_header(self):
+        """MIME encoding with meta characters in Subject header"""
+
+        self.env.config.set('notification', 'smtp_from', 't...@example.com')
+        self.env.config.set('notification', 'mime_encoding', 'base64')
+        summary = u'=?utf-8?q?****?='
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joeuser'
+        ticket['summary'] = summary
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        headers, body = parse_smtp_message(message)
+        self.assertIn('\nSubject: =?utf-8?b?', message)  # is mime-encoded
+        self.assertEqual(summary,
+                         re.split(r' #[0-9]+: ', headers['Subject'], 1)[1])
 
 
 class NotificationTestSuite(unittest.TestSuite):
@@ -1079,18 +1258,20 @@ class NotificationTestSuite(unittest.Tes
         unittest.TestSuite.__init__(self)
         self.smtpd = SMTPThreadedServer(SMTP_TEST_PORT)
         self.smtpd.start()
-        self.addTest(unittest.makeSuite(NotificationTestCase, 'test'))
+        self.addTest(unittest.makeSuite(RecipientTestCase))
+        self.addTest(unittest.makeSuite(NotificationTestCase))
         self.remaining = self.countTestCases()
 
     def tear_down(self):
         """Reset the local SMTP test server"""
         self.smtpd.cleanup()
-        self.remaining = self.remaining-1
+        self.remaining -= 1
         if self.remaining > 0:
             return
         # stop the SMTP test server when all tests have been completed
         self.smtpd.stop()
 
+
 def suite():
     global notifysuite
     if not notifysuite:
@@ -1098,4 +1279,4 @@ def suite():
     return notifysuite
 
 if __name__ == '__main__':
-    unittest.TextTestRunner(verbosity=2).run(suite())
+    unittest.main(defaultTest='suite')


Reply via email to