You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2022/09/15 18:36:51 UTC

[allura] 05/05: [#8455] allura pytest - fix misc test failures that popped up during pytest conversion

This is an automated email from the ASF dual-hosted git repository.

dill0wn pushed a commit to branch dw/8455
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 32da7de2076a72b2b35debe0147ffadfef5c5f56
Author: Dillon Walls <di...@slashdotmedia.com>
AuthorDate: Mon Sep 12 20:00:41 2022 +0000

    [#8455] allura pytest - fix misc test failures that popped up during pytest conversion
---
 Allura/allura/lib/macro.py                         |  15 +--
 Allura/allura/model/project.py                     |  20 +++-
 Allura/allura/tests/exclude_from_rewrite_hook.py   |  31 ++++++
 .../allura/tests/functional/test_neighborhood.py   |   8 +-
 .../allura/tests/functional/test_user_profile.py   |  70 +++++++------
 Allura/allura/tests/model/test_auth.py             |   2 +-
 Allura/allura/tests/test_globals.py                | 115 ++++++++++++---------
 Allura/allura/tests/test_plugin.py                 |  34 +++---
 Allura/allura/tests/test_tasks.py                  |  38 +++----
 Allura/allura/tests/test_webhooks.py               | 101 +++++++++---------
 10 files changed, 249 insertions(+), 185 deletions(-)

diff --git a/Allura/allura/lib/macro.py b/Allura/allura/lib/macro.py
index b1af50297..0e293037e 100644
--- a/Allura/allura/lib/macro.py
+++ b/Allura/allura/lib/macro.py
@@ -177,14 +177,15 @@ def project_blog_posts(max_number=5, sort='timestamp', summary=False, mount_poin
         'state': 'published',
     })
     posts = posts.sort(sort, pymongo.DESCENDING).limit(int(max_number)).all()
-    output = ((dict(
-        href=post.url(),
-        title=post.title,
-        author=post.author().display_name,
-        ago=h.ago(post.timestamp),
-        description=summary and '&nbsp;' or g.markdown.cached_convert(post, 'text')))
+    output = [
+        dict(href=post.url(),
+             title=post.title,
+             author=post.author().display_name,
+             ago=h.ago(post.timestamp),
+             description=summary and '&nbsp;' or g.markdown.cached_convert(post, 'text'))
         for post in posts if security.has_access(post, 'read', project=post.app.project)() and
-        security.has_access(post.app.project, 'read', project=post.app.project)())
+            security.has_access(post.app.project, 'read', project=post.app.project)()
+    ]
     posts = BlogPosts(posts=output)
     g.resource_manager.register(posts)
     response = posts.display(posts=output)
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index c0a010f5e..66b265e80 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -1102,12 +1102,30 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
             ThreadLocalORMSession.flush_all()
 
     def add_user(self, user, role_names):
-        'Convenience method to add member with the given role(s).'
+        '''Convenience method to add member with the given role(s).'''
         pr = ProjectRole.by_user(user, project=self, upsert=True)
         for role_name in role_names:
             r = ProjectRole.by_name(role_name, self)
             pr.roles.append(r._id)
 
+    def remove_user(self, user, role_names=None):
+        '''Convenience method to add member with the given role(s).'''
+        pr = ProjectRole.by_user(user, project=self)
+        if not pr:
+            return
+
+        if not role_names or not isinstance(role_names, Iterable):
+            ProjectRole.query.remove({'_id': pr._id})
+            return
+
+        for role_name in role_names:
+            r = ProjectRole.by_name(role_name, self)
+            if r._id in pr.roles:
+                pr.roles.remove(r._id)
+
+        if not pr.roles:
+            pr.remove()
+
     @property
     def twitter_handle(self):
         return self.social_account('Twitter').accounturl
diff --git a/Allura/allura/tests/exclude_from_rewrite_hook.py b/Allura/allura/tests/exclude_from_rewrite_hook.py
new file mode 100644
index 000000000..509841776
--- /dev/null
+++ b/Allura/allura/tests/exclude_from_rewrite_hook.py
@@ -0,0 +1,31 @@
+import sys
+
+from allura.app import Application
+from allura.lib.decorators import task
+from allura.lib.exceptions import CompoundError
+
+
+class ThemeProviderTestApp(Application):
+    """
+    If this test class is added directly to a test module, pkg_resources internals
+    will throw this error:
+        NotImplementedError: Can't perform this operation for unregistered loader type
+    This is because pytest adds a hook to override the default assert behavior and this
+    conflicts/messes-with pkg_resources. Theoretically on python > py37, importlib.resources
+    can do the same things as pkg_resources and faster, but those solutions don't currently
+    work on py37.
+    """
+    icons = {
+        24: 'images/testapp_24.png',
+    }
+
+
+@task
+def raise_compound_exception():
+    errs = []
+    for x in range(10):
+        try:
+            assert False, 'assert %d' % x
+        except Exception:
+            errs.append(sys.exc_info())
+    raise CompoundError(*errs)
\ No newline at end of file
diff --git a/Allura/allura/tests/functional/test_neighborhood.py b/Allura/allura/tests/functional/test_neighborhood.py
index a1d1e7d57..ac7ee3c9d 100644
--- a/Allura/allura/tests/functional/test_neighborhood.py
+++ b/Allura/allura/tests/functional/test_neighborhood.py
@@ -43,12 +43,12 @@ from allura.tests.pytest_helpers import with_nose_compatibility
 @with_nose_compatibility
 class TestNeighborhood(TestController):
 
-    def setUp(self):
-        super().setUp()
+    def setup_method(self, method):
+        super().setup_method(method)
         self._cleanup_audit_log()
 
-    def tearDown(self):
-        super().tearDown()
+    def teardown_method(self, method):
+        super().teardown_method(method)
         self._cleanup_audit_log()
 
     def _cleanup_audit_log(self):
diff --git a/Allura/allura/tests/functional/test_user_profile.py b/Allura/allura/tests/functional/test_user_profile.py
index 4a54ffa69..e4df160e1 100644
--- a/Allura/allura/tests/functional/test_user_profile.py
+++ b/Allura/allura/tests/functional/test_user_profile.py
@@ -26,6 +26,46 @@ from allura.tests import TestController
 from allura.tests.pytest_helpers import with_nose_compatibility
 
 
+class TestUserProfileSections(TestController):
+
+    def teardown_method(self, method):
+        super().teardown_method(method)
+        project = Project.query.get(shortname='u/test-user')
+        app = project.app_instance('profile')
+        if hasattr(type(app), '_sections'):
+            delattr(type(app), '_sections')
+
+    @td.with_user_project('test-user')
+    def test_profile_sections(self):
+        project = Project.query.get(shortname='u/test-user')
+        app = project.app_instance('profile')
+
+        def ep(n):
+            m = mock.Mock()
+            m.name = n
+            m.load()().display.return_value = 'Section %s' % n
+            return m
+        eps = list(map(ep, ['a', 'b', 'c', 'd']))
+        order = {'user_profile_sections.order': 'b, d,c , f '}
+        if hasattr(type(app), '_sections'):
+            delattr(type(app), '_sections')
+        with mock.patch('allura.lib.helpers.iter_entry_points') as iep:
+            with mock.patch.dict(tg.config, order):
+                iep.return_value = eps
+                sections = app.profile_sections
+                assert sections == [
+                    eps[1].load(),
+                    eps[3].load(),
+                    eps[2].load(),
+                    eps[0].load()]
+        r = self.app.get('/u/test-user/profile')
+        assert 'Section a' in r.text
+        assert 'Section b' in r.text
+        assert 'Section c' in r.text
+        assert 'Section d' in r.text
+        assert 'Section f' not in r.text
+
+
 @with_nose_compatibility
 class TestUserProfile(TestController):
 
@@ -230,36 +270,6 @@ class TestUserProfile(TestController):
         r = self.app.get('/u/test-user/profile/send_message', status=200)
         assert 'you currently have user messages disabled' in r
 
-    @td.with_user_project('test-user')
-    def test_profile_sections(self):
-        project = Project.query.get(shortname='u/test-user')
-        app = project.app_instance('profile')
-
-        def ep(n):
-            m = mock.Mock()
-            m.name = n
-            m.load()().display.return_value = 'Section %s' % n
-            return m
-        eps = list(map(ep, ['a', 'b', 'c', 'd']))
-        order = {'user_profile_sections.order': 'b, d,c , f '}
-        if hasattr(type(app), '_sections'):
-            delattr(type(app), '_sections')
-        with mock.patch('allura.lib.helpers.iter_entry_points') as iep:
-            with mock.patch.dict(tg.config, order):
-                iep.return_value = eps
-                sections = app.profile_sections
-                assert sections == [
-                    eps[1].load(),
-                    eps[3].load(),
-                    eps[2].load(),
-                    eps[0].load()]
-        r = self.app.get('/u/test-user/profile')
-        assert 'Section a' in r.text
-        assert 'Section b' in r.text
-        assert 'Section c' in r.text
-        assert 'Section d' in r.text
-        assert 'Section f' not in r.text
-
     def test_no_index_tag_in_empty_profile(self):
         r = self.app.get('/u/test-user/profile/')
         assert 'content="noindex, follow"' in r.text
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index 2d6aa75f7..7f48c49bc 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -352,8 +352,8 @@ def test_check_sent_user_message_times():
     assert not user1.can_send_user_message()
 
 
-@td.with_user_project('test-admin')
 @with_setup(setup_method)
+@td.with_user_project('test-admin')
 def test_user_track_active():
     # without this session flushing inside track_active raises Exception
     setup_functional_test()
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index 4d4b3b592..389c7a228 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -18,6 +18,7 @@
 
 import re
 import os
+from textwrap import dedent
 import allura
 import unittest
 import hashlib
@@ -48,11 +49,6 @@ from forgewiki import model as WM
 from forgeblog import model as BM
 
 
-def setup_module(module):
-    setup_basic_test()
-    setup_unit_test()
-
-
 def squish_spaces(text):
     # \s is whitespace
     # \xa0 is &nbsp; in unicode form
@@ -84,8 +80,28 @@ def get_projects_property_in_the_same_order(names, prop):
 @with_nose_compatibility
 class Test():
 
+    @classmethod
+    def setup_class(cls):
+        setup_basic_test()
+        setup_global_objects()
+
     def setup_method(self, method):
         setup_global_objects()
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        self.acl_bak = p_test.acl.copy()
+
+    def teardown_method(self, method):
+        user = M.User.by_username('test-admin')
+        user.display_name = 'Test Admin'
+
+        p_nbhd = M.Neighborhood.query.get(name='Projects')
+        p_test = M.Project.query.get(shortname='test', neighborhood_id=p_nbhd._id)
+        p_test.remove_user(M.User.by_username('test-user'))
+        p_test.remove_user(M.User.by_username('test-user-0'))
+        p_test.acl = self.acl_bak
+
+        ThreadLocalORMSession.flush_all()
 
     @td.with_wiki
     def test_app_globals(self):
@@ -358,17 +374,19 @@ class Test():
 
     def test_markdown_toc(self):
         with h.push_context('test', neighborhood='Projects'):
-            r = g.markdown_wiki.convert("""[TOC]
+            r = g.markdown_wiki.convert(dedent("""\
+                [TOC]
 
-    # Header 1
+                # Header 1
 
-    ## Header 2""")
-        assert '''<ul>
-    <li><a href="#header-1">Header 1</a><ul>
-    <li><a href="#header-2">Header 2</a></li>
-    </ul>
-    </li>
-    </ul>''' in r, r
+                ## Header 2"""))
+        assert dedent('''\
+            <ul>
+            <li><a href="#header-1">Header 1</a><ul>
+            <li><a href="#header-2">Header 2</a></li>
+            </ul>
+            </li>
+            </ul>''') in r
 
     @td.with_wiki
     def test_wiki_artifact_links(self):
@@ -442,38 +460,35 @@ class Test():
             '<p>Line</p></div>')
 
         # should not raise an exception:
-        assert (
-            g.markdown.convert("<class 'foo'>") ==
-            '''<div class="markdown_content"><p>&lt;class 'foo'=""&gt;&lt;/class&gt;</p></div>''')
-
-        assert (
-            g.markdown.convert('''# Header
-
-    Some text in a regular paragraph
-
-        :::python
-        for i in range(10):
-            print i
-    ''') ==
-            # no <br
-            '<div class="markdown_content"><h1 id="header">Header</h1>\n'
-            '<p>Some text in a regular paragraph</p>\n'
-            '<div class="codehilite"><pre><span></span><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>\n'
-            '    <span class="nb">print</span> <span class="n">i</span>\n'
-            '</code></pre></div>\n'
-            '</div>')
+        assert g.markdown.convert("<class 'foo'>") == \
+            '''<div class="markdown_content"><p>&lt;class 'foo'=""&gt;&lt;/class&gt;</p></div>'''
+
+        assert g.markdown.convert(dedent('''\
+            # Header
+
+            Some text in a regular paragraph
+
+                :::python
+                for i in range(10):
+                    print i
+            ''')) == dedent('''\
+                <div class="markdown_content"><h1 id="header">Header</h1>
+                <p>Some text in a regular paragraph</p>
+                <div class="codehilite"><pre><span></span><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>
+                    <span class="nb">print</span> <span class="n">i</span>
+                </code></pre></div>
+                </div>''')
         assert (
             g.forge_markdown(email=True).convert('[Home]') ==
             # uses localhost:
             '<div class="markdown_content"><p><a class="alink" href="http://localhost/p/test/wiki/Home/">[Home]</a></p></div>')
-        assert (
-            g.markdown.convert('''
-    ~~~~
-    def foo(): pass
-    ~~~~''') ==
-            '<div class="markdown_content"><div class="codehilite"><pre><span></span><code>def foo(): pass\n'
-            '</code></pre></div>\n'
-            '</div>')
+        assert g.markdown.convert(dedent('''\
+            ~~~~
+            def foo(): pass
+            ~~~~''')) == dedent('''\
+                <div class="markdown_content"><div class="codehilite"><pre><span></span><code>def foo(): pass
+                </code></pre></div>
+                </div>''')
 
     def test_markdown_list_without_break(self):
         # this is not a valid way to make a list in original Markdown or python-markdown
@@ -482,37 +497,37 @@ class Test():
         # TODO: try https://github.com/adamb70/mdx-breakless-lists
         #       or https://gitlab.com/ayblaq/prependnewline
         assert (
-            g.markdown.convert('''\
+            g.markdown.convert(dedent('''\
     Regular text
     * first item
-    * second item''') ==
+    * second item''')) ==
             '<div class="markdown_content"><p>Regular text\n'  # no <br>
             '* first item\n'  # no <br>
             '* second item</p></div>')
 
         assert (
-            g.markdown.convert('''\
+            g.markdown.convert(dedent('''\
     Regular text
     - first item
-    - second item''') ==
+    - second item''')) ==
             '<div class="markdown_content"><p>Regular text<br/>\n'
             '- first item<br/>\n'
             '- second item</p></div>')
 
         assert (
-            g.markdown.convert('''\
+            g.markdown.convert(dedent('''\
     Regular text
     + first item
-    + second item''') ==
+    + second item''')) ==
             '<div class="markdown_content"><p>Regular text<br/>\n'
             '+ first item<br/>\n'
             '+ second item</p></div>')
 
         assert (
-            g.markdown.convert('''\
+            g.markdown.convert(dedent('''\
     Regular text
     1. first item
-    2. second item''') ==
+    2. second item''')) ==
             '<div class="markdown_content"><p>Regular text<br/>\n'
             '1. first item<br/>\n'
             '2. second item</p></div>')
diff --git a/Allura/allura/tests/test_plugin.py b/Allura/allura/tests/test_plugin.py
index 22a7ed8bf..f11eae8d7 100644
--- a/Allura/allura/tests/test_plugin.py
+++ b/Allura/allura/tests/test_plugin.py
@@ -35,7 +35,6 @@ from alluratest.tools import (
 from mock import Mock, MagicMock, patch
 
 from allura import model as M
-from allura.app import Application
 from allura.lib import plugin
 from allura.lib import phone
 from allura.lib import helpers as h
@@ -44,15 +43,19 @@ from allura.lib.plugin import ProjectRegistrationProvider
 from allura.lib.plugin import ThemeProvider
 from allura.lib.exceptions import ProjectConflict, ProjectShortnameInvalid
 from allura.tests.decorators import audits
+from allura.tests.exclude_from_rewrite_hook import ThemeProviderTestApp
 from alluratest.controller import setup_basic_test, setup_global_objects
 from allura.tests.pytest_helpers import with_nose_compatibility
 
 
+def setup_module(module):
+    setup_basic_test()
+
+
 @with_nose_compatibility
 class TestProjectRegistrationProvider:
 
     def setup_method(self, method):
-        setup_basic_test()
         self.provider = ProjectRegistrationProvider()
 
     @patch('allura.lib.security.has_access')
@@ -170,7 +173,6 @@ class TestProjectRegistrationProviderPhoneVerification:
         self.user = UserMock()
         self.nbhd = MagicMock()
 
-
     def test_phone_verified_disabled(self):
         with h.push_config(tg.config, **{'project.verify_phone': 'false'}):
             assert self.p.phone_verified(self.user, self.nbhd)
@@ -280,36 +282,30 @@ class TestProjectRegistrationProviderPhoneVerification:
                     assert result == g.phone_service.verify.return_value
             assert 5 == g.phone_service.verify.call_count
 
+
 @with_nose_compatibility
 class TestThemeProvider:
 
     @patch('allura.app.g')
     @patch('allura.lib.plugin.g')
     def test_app_icon_str(self, plugin_g, app_g):
-        class TestApp(Application):
-            icons = {
-                24: 'images/testapp_24.png',
-            }
-        plugin_g.entry_points = {'tool': {'testapp': TestApp}}
-        assert (ThemeProvider().app_icon_url('testapp', 24) ==
-                      app_g.theme_href.return_value)
+        plugin_g.entry_points = {'tool': {'testapp': ThemeProviderTestApp}}
+        app_icon = ThemeProvider().app_icon_url('testapp', 24)
+        other = app_g.theme_href.return_value
+        assert app_icon == other
+            
         app_g.theme_href.assert_called_with('images/testapp_24.png')
 
     @patch('allura.lib.plugin.g')
     def test_app_icon_str_invalid(self, g):
         g.entry_points = {'tool': {'testapp': Mock()}}
-        assert (ThemeProvider().app_icon_url('invalid', 24) ==
-                      None)
+        assert ThemeProvider().app_icon_url('invalid', 24) == None
 
     @patch('allura.app.g')
     def test_app_icon_app(self, g):
-        class TestApp(Application):
-            icons = {
-                24: 'images/testapp_24.png',
-            }
-        app = TestApp(None, None)
-        assert (ThemeProvider().app_icon_url(app, 24) ==
-                      g.theme_href.return_value)
+        app = ThemeProviderTestApp(None, None)
+        assert ThemeProvider().app_icon_url(app, 24) == \
+            g.theme_href.return_value
         g.theme_href.assert_called_with('images/testapp_24.png')
 
 
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 735b3294b..429effd80 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -18,6 +18,7 @@
 import operator
 import shutil
 import sys
+from textwrap import dedent
 import unittest
 
 import six
@@ -52,6 +53,7 @@ from allura.tasks import export_tasks
 from allura.tasks import admin_tasks
 from allura.tests import decorators as td
 from allura.tests.pytest_helpers import with_nose_compatibility
+from allura.tests.exclude_from_rewrite_hook import raise_compound_exception
 from allura.lib.decorators import event_handler, task
 
 
@@ -148,7 +150,7 @@ class TestEventTasks(unittest.TestCase):
         assert M.MonQTask.query.get(task_name='allura.tasks.event_tasks.event', args=['my_event4'])
 
     def test_compound_error(self):
-        t = raise_exc.post()
+        t = raise_compound_exception.post()
         with LogCapture(level=logging.ERROR) as l, \
                 mock.patch.dict(tg.config, {'monq.raise_errors': False}):  # match normal non-test behavior
             t()
@@ -158,7 +160,7 @@ class TestEventTasks(unittest.TestCase):
         assert "AssertionError('assert 0'" in msg
         assert "AssertionError('assert 5'" in msg
         assert ' on job <MonQTask ' in msg
-        assert ' (error) P:10 allura.tests.test_tasks.raise_exc ' in msg
+        assert ' (error) P:10 allura.tests.exclude_from_rewrite_hook.raise_compound_exception ' in msg
         for x in range(10):
             assert ('assert %d' % x) in t.result
 
@@ -510,16 +512,17 @@ class TestMailTasks(unittest.TestCase):
 
     @td.with_tool('test', 'Tickets', 'bugs')
     def test_receive_autoresponse(self):
-        message = '''Date: Wed, 30 Oct 2013 01:38:40 -0700
-From: <te...@domain.net>
-To: <1...@bugs.test.p.in.localhost>
-Message-ID: <super-unique-id>
-Subject: Not here Re: Message notification
-Precedence: bulk
-X-Autoreply: yes
-Auto-Submitted: auto-replied
-
-I'm not here'''
+        message = dedent('''\
+            Date: Wed, 30 Oct 2013 01:38:40 -0700
+            From: <te...@domain.net>
+            To: <1...@bugs.test.p.in.localhost>
+            Message-ID: <super-unique-id>
+            Subject: Not here Re: Message notification
+            Precedence: bulk
+            X-Autoreply: yes
+            Auto-Submitted: auto-replied
+
+            I'm not here''')
         import forgetracker
         c.user = M.User.by_username('test-admin')
         with mock.patch.object(forgetracker.tracker_main.ForgeTrackerApp, 'handle_message') as hm:
@@ -597,17 +600,6 @@ def _my_event(event_type, testcase, *args, **kwargs):
     testcase.called_with.append((args, kwargs))
 
 
-@task
-def raise_exc():
-    errs = []
-    for x in range(10):
-        try:
-            assert False, 'assert %d' % x
-        except Exception:
-            errs.append(sys.exc_info())
-    raise CompoundError(*errs)
-
-
 class _TestArtifact(M.Artifact):
     _shorthand_id = FieldProperty(str)
     text = FieldProperty(str)
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
index 5b9d5b1f2..8237847d6 100644
--- a/Allura/allura/tests/test_webhooks.py
+++ b/Allura/allura/tests/test_webhooks.py
@@ -132,6 +132,7 @@ class TestValidators(TestWebhookBase):
 
 @with_nose_compatibility
 class TestWebhookController(TestController):
+
     def setup_method(self, method):
         super().setup_method(method)
         self.patches = self.monkey_patch()
@@ -182,6 +183,56 @@ class TestWebhookController(TestController):
         else:
             assert False, 'Validation error not found'
 
+    def test_AAAA_WORKAROUND__edit(self):
+        """
+        This must run first in this test class for unknown reasons ever since
+            https://github.com/TurboGears/tg2/commit/02fb49b14e70fdd8ac16973488fb3637e5e59114
+
+        If any test runs the self.app.post from create_webhook before this one, then this test will fail on:
+            with td.audits(msg):
+                r = form.submit()
+        because WebhookValidator's `value` will be "create" instead of an objectid str
+
+        Maybe something to do with WebhookControllerMeta setup of `validate` decorators?
+        """
+        data1 = {'url': 'http://httpbin.org/post',
+                 'secret': 'secret'}
+        data2 = {'url': 'http://example.com/hook',
+                 'secret': 'secret2'}
+        self.create_webhook(data1).follow()
+        self.create_webhook(data2).follow()
+        assert M.Webhook.query.find().count() == 2
+        wh1 = M.Webhook.query.get(hook_url=data1['url'])
+        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
+        form = r.forms[0]
+        assert form['url'].value == data1['url']
+        assert form['secret'].value == data1['secret']
+        assert form['webhook'].value == str(wh1._id)
+        form['url'] = 'http://host.org/hook'
+        form['secret'] = 'new secret'
+        msg = 'edit webhook repo-push\n{} => {}\n{}'.format(
+            data1['url'], form['url'].value, 'secret changed')
+        with td.audits(msg):
+            r = form.submit()
+        wf = json.loads(self.webflash(r))
+        assert wf['status'] == 'ok'
+        assert wf['message'] == 'Edited successfully'
+        assert M.Webhook.query.find().count() == 2
+        wh1 = M.Webhook.query.get(_id=wh1._id)
+        assert wh1.hook_url == 'http://host.org/hook'
+        assert wh1.app_config_id == self.git.config._id
+        assert wh1.secret == 'new secret'
+        assert wh1.type == 'repo-push'
+
+        # Duplicates
+        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
+        form = r.forms[0]
+        form['url'] = data2['url']
+        r = form.submit()
+        self.find_error(r, '_the_form',
+                        '"repo-push" webhook already exists for Git http://example.com/hook',
+                        form_type='edit')
+
     def test_access(self):
         self.app.get(self.url + '/repo-push/')
         self.app.get(self.url + '/repo-push/',
@@ -256,56 +307,6 @@ class TestWebhookController(TestController):
         self.find_error(r, 'url',
                         'You must provide a full domain name (like qwer.com)')
 
-    def test_AAAA_WORKAROUND__edit(self):
-        """
-        This must run first in this test class for unknown reasons ever since
-            https://github.com/TurboGears/tg2/commit/02fb49b14e70fdd8ac16973488fb3637e5e59114
-
-        If any test runs the self.app.post from create_webhook before this one, then this test will fail on:
-            with td.audits(msg):
-                r = form.submit()
-        because WebhookValidator's `value` will be "create" instead of an objectid str
-
-        Maybe something to do with WebhookControllerMeta setup of `validate` decorators?
-        """
-        data1 = {'url': 'http://httpbin.org/post',
-                 'secret': 'secret'}
-        data2 = {'url': 'http://example.com/hook',
-                 'secret': 'secret2'}
-        self.create_webhook(data1).follow()
-        self.create_webhook(data2).follow()
-        assert M.Webhook.query.find().count() == 2
-        wh1 = M.Webhook.query.get(hook_url=data1['url'])
-        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
-        form = r.forms[0]
-        assert form['url'].value == data1['url']
-        assert form['secret'].value == data1['secret']
-        assert form['webhook'].value == str(wh1._id)
-        form['url'] = 'http://host.org/hook'
-        form['secret'] = 'new secret'
-        msg = 'edit webhook repo-push\n{} => {}\n{}'.format(
-            data1['url'], form['url'].value, 'secret changed')
-        with td.audits(msg):
-            r = form.submit()
-        wf = json.loads(self.webflash(r))
-        assert wf['status'] == 'ok'
-        assert wf['message'] == 'Edited successfully'
-        assert M.Webhook.query.find().count() == 2
-        wh1 = M.Webhook.query.get(_id=wh1._id)
-        assert wh1.hook_url == 'http://host.org/hook'
-        assert wh1.app_config_id == self.git.config._id
-        assert wh1.secret == 'new secret'
-        assert wh1.type == 'repo-push'
-
-        # Duplicates
-        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
-        form = r.forms[0]
-        form['url'] = data2['url']
-        r = form.submit()
-        self.find_error(r, '_the_form',
-                        '"repo-push" webhook already exists for Git http://example.com/hook',
-                        form_type='edit')
-
     def test_edit_validation(self):
         invalid = M.Webhook(
             type='invalid type',