You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by rj...@apache.org on 2014/11/15 02:14:53 UTC

svn commit: r1639823 [17/29] - in /bloodhound/branches/trac-1.0.2-integration/trac: ./ contrib/ contrib/cgi-bin/ contrib/workflow/ doc/ doc/dev/ doc/utils/ sample-plugins/ sample-plugins/permissions/ sample-plugins/workflow/ trac/ trac/admin/ trac/admi...

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/functional.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/functional.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/functional.py (original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/tests/functional.py Sat Nov 15 01:14:46 2014
@@ -1,24 +1,89 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 import os
 import re
 
 from datetime import datetime, timedelta
 
+from trac.admin.tests.functional import AuthorizationTestCaseSetup
 from trac.test import locale_en
 from trac.tests.functional import *
+from trac.util import create_file
 from trac.util.datefmt import utc, localtz, format_date, format_datetime
+from trac.util.text import to_utf8
+
+try:
+    from configobj import ConfigObj
+except ImportError:
+    ConfigObj = None
 
 
 class TestTickets(FunctionalTwillTestCaseSetup):
     def runTest(self):
-        """Create a ticket, comment on it, and attach a file"""
+        """Create a ticket and comment on it."""
         # TODO: this should be split into multiple tests
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.create_ticket()
-        self._tester.add_comment(ticketid)
-        self._tester.attach_file_to_ticket(ticketid)
+        id = self._tester.create_ticket()
+        self._tester.add_comment(id)
+
+
+class TestTicketMaxSummarySize(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test `[ticket] max_summary_size` option.
+        http://trac.edgewall.org/ticket/11472"""
+        prev_max_summary_size = \
+            self._testenv.get_config('ticket', 'max_summary_size')
+        short_summary = "abcdefghijklmnopqrstuvwxyz"
+        long_summary = short_summary + "."
+        max_summary_size = len(short_summary)
+        warning_message = r"Ticket summary is too long \(must be less " \
+                          r"than %s characters\)" % max_summary_size
+        self._testenv.set_config('ticket', 'max_summary_size',
+                                 str(max_summary_size))
+        try:
+            self._tester.create_ticket(short_summary)
+            tc.find(short_summary)
+            tc.notfind(warning_message)
+            self._tester.go_to_front()
+            tc.follow(r"\bNew Ticket\b")
+            tc.notfind(internal_error)
+            tc.url(self._tester.url + '/newticket')
+            tc.formvalue('propertyform', 'field_summary', long_summary)
+            tc.submit('submit')
+            tc.url(self._tester.url + '/newticket')
+            tc.find(warning_message)
+        finally:
+            self._testenv.set_config('ticket', 'max_summary_size',
+                                     prev_max_summary_size)
+
+
+class TestTicketAddAttachment(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Add attachment to a ticket. Test that the attachment button
+        reads 'Attach file' when no files have been attached, and 'Attach
+        another file' when there are existing attachments.
+        Feature added in http://trac.edgewall.org/ticket/10281"""
+        id = self._tester.create_ticket()
+        tc.find("Attach file")
+        filename = self._tester.attach_file_to_ticket(id)
+
+        self._tester.go_to_ticket(id)
+        tc.find("Attach another file")
+        tc.find('Attachments <span class="trac-count">\(1\)</span>')
+        tc.find(filename)
+        tc.find('Download all attachments as:\s+<a rel="nofollow" '
+                'href="/zip-attachment/ticket/%s/">.zip</a>' % id)
 
 
 class TestTicketPreview(FunctionalTwillTestCaseSetup):
@@ -51,27 +116,72 @@ class TestTicketNoSummary(FunctionalTwil
         tc.find('ticket not yet created')
 
 
+class TestTicketManipulator(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        plugin_name = self.__class__.__name__
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', plugin_name + '.*', 'enabled')
+        env.config.save()
+        create_file(os.path.join(env.path, 'plugins', plugin_name + '.py'),
+"""\
+from genshi.builder import tag
+from trac.core import Component, implements
+from trac.ticket.api import ITicketManipulator
+from trac.util.translation import tag_
+
+
+class TicketManipulator(Component):
+    implements(ITicketManipulator)
+
+    def prepare_ticket(self, req, ticket, fields, actions):
+        pass
+
+    def validate_ticket(self, req, ticket):
+        field = 'reporter'
+        yield None, tag_("A ticket with the summary %(summary)s"
+                         " already exists.",
+                          summary=tag.em("Testing ticket manipulator"))
+        yield field, tag_("The ticket %(field)s is %(status)s.",
+                          field=tag.strong(field),
+                          status=tag.em("invalid"))
+""")
+        self._testenv.restart()
+
+        try:
+            self._tester.go_to_front()
+            tc.follow("New Ticket")
+            tc.formvalue('propertyform', 'field-description',
+                         "Testing ticket manipulator")
+            tc.submit('submit')
+            tc.url(self._tester.url + '/newticket$')
+            tc.find("A ticket with the summary <em>Testing ticket "
+                    "manipulator</em> already exists.")
+            tc.find("The ticket field 'reporter' is invalid: The"
+                    " ticket <strong>reporter</strong> is <em>invalid</em>.")
+        finally:
+            env.config.set('components', plugin_name + '.*', 'disabled')
+            env.config.save()
+
+
 class TestTicketAltFormats(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in alternative formats"""
         summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(summary)
         for format in ['Comma-delimited Text', 'Tab-delimited Text',
                        'RSS Feed']:
             tc.follow(format)
             content = b.get_html()
             if content.find(summary) < 0:
-                raise AssertionError('Summary missing from %s format' % format)
+                raise AssertionError('Summary missing from %s format'
+                                     % format)
             tc.back()
 
 
 class TestTicketCSVFormat(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in CSV format"""
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
         tc.follow('Comma-delimited Text')
         csv = b.get_html()
         if not csv.startswith('\xef\xbb\xbfid,summary,'): # BOM
@@ -80,24 +190,20 @@ class TestTicketCSVFormat(FunctionalTest
 
 class TestTicketTabFormat(FunctionalTestCaseSetup):
     def runTest(self):
-        """Download ticket in Tab-delimitted format"""
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        """Download ticket in Tab-delimited format"""
+        self._tester.create_ticket()
         tc.follow('Tab-delimited Text')
         tab = b.get_html()
         if not tab.startswith('\xef\xbb\xbfid\tsummary\t'): # BOM
-            raise AssertionError('Bad tab delimitted format')
+            raise AssertionError('Bad tab delimited format')
 
 
 class TestTicketRSSFormat(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in RSS format"""
         summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(summary)
         # Make a number of changes to exercise all of the RSS feed code
-        self._tester.go_to_ticket(ticketid)
         tc.formvalue('propertyform', 'comment', random_sentence(3))
         tc.formvalue('propertyform', 'field-type', 'task')
         tc.formvalue('propertyform', 'description', summary + '\n\n' +
@@ -119,7 +225,7 @@ class TestTicketSearch(FunctionalTwillTe
     def runTest(self):
         """Test ticket search"""
         summary = random_sentence(4)
-        ticketid = self._tester.create_ticket(summary)
+        self._tester.create_ticket(summary)
         self._tester.go_to_front()
         tc.follow('Search')
         tc.formvalue('fullsearch', 'ticket', True)
@@ -135,7 +241,7 @@ class TestNonTicketSearch(FunctionalTwil
         # Create a summary containing only unique words
         summary = ' '.join([random_word() + '_TestNonTicketSearch'
                             for i in range(5)])
-        ticketid = self._tester.create_ticket(summary)
+        self._tester.create_ticket(summary)
         self._tester.go_to_front()
         tc.follow('Search')
         tc.formvalue('fullsearch', 'ticket', False)
@@ -150,9 +256,14 @@ class TestTicketHistory(FunctionalTwillT
         """Test ticket history"""
         summary = random_sentence(5)
         ticketid = self._tester.create_ticket(summary)
-        comment = random_sentence(5)
-        self._tester.add_comment(ticketid, comment=comment)
+        comment = self._tester.add_comment(ticketid)
         self._tester.go_to_ticket(ticketid)
+        tc.find(r'<a [^>]+>\bModify\b</a>')
+        tc.find(r"\bAttach file\b")
+        tc.find(r"\bAdd Comment\b")
+        tc.find(r"\bModify Ticket\b")
+        tc.find(r"\bPreview\b")
+        tc.find(r"\bSubmit changes\b")
         url = b.get_url()
         tc.go(url + '?version=0')
         tc.find('at <[^>]*>*Initial Version')
@@ -162,19 +273,23 @@ class TestTicketHistory(FunctionalTwillT
         tc.find('at <[^>]*>*Version 1')
         tc.find(summary)
         tc.find(comment)
+        tc.notfind(r'<a [^>]+>\bModify\b</a>')
+        tc.notfind(r"\bAttach file\b")
+        tc.notfind(r"\bAdd Comment\b")
+        tc.notfind(r"\bModify Ticket\b")
+        tc.notfind(r"\bPreview\b")
+        tc.notfind(r"\bSubmit changes\b")
 
 
 class TestTicketHistoryDiff(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket history (diff)"""
-        name = 'TestTicketHistoryDiff'
-        ticketid = self._tester.create_ticket(name)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
         tc.formvalue('propertyform', 'description', random_sentence(6))
         tc.submit('submit')
         tc.find('Description<[^>]*>\\s*modified \\(<[^>]*>diff', 's')
         tc.follow('diff')
-        tc.find('Changes\\s*between\\s*<[^>]*>Initial Version<[^>]*>\\s*and' \
+        tc.find('Changes\\s*between\\s*<[^>]*>Initial Version<[^>]*>\\s*and'
                 '\\s*<[^>]*>Version 1<[^>]*>\\s*of\\s*<[^>]*>Ticket #' , 's')
 
 
@@ -214,14 +329,63 @@ class TestTicketQueryLinks(FunctionalTwi
         tc.find('class="missing">Next Ticket &rarr;')
 
 
+class TestTicketQueryLinksQueryModuleDisabled(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Ticket query links should not be present when the QueryModule
+        is disabled."""
+        def enable_query_module(enable):
+            self._tester.go_to_admin('Plugins')
+            tc.formvalue('edit-plugin-trac', 'component',
+                         'trac.ticket.query.QueryModule')
+            tc.formvalue('edit-plugin-trac', 'enable',
+                         '%strac.ticket.query.QueryModule'
+                         % ('+' if enable else '-'))
+            tc.submit()
+            tc.find("The following component has been %s:"
+                    ".*QueryModule.*\(trac\.ticket\.query\.\*\)"
+                    % ("enabled" if enable else "disabled"))
+        props = {'cc': 'user1, user2',
+                 'component': 'component1',
+                 'keywords': 'kw1, kw2',
+                 'milestone': 'milestone1',
+                 'owner': 'user',
+                 'priority': 'major',
+                 'reporter': 'admin',
+                 'version': '2.0'}
+        tid = self._tester.create_ticket(info=props)
+        milestone_cell = \
+            r'<td headers="h_milestone">\s*' \
+            r'<a class="milestone" href="/milestone/%(milestone)s" ' \
+            r'title=".*">\s*%(milestone)s\s*</a>\s*</td>'\
+            % {'milestone': props['milestone']}
+        try:
+            for field, value in props.iteritems():
+                if field != 'milestone':
+                    links = r', '.join(r'<a href="/query.*>%s</a>'
+                                       % v.strip() for v in value.split(','))
+                    tc.find(r'<td headers="h_%s"( class="searchable")?>'
+                            r'\s*%s\s*</td>' % (field, links))
+                else:
+                    tc.find(milestone_cell)
+            enable_query_module(False)
+            self._tester.go_to_ticket(tid)
+            for field, value in props.iteritems():
+                if field != 'milestone':
+                    tc.find(r'<td headers="h_%s"( class="searchable")?>'
+                            r'\s*%s\s*</td>' % (field, value))
+                else:
+                    tc.find(milestone_cell)
+        finally:
+            enable_query_module(True)
+
+
 class TestTicketQueryOrClause(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket query with an or clauses"""
         count = 3
-        ticket_ids = [self._tester.create_ticket(
-                        summary='TestTicketQueryOrClause%s' % i,
-                        info={'keywords': str(i)})
-                      for i in range(count)]
+        [self._tester.create_ticket(summary='TestTicketQueryOrClause%s' % i,
+                                    info={'keywords': str(i)})
+         for i in range(count)]
         self._tester.go_to_query()
         tc.formvalue('query', '0_owner', '')
         tc.submit('rm_filter_0_owner_0')
@@ -233,7 +397,7 @@ class TestTicketQueryOrClause(Functional
         tc.formvalue('query', '1_keywords', '2')
         tc.submit('update')
         tc.notfind('TestTicketQueryOrClause0')
-        for i in [1, 2]:
+        for i in (1, 2):
             tc.find('TestTicketQueryOrClause%s' % i)
 
 
@@ -249,11 +413,8 @@ class TestTicketCustomFieldTextNoFormat(
         env.config.set('ticket-custom', 'newfield.format', '')
         env.config.save()
 
-        self._testenv.restart()
         val = "%s %s" % (random_unique_camel(), random_word())
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
 
 
@@ -269,11 +430,8 @@ class TestTicketCustomFieldTextAreaNoFor
         env.config.set('ticket-custom', 'newfield.format', '')
         env.config.save()
 
-        self._testenv.restart()
         val = "%s %s" % (random_unique_camel(), random_word())
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
 
 
@@ -290,13 +448,10 @@ class TestTicketCustomFieldTextWikiForma
         env.config.set('ticket-custom', 'newfield.format', 'wiki')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         wiki = '<a [^>]*>%s\??</a> %s' % (word1, word2)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
 
@@ -313,13 +468,10 @@ class TestTicketCustomFieldTextAreaWikiF
         env.config.set('ticket-custom', 'newfield.format', 'wiki')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         wiki = '<p>\s*<a [^>]*>%s\??</a> %s<br />\s*</p>' % (word1, word2)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
 
@@ -338,13 +490,10 @@ class TestTicketCustomFieldTextReference
         env.config.set('ticket-custom', 'newfield.format', 'reference')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         query = 'status=!closed&amp;newfield=%s\+%s' % (word1, word2)
         querylink = '<a href="/query\?%s">%s</a>' % (query, val)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylink)
@@ -364,13 +513,10 @@ class TestTicketCustomFieldTextListForma
         env.config.set('ticket-custom', 'newfield.format', 'list')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         query1 = 'status=!closed&amp;newfield=~%s' % word1
         query2 = 'status=!closed&amp;newfield=~%s' % word2
         querylink1 = '<a href="/query\?%s">%s</a>' % (query1, word1)
@@ -392,9 +538,7 @@ class RegressionTestTicket10828(Function
         env.config.set('ticket-custom', 'newfield.format', 'list')
         env.config.save()
 
-        self._testenv.restart()
-        ticketid = self._tester.create_ticket(summary=random_sentence(3))
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
 
         word1 = random_unique_camel()
         word2 = random_word()
@@ -431,7 +575,7 @@ class RegressionTestTicket10828(Function
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylinks)
 
 
-class TestTimelineTicketDetails(FunctionalTwillTestCaseSetup):
+class TestTicketTimeline(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket details on timeline"""
         env = self._testenv.get_trac_environment()
@@ -439,14 +583,18 @@ class TestTimelineTicketDetails(Function
         env.config.save()
         summary = random_sentence(5)
         ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
         self._tester.add_comment(ticketid)
+
         self._tester.go_to_timeline()
+        tc.formvalue('prefs', 'ticket', True)
+        tc.submit()
+        tc.find('Ticket.*#%s.*created' % ticketid)
         tc.formvalue('prefs', 'ticket_details', True)
         tc.submit()
         htmltags = '(<[^>]*>)*'
-        tc.find('Ticket ' + htmltags + '#' + str(ticketid) + htmltags + ' \\(' +
-                summary + '\\) updated\\s+by\\s+' + htmltags + 'admin', 's')
+        tc.find('Ticket ' + htmltags + '#' + str(ticketid) + htmltags +
+                ' \\(' + summary.split()[0] +
+                ' [^\\)]+\\) updated\\s+by\\s+' + htmltags + 'admin', 's')
 
 
 class TestAdminComponent(FunctionalTwillTestCaseSetup):
@@ -455,10 +603,17 @@ class TestAdminComponent(FunctionalTwill
         self._tester.create_component()
 
 
+class TestAdminComponentAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Components
+        panel."""
+        self.test_authorization('/admin/ticket/components', 'TICKET_ADMIN',
+                                "Manage Components")
+
 class TestAdminComponentDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate component"""
-        name = "DuplicateMilestone"
+        name = "DuplicateComponent"
         self._tester.create_component(name)
         component_url = self._tester.url + "/admin/ticket/components"
         tc.go(component_url)
@@ -521,12 +676,44 @@ class TestAdminComponentDetail(Functiona
         tc.notfind(desc)
 
 
+class TestAdminComponentNoneDefined(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """The table should be hidden and help text shown when there are no
+        components defined (#11103)."""
+        from trac.ticket import model
+        env = self._testenv.get_trac_environment()
+        components = list(model.Component.select(env))
+        self._tester.go_to_admin()
+        tc.follow(r"\bComponents\b")
+
+        try:
+            for comp in components:
+                tc.formvalue('component_table', 'sel', comp.name)
+            tc.submit('remove')
+            tc.notfind('<table class="listing" id="complist">')
+            tc.find("As long as you don't add any items to the list, this "
+                    "field[ \t\n]*will remain completely hidden from the "
+                    "user interface.")
+        finally:
+            for comp in components:
+                self._tester.create_component(comp.name, comp.owner,
+                                              comp.description)
+
+
 class TestAdminMilestone(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create milestone"""
         self._tester.create_milestone()
 
 
+class TestAdminMilestoneAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Milestone
+        panel."""
+        self.test_authorization('/admin/ticket/milestones', 'TICKET_ADMIN',
+                                "Manage Milestones")
+
+
 class TestAdminMilestoneSpace(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create milestone with a space"""
@@ -590,7 +777,8 @@ class TestAdminMilestoneDue(FunctionalTw
         """Admin milestone duedate"""
         name = "DueMilestone"
         duedate = datetime.now(tz=utc)
-        duedate_string = format_datetime(duedate, tzinfo=utc, locale=locale_en)
+        duedate_string = format_datetime(duedate, tzinfo=utc,
+                                         locale=locale_en)
         self._tester.create_milestone(name, due=duedate_string)
         tc.find(duedate_string)
 
@@ -609,13 +797,40 @@ class TestAdminMilestoneDetailDue(Functi
         tc.follow(name)
         tc.url(milestone_url + '/' + name)
         duedate = datetime.now(tz=utc)
-        duedate_string = format_datetime(duedate, tzinfo=utc, locale=locale_en)
+        duedate_string = format_datetime(duedate, tzinfo=utc,
+                                         locale=locale_en)
         tc.formvalue('modifymilestone', 'due', duedate_string)
         tc.submit('save')
         tc.url(milestone_url + '$')
         tc.find(name + '(<[^>]*>|\\s)*'+ duedate_string, 's')
 
 
+class TestAdminMilestoneDetailRename(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Admin rename milestone"""
+        name1 = self._tester.create_milestone()
+        name2 = random_unique_camel()
+        tid = self._tester.create_ticket(info={'milestone': name1})
+        milestone_url = self._tester.url + '/admin/ticket/milestones'
+
+        self._tester.go_to_url(milestone_url)
+        tc.follow(name1)
+        tc.url(milestone_url + '/' + name1)
+        tc.formvalue('modifymilestone', 'name', name2)
+        tc.submit('save')
+
+        tc.find(r"Your changes have been saved\.")
+        tc.find(r"\b%s\b" % name2)
+        tc.notfind(r"\b%s\b" % name1)
+        self._tester.go_to_ticket(tid)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': name2})
+        tc.find('<strong class="trac-field-milestone">Milestone</strong>'
+                '[ \t\n]+changed from <em>%s</em> to <em>%s</em>'
+                % (name1, name2))
+        tc.find("Milestone renamed")
+
+
 class TestAdminMilestoneCompleted(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin milestone completed"""
@@ -642,7 +857,7 @@ class TestAdminMilestoneCompletedFuture(
         tc.follow(name)
         tc.url(milestone_url + '/' + name)
         tc.formvalue('modifymilestone', 'completed', True)
-        cdate = datetime.now(tz=utc) + timedelta(days=1)
+        cdate = datetime.now(tz=utc) + timedelta(days=2)
         cdate_string = format_date(cdate, tzinfo=localtz, locale=locale_en)
         tc.formvalue('modifymilestone', 'completeddate', cdate_string)
         tc.submit('save')
@@ -657,12 +872,22 @@ class TestAdminMilestoneRemove(Functiona
         """Admin remove milestone"""
         name = "MilestoneRemove"
         self._tester.create_milestone(name)
-        milestone_url = self._tester.url + "/admin/ticket/milestones"
+        tid = self._tester.create_ticket(info={'milestone': name})
+        milestone_url = self._tester.url + '/admin/ticket/milestones'
+
         tc.go(milestone_url)
         tc.formvalue('milestone_table', 'sel', name)
         tc.submit('remove')
+
         tc.url(milestone_url + '$')
         tc.notfind(name)
+        self._tester.go_to_ticket(tid)
+        tc.find('<th id="h_milestone" class="missing">'
+                '[ \t\n]*Milestone:[ \t\n]*</th>')
+        tc.find('<strong class="trac-field-milestone">Milestone'
+                '</strong>[ \t\n]*<em>%s</em>[ \t\n]*deleted'
+                % name)
+        tc.find("Milestone deleted")
 
 
 class TestAdminMilestoneRemoveMulti(FunctionalTwillTestCaseSetup):
@@ -717,6 +942,14 @@ class TestAdminPriority(FunctionalTwillT
         self._tester.create_priority()
 
 
+class TestAdminPriorityAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Priority
+        panel."""
+        self.test_authorization('/admin/ticket/priority', 'TICKET_ADMIN',
+                                "Manage Priorities")
+
+
 class TestAdminPriorityDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate priority"""
@@ -846,8 +1079,10 @@ class TestAdminPriorityRenumber(Function
         tc.url(priority_url + '$')
         tc.find(name + '1')
         tc.find(name + '2')
-        tc.formvalue('enumtable', 'value_%s' % (max_priority + 1), str(max_priority + 2))
-        tc.formvalue('enumtable', 'value_%s' % (max_priority + 2), str(max_priority + 1))
+        tc.formvalue('enumtable',
+                     'value_%s' % (max_priority + 1), str(max_priority + 2))
+        tc.formvalue('enumtable',
+                     'value_%s' % (max_priority + 2), str(max_priority + 1))
         tc.submit('apply')
         tc.url(priority_url + '$')
         # Verify that their order has changed.
@@ -873,6 +1108,14 @@ class TestAdminResolution(FunctionalTwil
         self._tester.create_resolution()
 
 
+class TestAdminResolutionAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Resolutions
+        panel."""
+        self.test_authorization('/admin/ticket/resolution', 'TICKET_ADMIN',
+                                "Manage Resolutions")
+
+
 class TestAdminResolutionDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate resolution"""
@@ -888,6 +1131,14 @@ class TestAdminSeverity(FunctionalTwillT
         self._tester.create_severity()
 
 
+class TestAdminSeverityAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Severities
+        panel."""
+        self.test_authorization('/admin/ticket/severity', 'TICKET_ADMIN',
+                                "Manage Severities")
+
+
 class TestAdminSeverityDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate severity"""
@@ -903,6 +1154,14 @@ class TestAdminType(FunctionalTwillTestC
         self._tester.create_type()
 
 
+class TestAdminTypeAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Types
+        panel."""
+        self.test_authorization('/admin/ticket/type', 'TICKET_ADMIN',
+                                "Manage Ticket Types")
+
+
 class TestAdminTypeDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate type"""
@@ -918,6 +1177,13 @@ class TestAdminVersion(FunctionalTwillTe
         self._tester.create_version()
 
 
+class TestAdminVersionAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Versions panel."""
+        self.test_authorization('/admin/ticket/versions', 'TICKET_ADMIN',
+                                "Manage Versions")
+
+
 class TestAdminVersionDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate version"""
@@ -965,7 +1231,8 @@ class TestAdminVersionDetailTime(Functio
         tc.formvalue('modifyversion', 'time', '')
         tc.submit('save')
         tc.url(version_admin + '$')
-        tc.find(name + '(<[^>]*>|\\s)*<[^>]* name="default" value="%s"' % name, 's')
+        tc.find(name + '(<[^>]*>|\\s)*<[^>]* name="default" value="%s"'
+                % name, 's')
 
 
 class TestAdminVersionDetailCancel(FunctionalTwillTestCaseSetup):
@@ -1114,6 +1381,191 @@ UNION ALL SELECT 'attachment', 'file.ext
                 'file[.]ext [(]WikiStart[)]</a>')
 
 
+class TestMilestone(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Create a milestone."""
+        self._tester.go_to_roadmap()
+        tc.submit(formname='add')
+        tc.url(self._tester.url + '/milestone\?action=new')
+        name = random_unique_camel()
+        due = format_datetime(datetime.now(tz=utc) + timedelta(hours=1),
+                              tzinfo=localtz, locale=locale_en)
+        tc.formvalue('edit', 'name', name)
+        tc.formvalue('edit', 'due', True)
+        tc.formvalue('edit', 'duedate', due)
+        tc.submit('add')
+        tc.url(self._tester.url + '/milestone/' + name + '$')
+        tc.find(r'<h1>Milestone %s</h1>' % name)
+        tc.find(due)
+        self._tester.create_ticket(info={'milestone': name})
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="Due in .+ (.+)">%(name)s</a>'
+                % {'name': name})
+
+
+class TestMilestoneAddAttachment(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Add attachment to a milestone. Test that the attachment
+        button reads 'Attach file' when no files have been attached, and
+        'Attach another file' when there are existing attachments.
+        Feature added in http://trac.edgewall.org/ticket/10281."""
+        name = self._tester.create_milestone()
+        self._tester.go_to_milestone(name)
+        tc.find("Attach file")
+        filename = self._tester.attach_file_to_milestone(name)
+
+        self._tester.go_to_milestone(name)
+        tc.find("Attach another file")
+        tc.find('Attachments <span class="trac-count">\(1\)</span>')
+        tc.find(filename)
+        tc.find('Download all attachments as:\s+<a rel="nofollow" '
+                'href="/zip-attachment/milestone/%s/">.zip</a>' % name)
+
+
+class TestMilestoneClose(FunctionalTwillTestCaseSetup):
+    """Close a milestone and verify that tickets are retargeted
+    to the selected milestone"""
+    def runTest(self):
+        name = self._tester.create_milestone()
+        retarget_to = self._tester.create_milestone()
+        tid1 = self._tester.create_ticket(info={'milestone': name})
+        tid2 = self._tester.create_ticket(info={'milestone': name})
+        tc.formvalue('propertyform', 'action', 'resolve')
+        tc.formvalue('propertyform',
+                     'action_resolve_resolve_resolution', 'fixed')
+        tc.submit('submit')
+
+        self._tester.go_to_milestone(name)
+        completed = format_datetime(datetime.now(tz=utc) - timedelta(hours=1),
+                                    tzinfo=localtz, locale=locale_en)
+        tc.submit(formname='editmilestone')
+        tc.formvalue('edit', 'completed', True)
+        tc.formvalue('edit', 'completeddate', completed)
+        tc.formvalue('edit', 'target', retarget_to)
+        tc.submit('save')
+
+        tc.url(self._tester.url + '/milestone/%s$' % name)
+        tc.find('The open tickets associated with milestone "%s" '
+                'have been retargeted to milestone "%s".'
+                % (name, retarget_to))
+        tc.find("Completed")
+        self._tester.go_to_ticket(tid1)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': retarget_to})
+        tc.find('changed from <em>%s</em> to <em>%s</em>'
+                % (name, retarget_to))
+        tc.find("Ticket retargeted after milestone closed")
+        self._tester.go_to_ticket(tid2)
+        tc.find('<a class="closed milestone" href="/milestone/%(name)s" '
+                'title="Completed .+ ago (.+)">%(name)s</a>'
+                % {'name': name})
+        tc.notfind('changed from <em>%s</em> to <em>%s</em>'
+                   % (name, retarget_to))
+        tc.notfind("Ticket retargeted after milestone closed")
+
+
+class TestMilestoneDelete(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Delete a milestone and verify that tickets are retargeted
+        to the selected milestone."""
+        def delete_milestone(name, retarget_to=None, tid=None):
+            self._tester.go_to_milestone(name)
+            tc.submit(formname='deletemilestone')
+            if retarget_to is not None:
+                tc.formvalue('edit', 'target', retarget_to)
+            tc.submit('delete', formname='edit')
+
+            tc.url(self._tester.url + '/roadmap')
+            tc.find('The milestone "%s" has been deleted.' % name)
+            tc.notfind('Milestone:.*%s' % name)
+            if retarget_to is not None:
+                tc.find('Milestone:.*%s' % retarget_to)
+            retarget_notice = 'The tickets associated with milestone "%s" ' \
+                              'have been retargeted to milestone "%s".' \
+                              % (name, str(retarget_to))
+            if tid is not None:
+                tc.find(retarget_notice)
+                self._tester.go_to_ticket(tid)
+                tc.find('Changed[ \t\n]+<a .*>\d+ seconds? ago</a>'
+                        '[ \t\n]+by admin')
+                if retarget_to is not None:
+                    tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                            'title="No date set">%(name)s</a>'
+                            % {'name': retarget_to})
+                    tc.find('<strong class="trac-field-milestone">Milestone'
+                            '</strong>[ \t\n]+changed from <em>%s</em> to '
+                            '<em>%s</em>' % (name, retarget_to))
+                else:
+                    tc.find('<th id="h_milestone" class="missing">'
+                            '[ \t\n]*Milestone:[ \t\n]*</th>')
+                    tc.find('<strong class="trac-field-milestone">Milestone'
+                            '</strong>[ \t\n]*<em>%s</em>[ \t\n]*deleted'
+                            % name)
+                tc.find("Ticket retargeted after milestone deleted")
+            else:
+                tc.notfind(retarget_notice)
+
+        # No tickets associated with milestone to be retargeted
+        name = self._tester.create_milestone()
+        delete_milestone(name)
+
+        # Don't select a milestone to retarget to
+        name = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+        delete_milestone(name, tid=tid)
+
+        # Select a milestone to retarget to
+        name = self._tester.create_milestone()
+        retarget_to = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+        delete_milestone(name, retarget_to, tid)
+
+        # Just navigate to the page and select cancel
+        name = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+
+        self._tester.go_to_milestone(name)
+        tc.submit(formname='deletemilestone')
+        tc.submit('cancel', formname='edit')
+
+        tc.url(self._tester.url + '/milestone/%s' % name)
+        tc.notfind('The milestone "%s" has been deleted.' % name)
+        tc.notfind('The tickets associated with milestone "%s" '
+                   'have been retargeted to milestone' % name)
+        self._tester.go_to_ticket(tid)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': name})
+        tc.notfind('<strong class="trac-field-milestone">Milestone</strong>'
+                   '[ \t\n]*<em>%s</em>[ \t\n]*deleted' % name)
+        tc.notfind("Ticket retargeted after milestone deleted<br />")
+
+
+class TestMilestoneRename(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Rename a milestone and verify that the rename is shown in the
+        change history for the associated tickets."""
+        name = self._tester.create_milestone()
+        new_name = random_unique_camel()
+        tid = self._tester.create_ticket(info={'milestone': name})
+
+        self._tester.go_to_milestone(name)
+        tc.submit(formname='editmilestone')
+        tc.formvalue('edit', 'name', new_name)
+        tc.submit('save')
+
+        tc.url(self._tester.url + '/milestone/' + new_name)
+        tc.find("Your changes have been saved.")
+        tc.find(r"<h1>Milestone %s</h1>" % new_name)
+        self._tester.go_to_ticket(tid)
+        tc.find('Changed[ \t\n]+<a .*>\d+ seconds? ago</a>[ \t\n]+by admin')
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': new_name})
+        tc.find('<strong class="trac-field-milestone">Milestone</strong>'
+                '[ \t\n]+changed from <em>%s</em> to <em>%s</em>'
+                % (name, new_name))
+        tc.find("Milestone renamed")
+
+
 class RegressionTestRev5665(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create version without release time (r5665)"""
@@ -1128,7 +1580,6 @@ class RegressionTestRev5994(FunctionalTw
         env.config.set('ticket-custom', 'custfield.label', 'Custom Field')
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.go_to_query()
             tc.find('<label>( |\\n)*<input[^<]*value="custfield"'
                     '[^<]*/>( |\\n)*Custom Field( |\\n)*</label>', 's')
@@ -1136,23 +1587,21 @@ class RegressionTestRev5994(FunctionalTw
             pass
             #env.config.set('ticket', 'restrict_owner', 'no')
             #env.config.save()
-            #self._testenv.restart()
 
 
 class RegressionTestTicket4447(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/4447"""
-        ticketid = self._tester.create_ticket(summary="Hello World")
-
         env = self._testenv.get_trac_environment()
         env.config.set('ticket-custom', 'newfield', 'text')
         env.config.set('ticket-custom', 'newfield.label',
                        'Another Custom Field')
         env.config.save()
-        self._testenv.restart()
-        self._tester.go_to_ticket(ticketid)
+
+        ticketid = self._tester.create_ticket(summary="Hello World")
         self._tester.add_comment(ticketid)
-        tc.notfind('deleted')
+        tc.notfind('<strong class="trac-field-newfield">Another Custom Field'
+                   '</strong>[ \t\n]+<em></em>[ \t\n]+deleted')
         tc.notfind('set to')
 
 
@@ -1163,25 +1612,27 @@ class RegressionTestTicket4630a(Function
         env.config.set('ticket', 'restrict_owner', 'yes')
         env.config.save()
         try:
-            self._testenv.restart()
             # Make sure 'user' has logged in.
             self._tester.go_to_front()
             self._tester.logout()
             self._tester.login('user')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('joe')
+            self._tester.go_to_front()
             self._tester.logout()
             self._tester.login('admin')
-            ticket_id = self._tester.create_ticket()
-            self._tester.go_to_ticket(ticket_id)
+            self._tester.create_ticket()
             tc.formvalue('propertyform', 'action', 'reassign')
             tc.find('reassign_reassign_owner')
-            tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'user')
+            tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                         'user')
             tc.submit('submit')
         finally:
             # Undo the config change for now since this (failing)
             # regression test causes problems for later tests.
             env.config.set('ticket', 'restrict_owner', 'no')
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket4630b(FunctionalTestCaseSetup):
@@ -1195,7 +1646,7 @@ class RegressionTestTicket4630b(Function
         users = perm.get_users_with_permission('TRAC_ADMIN')
         self.assertEqual(users, ['admin'])
         users = perm.get_users_with_permission('TICKET_MODIFY')
-        self.assertEqual(users, ['admin', 'user'])
+        self.assertEqual(sorted(users), ['admin', 'joe', 'user'])
 
 
 class RegressionTestTicket5022(FunctionalTwillTestCaseSetup):
@@ -1217,14 +1668,13 @@ class RegressionTestTicket5394a(Function
         env = self._testenv.get_trac_environment()
         env.config.set('ticket', 'restrict_owner', 'yes')
         env.config.save()
-        self._testenv.restart()
 
         self._tester.go_to_front()
         self._tester.logout()
 
         test_users = ['alice', 'bob', 'jane', 'john', 'charlie', 'alan',
                       'zorro']
-        # Apprently it takes a sec for the new user to be recognized by the
+        # Apparently it takes a sec for the new user to be recognized by the
         # environment.  So we add all the users, then log in as the users
         # in a second loop.  This should be faster than adding a sleep(1)
         # between the .adduser and .login steps.
@@ -1232,17 +1682,17 @@ class RegressionTestTicket5394a(Function
             self._testenv.adduser(user)
         for user in test_users:
             self._tester.login(user)
+            self._tester.go_to_front()
             self._tester.logout()
 
         self._tester.login('admin')
 
-        ticketid = self._tester.create_ticket("regression test 5394a")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5394a")
 
         options = 'id="action_reassign_reassign_owner">' + \
             ''.join(['<option[^>]*>%s</option>' % user for user in
-                     sorted(test_users + ['admin', 'user'])])
-        tc.find(options, 's')
+                     sorted(test_users + ['admin', 'joe', 'user'])])
+        tc.find(to_utf8(options), 's')
         # We don't have a good way to fully delete a user from the Trac db.
         # Once we do, we may want to cleanup our list of users here.
 
@@ -1285,8 +1735,7 @@ class RegressionTestTicket5497a(Function
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 a
         Open ticket, component changed, owner not changed"""
-        ticketid = self._tester.create_ticket("regression test 5497a")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497a")
         tc.formvalue('propertyform', 'field-component', 'regression5497')
         tc.submit('submit')
         tc.find(regex_owned_by('user'))
@@ -1295,11 +1744,11 @@ class RegressionTestTicket5497b(Function
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 b
         Open ticket, component changed, owner changed"""
-        ticketid = self._tester.create_ticket("regression test 5497b")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497b")
         tc.formvalue('propertyform', 'field-component', 'regression5497')
         tc.formvalue('propertyform', 'action', 'reassign')
-        tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'admin')
+        tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                     'admin')
         tc.submit('submit')
         tc.notfind(regex_owned_by('user'))
         tc.find(regex_owned_by('admin'))
@@ -1308,18 +1757,17 @@ class RegressionTestTicket5497c(Function
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 c
         New ticket, component changed, owner not changed"""
-        ticketid = self._tester.create_ticket("regression test 5497c",
-            {'component':'regression5497'})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497c",
+                                   {'component':'regression5497'})
         tc.find(regex_owned_by('user'))
 
 class RegressionTestTicket5497d(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 d
         New ticket, component changed, owner changed"""
-        ticketid = self._tester.create_ticket("regression test 5497d",
-            {'component':'regression5497', 'owner':'admin'})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497d",
+                                   {'component':'regression5497',
+                                    'owner':'admin'})
         tc.find(regex_owned_by('admin'))
 
 
@@ -1328,15 +1776,16 @@ class RegressionTestTicket5602(Functiona
         """Test for regression of http://trac.edgewall.org/ticket/5602"""
         # Create a set of tickets, and assign them all to a milestone
         milestone = self._tester.create_milestone()
-        ids = [self._tester.create_ticket() for x in range(5)]
-        [self._tester.ticket_set_milestone(x, milestone) for x in ids]
+        ids = [self._tester.create_ticket(info={'milestone': milestone})
+               for x in range(5)]
         # Need a ticket in each state: new, assigned, accepted, closed,
         # reopened
         # leave ids[0] as new
         # make ids[1] be assigned
         self._tester.go_to_ticket(ids[1])
         tc.formvalue('propertyform', 'action', 'reassign')
-        tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'admin')
+        tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                     'admin')
         tc.submit('submit')
         # make ids[2] be accepted
         self._tester.go_to_ticket(ids[2])
@@ -1345,12 +1794,14 @@ class RegressionTestTicket5602(Functiona
         # make ids[3] be closed
         self._tester.go_to_ticket(ids[3])
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('submit')
         # make ids[4] be reopened
         self._tester.go_to_ticket(ids[4])
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('submit')
         # FIXME: we have to wait a second to avoid "IntegrityError: columns
         # ticket, time, field are not unique"
@@ -1380,9 +1831,10 @@ class RegressionTestTicket5602(Functiona
 class RegressionTestTicket5687(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5687"""
+        self._tester.go_to_front()
         self._tester.logout()
         self._tester.login('user')
-        ticketid = self._tester.create_ticket()
+        self._tester.create_ticket()
         self._tester.logout()
         self._tester.login('admin')
 
@@ -1406,10 +1858,10 @@ class RegressionTestTicket6048(Functiona
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/6048"""
         # Setup the DeleteTicket plugin
-        plugin = open(os.path.join(self._testenv.command_cwd, 'sample-plugins',
+        plugin = open(os.path.join(self._testenv.trac_src, 'sample-plugins',
                                    'workflow', 'DeleteTicket.py')).read()
-        open(os.path.join(self._testenv.tracdir, 'plugins', 'DeleteTicket.py'),
-             'w').write(plugin)
+        open(os.path.join(self._testenv.tracdir, 'plugins',
+                          'DeleteTicket.py'), 'w').write(plugin)
         env = self._testenv.get_trac_environment()
         prevconfig = env.config.get('ticket', 'workflow')
         env.config.set('ticket', 'workflow',
@@ -1418,8 +1870,7 @@ class RegressionTestTicket6048(Functiona
         env = self._testenv.get_trac_environment() # reload environment
 
         # Create a ticket and delete it
-        ticket_id = self._tester.create_ticket(
-            summary='RegressionTestTicket6048')
+        ticket_id = self._tester.create_ticket('RegressionTestTicket6048')
         # (Create a second ticket so that the ticket id does not get reused
         # and confuse the tester object.)
         self._tester.create_ticket(summary='RegressionTestTicket6048b')
@@ -1454,10 +1905,7 @@ class RegressionTestTicket6747(Functiona
         env.config.save()
 
         try:
-            self._testenv.restart()
-
-            ticket_id = self._tester.create_ticket("RegressionTestTicket6747")
-            self._tester.go_to_ticket(ticket_id)
+            self._tester.create_ticket("RegressionTestTicket6747")
             tc.find("a_specified_owner")
             tc.notfind("a_specified_owneras")
 
@@ -1468,7 +1916,6 @@ class RegressionTestTicket6747(Functiona
                            'set_resolution')
             env.config.remove('ticket-workflow', 'resolve.set_owner')
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket6879a(FunctionalTwillTestCaseSetup):
@@ -1479,10 +1926,10 @@ class RegressionTestTicket6879a(Function
         be those for the close status.
         """
         # create a ticket, then preview resolving the ticket twice
-        ticket_id = self._tester.create_ticket("RegressionTestTicket6879 a")
-        self._tester.go_to_ticket(ticket_id)
+        self._tester.create_ticket("RegressionTestTicket6879 a")
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('preview')
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('preview')
@@ -1496,10 +1943,10 @@ class RegressionTestTicket6879b(Function
         be those for the close status.
         """
         # create a ticket, then preview resolving the ticket twice
-        ticket_id = self._tester.create_ticket("RegressionTestTicket6879 b")
-        self._tester.go_to_ticket(ticket_id)
+        self._tester.create_ticket("RegressionTestTicket6879 b")
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('preview')
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('submit')
@@ -1510,7 +1957,7 @@ class RegressionTestTicket6912a(Function
         """Test for regression of http://trac.edgewall.org/ticket/6912 a"""
         try:
             self._tester.create_component(name='RegressionTestTicket6912a',
-                                          user='')
+                                          owner='')
         except twill.utils.ClientForm.ItemNotFoundError, e:
             raise twill.errors.TwillAssertionError(e)
 
@@ -1519,7 +1966,7 @@ class RegressionTestTicket6912b(Function
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/6912 b"""
         self._tester.create_component(name='RegressionTestTicket6912b',
-                                      user='admin')
+                                      owner='admin')
         tc.follow('RegressionTestTicket6912b')
         try:
             tc.formvalue('modcomp', 'owner', '')
@@ -1532,7 +1979,8 @@ class RegressionTestTicket6912b(Function
 
 class RegressionTestTicket7821group(FunctionalTwillTestCaseSetup):
     def runTest(self):
-        """Test for regression of http://trac.edgewall.org/ticket/7821 group"""
+        """Test for regression of http://trac.edgewall.org/ticket/7821 group.
+        """
         env = self._testenv.get_trac_environment()
         saved_default_query = env.config.get('query', 'default_query')
         default_query = 'status!=closed&order=status&group=status&max=42' \
@@ -1541,7 +1989,6 @@ class RegressionTestTicket7821group(Func
         env.config.set('query', 'default_query', default_query)
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.create_ticket('RegressionTestTicket7821 group')
             self._tester.go_to_query()
             # $USER
@@ -1573,7 +2020,6 @@ class RegressionTestTicket7821group(Func
         finally:
             env.config.set('query', 'default_query', saved_default_query)
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket7821var(FunctionalTwillTestCaseSetup):
@@ -1587,7 +2033,6 @@ class RegressionTestTicket7821var(Functi
         env.config.set('ticket', 'restrict_owner', 'no')
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.create_ticket('RegressionTestTicket7821 var')
             self._tester.go_to_query()
             # $USER in default_query
@@ -1606,7 +2051,6 @@ class RegressionTestTicket7821var(Functi
             env.config.set('query', 'default_query', saved_default_query)
             env.config.set('ticket', 'restrict_owner', saved_restrict_owner)
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket8247(FunctionalTwillTestCaseSetup):
@@ -1627,7 +2071,7 @@ class RegressionTestTicket8247(Functiona
         tc.find('<strong class="trac-field-milestone">Milestone</strong>'
                 '[ \n\t]*<em>%s</em> deleted' % name)
         tc.find('Changed <a.* ago</a> by admin')
-        tc.notfind('anonymous')
+        tc.notfind('</a> ago by anonymous')
 
 
 class RegressionTestTicket8861(FunctionalTwillTestCaseSetup):
@@ -1665,23 +2109,197 @@ class RegressionTestTicket9084(Functiona
 class RegressionTestTicket9981(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/9981"""
-        ticketid = self._tester.create_ticket()
-        self._tester.add_comment(ticketid)
+        tid1 = self._tester.create_ticket()
+        self._tester.add_comment(tid1)
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('submit')
-        comment = '[ticket:%s#comment:1]' % ticketid
-        self._tester.add_comment(ticketid, comment=comment)
-        self._tester.go_to_ticket(ticketid)
-        tc.find('class="closed ticket".*ticket/%s#comment:1"' % ticketid)
+        tid2 = self._tester.create_ticket()
+        comment = '[comment:1:ticket:%s]' % tid1
+        self._tester.add_comment(tid2, comment)
+        self._tester.go_to_ticket(tid2)
+        tc.find('<a class="closed ticket"[ \t\n]+'
+                'href="/ticket/%(num)s#comment:1"[ \t\n]+'
+                'title="Comment 1 for Ticket #%(num)s"' % {'num': tid1})
+
+
+class RegressionTestTicket11028(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11028"""
+        self._tester.go_to_roadmap()
+
+        try:
+            # Check that a milestone is found on the roadmap,
+            # even for anonymous
+            tc.find('<a href="/milestone/milestone1">[ \n\t]*'
+                    'Milestone: <em>milestone1</em>[ \n\t]*</a>')
+            self._tester.logout()
+            tc.find('<a href="/milestone/milestone1">[ \n\t]*'
+                    'Milestone: <em>milestone1</em>[ \n\t]*</a>')
+
+            # Check that no milestones are found on the roadmap when
+            # MILESTONE_VIEW is revoked
+            self._testenv.revoke_perm('anonymous', 'MILESTONE_VIEW')
+            tc.reload()
+            tc.notfind('Milestone: <em>milestone\d+</em>')
+
+            # Check that roadmap can't be viewed without ROADMAP_VIEW
+
+            self._testenv.revoke_perm('anonymous', 'ROADMAP_VIEW')
+            self._tester.go_to_url(self._tester.url + '/roadmap')
+            tc.find('<h1>Error: Forbidden</h1>')
+        finally:
+            # Restore state prior to test execution
+            self._tester.login('admin')
+            self._testenv.grant_perm('anonymous',
+                                     ('ROADMAP_VIEW', 'MILESTONE_VIEW'))
+
+
+class RegressionTestTicket11152(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11152"""
+        # Check that "View Tickets" mainnav entry links to the report page
+        self._tester.go_to_view_tickets()
+
+        # Check that "View Tickets" mainnav entry links to the query page
+        # when the user doesn't have REPORT_VIEW, and that the mainnav entry
+        # is not present when the user doesn't have TICKET_VIEW.
+        try:
+            self._tester.logout()
+            self._testenv.revoke_perm('anonymous', 'REPORT_VIEW')
+            self._tester.go_to_view_tickets('query')
+
+            self._testenv.revoke_perm('anonymous', 'TICKET_VIEW')
+            self._tester.go_to_front()
+            tc.notfind('\\bView Tickets\\b')
+        finally:
+            self._testenv.grant_perm('anonymous',
+                                     ('REPORT_VIEW', 'TICKET_VIEW'))
+            self._tester.login('admin')
+
+        # Disable the ReportModule component and check that "View Tickets"
+        # mainnav entry links to the `/query` page.
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', 'trac.ticket.report.ReportModule',
+                       'disabled')
+        env.config.save()
+
+        try:
+            self._tester.go_to_view_tickets('query')
+        finally:
+            env.config.remove('components', 'trac.ticket.report.ReportModule')
+            env.config.save()
+
+        # Disable the QueryModule component and check that "View Tickets"
+        # mainnav entry links to the `/report` page
+        env.config.set('components', 'trac.ticket.query.QueryModule',
+                       'disabled')
+        env.config.save()
+
+        try:
+            self._tester.go_to_view_tickets('report')
+            tc.notfind('<li class="last first">Available Reports</li>')
+        finally:
+            env.config.remove('components', 'trac.ticket.query.QueryModule')
+            env.config.save()
+
+
+class RegressionTestTicket11176(FunctionalTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11176
+        Fine-grained permission checks should be enforced on the Report list
+        page, the report pages and query pages."""
+        self._testenv.enable_authz_permpolicy("""
+            [report:1]
+            anonymous = REPORT_VIEW
+            [report:2]
+            anonymous = REPORT_VIEW
+            [report:*]
+            anonymous =
+        """)
+        self._tester.go_to_front()
+        self._tester.logout()
+        self._tester.go_to_view_tickets()
+        try:
+            # Check that permissions are enforced on the report list page
+            tc.find(r'<a title="View report" '
+                    r'href="/report/1">[ \n\t]*<em>\{1\}</em>')
+            tc.find(r'<a title="View report" '
+                    r'href="/report/2">[ \n\t]*<em>\{2\}</em>')
+            for report_num in range(3, 9):
+                tc.notfind(r'<a title="View report" '
+                           r'href="/report/%(num)s">[ \n\t]*'
+                           r'<em>\{%(num)s\}</em>' % {'num': report_num})
+            # Check that permissions are enforced on the report pages
+            tc.go(self._tester.url + '/report/1')
+            tc.find(r'<h1>\{1\} Active Tickets[ \n\t]*'
+                    r'(<span class="numrows">\(\d+ matches\)</span>)?'
+                    r'[ \n\t]*</h1>')
+            tc.go(self._tester.url + '/report/2')
+            tc.find(r'<h1>\{2\} Active Tickets by Version[ \n\t]*'
+                    r'(<span class="numrows">\(\d+ matches\)</span>)?'
+                    r'[ \n\t]*</h1>')
+            for report_num in range(3, 9):
+                tc.go(self._tester.url + '/report/%d' % report_num)
+                tc.find(r'<h1>Error: Forbidden</h1>')
+            # Check that permissions are enforced on the query pages
+            tc.go(self._tester.url + '/query?report=1')
+            tc.find(r'<h1>Active Tickets '
+                    r'<span class="numrows">\(\d+ matches\)</span></h1>')
+            tc.go(self._tester.url + '/query?report=2')
+            tc.find(r'<h1>Active Tickets by Version '
+                    r'<span class="numrows">\(\d+ matches\)</span></h1>')
+            for report_num in range(3, 9):
+                tc.go(self._tester.url + '/query?report=%d' % report_num)
+                tc.find(r'<h1>Error: Forbidden</h1>')
+        finally:
+            self._tester.login('admin')
+            self._testenv.disable_authz_permpolicy()
+
+
+class RegressionTestTicket11590(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11590"""
+        report_id = self._tester.create_report('#11590', 'SELECT 1',
+                                               '[./ this report]')
+        self._tester.go_to_view_tickets()
+        tc.notfind(internal_error)
+        tc.find('<a class="report" href="[^>"]*?/report/%s">this report</a>' %
+                report_id)
+
+
+class RegressionTestTicket11618(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11618
+        fix for malformed `readonly="True"` attribute in milestone admin page
+        """
+        name = "11618Milestone"
+        self._tester.create_milestone(name)
+        try:
+            self._testenv.grant_perm('user', 'TICKET_ADMIN')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('user')
+            tc.go(self._tester.url + "/admin/ticket/milestones/" + name)
+            tc.notfind('No administration panels available')
+            tc.find(' readonly="readonly"')
+            tc.notfind(' readonly="True"')
+        finally:
+            self._testenv.revoke_perm('user', 'TICKET_ADMIN')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('admin')
 
 
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(TestTickets())
+    suite.addTest(TestTicketMaxSummarySize())
+    suite.addTest(TestTicketAddAttachment())
     suite.addTest(TestTicketPreview())
     suite.addTest(TestTicketNoSummary())
+    suite.addTest(TestTicketManipulator())
     suite.addTest(TestTicketAltFormats())
     suite.addTest(TestTicketCSVFormat())
     suite.addTest(TestTicketTabFormat())
@@ -1691,6 +2309,7 @@ def functionalSuite(suite=None):
     suite.addTest(TestTicketHistory())
     suite.addTest(TestTicketHistoryDiff())
     suite.addTest(TestTicketQueryLinks())
+    suite.addTest(TestTicketQueryLinksQueryModuleDisabled())
     suite.addTest(TestTicketQueryOrClause())
     suite.addTest(TestTicketCustomFieldTextNoFormat())
     suite.addTest(TestTicketCustomFieldTextWikiFormat())
@@ -1699,19 +2318,23 @@ def functionalSuite(suite=None):
     suite.addTest(TestTicketCustomFieldTextReferenceFormat())
     suite.addTest(TestTicketCustomFieldTextListFormat())
     suite.addTest(RegressionTestTicket10828())
-    suite.addTest(TestTimelineTicketDetails())
+    suite.addTest(TestTicketTimeline())
     suite.addTest(TestAdminComponent())
+    suite.addTest(TestAdminComponentAuthorization())
     suite.addTest(TestAdminComponentDuplicates())
     suite.addTest(TestAdminComponentRemoval())
     suite.addTest(TestAdminComponentNonRemoval())
     suite.addTest(TestAdminComponentDefault())
     suite.addTest(TestAdminComponentDetail())
+    suite.addTest(TestAdminComponentNoneDefined())
     suite.addTest(TestAdminMilestone())
+    suite.addTest(TestAdminMilestoneAuthorization())
     suite.addTest(TestAdminMilestoneSpace())
     suite.addTest(TestAdminMilestoneDuplicates())
     suite.addTest(TestAdminMilestoneDetail())
     suite.addTest(TestAdminMilestoneDue())
     suite.addTest(TestAdminMilestoneDetailDue())
+    suite.addTest(TestAdminMilestoneDetailRename())
     suite.addTest(TestAdminMilestoneCompleted())
     suite.addTest(TestAdminMilestoneCompletedFuture())
     suite.addTest(TestAdminMilestoneRemove())
@@ -1719,6 +2342,7 @@ def functionalSuite(suite=None):
     suite.addTest(TestAdminMilestoneNonRemoval())
     suite.addTest(TestAdminMilestoneDefault())
     suite.addTest(TestAdminPriority())
+    suite.addTest(TestAdminPriorityAuthorization())
     suite.addTest(TestAdminPriorityModify())
     suite.addTest(TestAdminPriorityRemove())
     suite.addTest(TestAdminPriorityRemoveMulti())
@@ -1728,12 +2352,16 @@ def functionalSuite(suite=None):
     suite.addTest(TestAdminPriorityRenumber())
     suite.addTest(TestAdminPriorityRenumberDup())
     suite.addTest(TestAdminResolution())
+    suite.addTest(TestAdminResolutionAuthorization())
     suite.addTest(TestAdminResolutionDuplicates())
     suite.addTest(TestAdminSeverity())
+    suite.addTest(TestAdminSeverityAuthorization())
     suite.addTest(TestAdminSeverityDuplicates())
     suite.addTest(TestAdminType())
+    suite.addTest(TestAdminTypeAuthorization())
     suite.addTest(TestAdminTypeDuplicates())
     suite.addTest(TestAdminVersion())
+    suite.addTest(TestAdminVersionAuthorization())
     suite.addTest(TestAdminVersionDuplicates())
     suite.addTest(TestAdminVersionDetail())
     suite.addTest(TestAdminVersionDetailTime())
@@ -1744,6 +2372,11 @@ def functionalSuite(suite=None):
     suite.addTest(TestAdminVersionDefault())
     suite.addTest(TestNewReport())
     suite.addTest(TestReportRealmDecoration())
+    suite.addTest(TestMilestone())
+    suite.addTest(TestMilestoneAddAttachment())
+    suite.addTest(TestMilestoneClose())
+    suite.addTest(TestMilestoneDelete())
+    suite.addTest(TestMilestoneRename())
     suite.addTest(RegressionTestRev5665())
     suite.addTest(RegressionTestRev5994())
 
@@ -1773,6 +2406,14 @@ def functionalSuite(suite=None):
     suite.addTest(RegressionTestTicket8861())
     suite.addTest(RegressionTestTicket9084())
     suite.addTest(RegressionTestTicket9981())
+    suite.addTest(RegressionTestTicket11028())
+    suite.addTest(RegressionTestTicket11152())
+    suite.addTest(RegressionTestTicket11590())
+    suite.addTest(RegressionTestTicket11618())
+    if ConfigObj:
+        suite.addTest(RegressionTestTicket11176())
+    else:
+        print "SKIP: RegressionTestTicket11176 (ConfigObj not installed)"
 
     return suite