You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by je...@apache.org on 2015/02/16 12:44:52 UTC

[21/37] allura git commit: [#4542] ticket:714 Add tests for new webhook functionality

[#4542] ticket:714 Add tests for new webhook functionality


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/e7ace573
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/e7ace573
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/e7ace573

Branch: refs/heads/ib/4542
Commit: e7ace573ae8995955fb37de7ec62cd87e9d7bc2e
Parents: ba555ec
Author: Igor Bondarenko <je...@gmail.com>
Authored: Fri Jan 30 15:01:49 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Mon Feb 16 10:17:38 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/test_utils.py               |   9 +
 Allura/allura/tests/test_webhooks.py            | 469 +++++++++++++++++++
 Allura/allura/webhooks.py                       |   2 +-
 .../forgegit/tests/model/test_repository.py     |  25 +
 .../forgesvn/tests/model/test_repository.py     |  25 +
 5 files changed, 529 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/tests/test_utils.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index e5f9c43..06579c5 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -17,8 +17,10 @@
 #       specific language governing permissions and limitations
 #       under the License.
 
+import json
 import time
 import unittest
+import datetime as dt
 from os import path
 
 from webob import Request
@@ -295,3 +297,10 @@ def test_empty_cursor():
     assert_raises(ValueError, cursor.one)
     assert_raises(StopIteration, cursor.next)
     assert_raises(StopIteration, cursor._next_impl)
+
+
+def test_DateJSONEncoder():
+    data = {'message': u'Hi!',
+            'date': dt.datetime(2015, 01, 30, 13, 13, 13)}
+    result = json.dumps(data, cls=utils.DateJSONEncoder)
+    assert_equal(result, '{"date": "2015-01-30T13:13:13Z", "message": "Hi!"}')

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/tests/test_webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_webhooks.py b/Allura/allura/tests/test_webhooks.py
new file mode 100644
index 0000000..fa0305f
--- /dev/null
+++ b/Allura/allura/tests/test_webhooks.py
@@ -0,0 +1,469 @@
+import json
+import hmac
+import hashlib
+
+from mock import Mock, patch
+from nose.tools import (
+    assert_raises,
+    assert_equal,
+    assert_not_in,
+    assert_in,
+)
+from formencode import Invalid
+from ming.odm import session
+from pylons import tmpl_context as c
+
+from allura import model as M
+from allura.lib import helpers as h
+from allura.lib.utils import DateJSONEncoder
+from allura.webhooks import (
+    MingOneOf,
+    WebhookValidator,
+    WebhookController,
+    send_webhook,
+    RepoPushWebhookSender,
+)
+from allura.tests import decorators as td
+from alluratest.controller import setup_basic_test, TestController
+
+
+# important to be distinct from 'test' and 'test2' which ForgeGit and
+# ForgeImporter use, so that the tests can run in parallel and not clobber each
+# other
+test_project_with_repo = 'adobe-1'
+with_git = td.with_tool(test_project_with_repo, 'git', 'src', 'Git')
+with_git2 = td.with_tool(test_project_with_repo, 'git', 'src2', 'Git2')
+
+
+class TestWebhookBase(object):
+
+    def setUp(self):
+        setup_basic_test()
+        self.setup_with_tools()
+        self.project = M.Project.query.get(shortname=test_project_with_repo)
+        self.git = self.project.app_instance('src')
+        self.wh = M.Webhook(
+            type='repo-push',
+            app_config_id=self.git.config._id,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(self.wh).flush(self.wh)
+
+    @with_git
+    def setup_with_tools(self):
+        pass
+
+
+class TestValidators(TestWebhookBase):
+
+    def test_ming_one_of(self):
+        ids = [ac._id for ac in M.AppConfig.query.find().all()[:2]]
+        v = MingOneOf(cls=M.AppConfig, ids=ids, not_empty=True)
+        with assert_raises(Invalid) as cm:
+            v.to_python(None)
+        assert_equal(cm.exception.msg, u'Please enter a value')
+        with assert_raises(Invalid) as cm:
+            v.to_python('invalid id')
+        assert_equal(cm.exception.msg,
+            u'Object must be one of: %s, not invalid id' % ids)
+        assert_equal(v.to_python(ids[0]), M.AppConfig.query.get(_id=ids[0]))
+        assert_equal(v.to_python(ids[1]), M.AppConfig.query.get(_id=ids[1]))
+        assert_equal(v.to_python(unicode(ids[0])),
+                     M.AppConfig.query.get(_id=ids[0]))
+        assert_equal(v.to_python(unicode(ids[1])),
+                     M.AppConfig.query.get(_id=ids[1]))
+
+    def test_webhook_validator(self):
+        sender = Mock(type='repo-push')
+        ids = [ac._id for ac in M.AppConfig.query.find().all()[:3]]
+        ids, invalid_id = ids[:2], ids[2]
+        v = WebhookValidator(sender=sender, ac_ids=ids, not_empty=True)
+        with assert_raises(Invalid) as cm:
+            v.to_python(None)
+        assert_equal(cm.exception.msg, u'Please enter a value')
+        with assert_raises(Invalid) as cm:
+            v.to_python('invalid id')
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh = M.Webhook(type='invalid type',
+                       app_config_id=invalid_id,
+                       hook_url='http://httpbin.org/post',
+                       secret='secret')
+        session(wh).flush(wh)
+        with assert_raises(Invalid) as cm:
+            v.to_python(wh._id)
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh.type = 'repo-push'
+        session(wh).flush(wh)
+        with assert_raises(Invalid) as cm:
+            v.to_python(wh._id)
+        assert_equal(cm.exception.msg, u'Invalid webhook')
+
+        wh.app_config_id = ids[0]
+        session(wh).flush(wh)
+        assert_equal(v.to_python(wh._id), wh)
+        assert_equal(v.to_python(unicode(wh._id)), wh)
+
+
+class TestWebhookController(TestController):
+
+    def setUp(self):
+        super(TestWebhookController, self).setUp()
+        self.setup_with_tools()
+        self.patches = self.monkey_patch()
+        for p in self.patches:
+            p.start()
+        self.project = M.Project.query.get(shortname=test_project_with_repo)
+        self.git = self.project.app_instance('src')
+        self.git2 = self.project.app_instance('src2')
+        self.url = str(self.project.url() + 'admin/webhooks')
+
+    def tearDown(self):
+        super(TestWebhookController, self).tearDown()
+        for p in self.patches:
+            p.stop()
+
+    @with_git
+    @with_git2
+    def setup_with_tools(self):
+        pass
+
+    def monkey_patch(self):
+        gen_secret = patch.object(
+            WebhookController,
+            'gen_secret',
+            return_value='super-secret',
+            autospec=True)
+        return [gen_secret]
+
+    def create_webhook(self, data):
+        r = self.app.post(self.url + '/repo-push/create', data)
+        wf = json.loads(self.webflash(r))
+        assert_equal(wf['status'], 'ok')
+        assert_equal(wf['message'], 'Created successfully')
+        return r
+
+    def find_error(self, r, field, msg, form_type='create'):
+        form = r.html.find('form', attrs={'action': form_type})
+        if field == '_the_form':
+            error = form.findPrevious('div', attrs={'class': 'error'})
+        else:
+            widget = 'select' if field == 'app' else 'input'
+            error = form.find(widget, attrs={'name': field})
+            error = error.findNext('div', attrs={'class': 'error'})
+        if error:
+            assert_in(h.escape(msg), error.getText())
+        else:
+            assert False, 'Validation error not found'
+
+    def test_access(self):
+        self.app.get(self.url + '/repo-push/')
+        self.app.get(self.url + '/repo-push/',
+                     extra_environ={'username': 'test-user'},
+                     status=403)
+        r = self.app.get(self.url + '/repo-push/',
+                         extra_environ={'username': '*anonymous'},
+                         status=302)
+        assert_equal(r.location,
+            'http://localhost/auth/'
+            '?return_to=%2Fadobe%2Fadobe-1%2Fadmin%2Fwebhooks%2Frepo-push%2F')
+
+    def test_invalid_hook_type(self):
+        self.app.get(self.url + '/invalid-hook-type/', status=404)
+
+    def test_create(self):
+        assert_equal(M.Webhook.query.find().count(), 0)
+        r = self.app.get(self.url)
+        assert_in('<h1>repo-push</h1>', r)
+        assert_not_in('http://httpbin.org/post', r)
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': ''}
+        msg = 'add webhook repo-push {} {}'.format(
+            data['url'], self.git.config.url())
+        with td.audits(msg):
+            r = self.create_webhook(data).follow().follow(status=200)
+        assert_in('http://httpbin.org/post', r)
+
+        hooks = M.Webhook.query.find().all()
+        assert_equal(len(hooks), 1)
+        assert_equal(hooks[0].type, 'repo-push')
+        assert_equal(hooks[0].hook_url, 'http://httpbin.org/post')
+        assert_equal(hooks[0].app_config_id, self.git.config._id)
+        assert_equal(hooks[0].secret, 'super-secret')
+
+        # Try to create duplicate
+        with td.out_audits(msg):
+            r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, '_the_form',
+            '"repo-push" webhook already exists for Git http://httpbin.org/post')
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+    def test_create_validation(self):
+        assert_equal(M.Webhook.query.find().count(), 0)
+        r = self.app.post(
+            self.url + '/repo-push/create', {}, status=404)
+
+        data = {'url': '', 'app': '', 'secret': ''}
+        r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, 'url', 'Please enter a value')
+        self.find_error(r, 'app', 'Please enter a value')
+
+        data = {'url': 'qwer', 'app': '123', 'secret': 'qwe'}
+        r = self.app.post(self.url + '/repo-push/create', data)
+        self.find_error(r, 'url',
+            'You must provide a full domain name (like qwer.com)')
+        self.find_error(r, 'app', 'Object must be one of: ')
+        self.find_error(r, 'app', '%s' % self.git.config._id)
+        self.find_error(r, 'app', '%s' % self.git2.config._id)
+
+    def test_edit(self):
+        data1 = {'url': u'http://httpbin.org/post',
+                 'app': unicode(self.git.config._id),
+                 'secret': u'secret'}
+        data2 = {'url': u'http://example.com/hook',
+                 'app': unicode(self.git2.config._id),
+                 'secret': u'secret2'}
+        self.create_webhook(data1).follow().follow(status=200)
+        self.create_webhook(data2).follow().follow(status=200)
+        assert_equal(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_equal(form['url'].value, data1['url'])
+        assert_equal(form['app'].value, data1['app'])
+        assert_equal(form['secret'].value, data1['secret'])
+        assert_equal(form['webhook'].value, unicode(wh1._id))
+        form['url'] = 'http://host.org/hook'
+        form['app'] = unicode(self.git2.config._id)
+        form['secret'] = 'new secret'
+        msg = 'edit webhook repo-push\n{} => {}\n{} => {}\n{}'.format(
+            data1['url'], form['url'].value,
+            self.git.config.url(), self.git2.config.url(),
+            'secret changed')
+        with td.audits(msg):
+            r = form.submit()
+        wf = json.loads(self.webflash(r))
+        assert_equal(wf['status'], 'ok')
+        assert_equal(wf['message'], 'Edited successfully')
+        assert_equal(M.Webhook.query.find().count(), 2)
+        wh1 = M.Webhook.query.get(_id=wh1._id)
+        assert_equal(wh1.hook_url, 'http://host.org/hook')
+        assert_equal(wh1.app_config_id, self.git2.config._id)
+        assert_equal(wh1.secret, 'new secret')
+        assert_equal(wh1.type, 'repo-push')
+
+        # Duplicates
+        r = self.app.get(self.url + '/repo-push/%s' % wh1._id)
+        form = r.forms[0]
+        form['url'] = data2['url']
+        form['app'] = data2['app']
+        r = form.submit()
+        self.find_error(r, '_the_form',
+            u'"repo-push" webhook already exists for Git2 http://example.com/hook',
+            form_type='edit')
+
+    def test_edit_validation(self):
+        invalid = M.Webhook(
+            type='invalid type',
+            app_config_id=None,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(invalid).flush(invalid)
+        self.app.get(self.url + '/repo-push/%s' % invalid._id, status=404)
+
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': u'secret'}
+        self.create_webhook(data).follow().follow(status=200)
+        wh = M.Webhook.query.get(hook_url=data['url'], type='repo-push')
+
+        # invalid id in hidden field, just in case
+        r = self.app.get(self.url + '/repo-push/%s' % wh._id)
+        data = {k: v[0].value for (k, v) in r.forms[0].fields.items()}
+        data['webhook'] = unicode(invalid._id)
+        self.app.post(self.url + '/repo-push/edit', data, status=404)
+
+        # empty values
+        data = {'url': '', 'app': '', 'secret': '', 'webhook': str(wh._id)}
+        r = self.app.post(self.url + '/repo-push/edit', data)
+        self.find_error(r, 'url', 'Please enter a value', 'edit')
+        self.find_error(r, 'app', 'Please enter a value', 'edit')
+
+        data = {'url': 'qwe', 'app': '123', 'secret': 'qwe',
+                'webhook': str(wh._id)}
+        r = self.app.post(self.url + '/repo-push/edit', data)
+        self.find_error(r, 'url',
+            'You must provide a full domain name (like qwe.com)', 'edit')
+        self.find_error(r, 'app', 'Object must be one of:', 'edit')
+        self.find_error(r, 'app', '%s' % self.git.config._id, 'edit')
+        self.find_error(r, 'app', '%s' % self.git2.config._id, 'edit')
+
+    def test_delete(self):
+        data = {'url': u'http://httpbin.org/post',
+                'app': unicode(self.git.config._id),
+                'secret': u'secret'}
+        self.create_webhook(data).follow().follow(status=200)
+        assert_equal(M.Webhook.query.find().count(), 1)
+        wh = M.Webhook.query.get(hook_url=data['url'])
+        data = {'webhook': unicode(wh._id)}
+        msg = 'delete webhook repo-push {} {}'.format(
+            wh.hook_url, self.git.config.url())
+        with td.audits(msg):
+            r = self.app.post(self.url + '/repo-push/delete', data)
+        assert_equal(r.json, {'status': 'ok'})
+        assert_equal(M.Webhook.query.find().count(), 0)
+
+    def test_delete_validation(self):
+        invalid = M.Webhook(
+            type='invalid type',
+            app_config_id=None,
+            hook_url='http://httpbin.org/post',
+            secret='secret')
+        session(invalid).flush(invalid)
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+        data = {'webhook': ''}
+        self.app.post(self.url + '/repo-push/delete', data, status=404)
+
+        data = {'webhook': unicode(invalid._id)}
+        self.app.post(self.url + '/repo-push/delete', data, status=404)
+        assert_equal(M.Webhook.query.find().count(), 1)
+
+    def test_list_webhooks(self):
+        data1 = {'url': u'http://httpbin.org/post',
+                 'app': unicode(self.git.config._id),
+                 'secret': 'secret'}
+        data2 = {'url': u'http://another-host.org/',
+                 'app': unicode(self.git2.config._id),
+                 'secret': 'secret2'}
+        self.create_webhook(data1).follow().follow(status=200)
+        self.create_webhook(data2).follow().follow(status=200)
+        wh1 = M.Webhook.query.get(hook_url=data1['url'])
+        wh2 = M.Webhook.query.get(hook_url=data2['url'])
+
+        r = self.app.get(self.url)
+        assert_in('<h1>repo-push</h1>', r)
+        rows = r.html.find('table').findAll('tr')
+        assert_equal(len(rows), 2)
+        rows = sorted([self._format_row(row) for row in rows])
+        expected_rows = sorted([
+            [{'href': self.url + '/repo-push/' + str(wh1._id),
+              'text': wh1.hook_url},
+             {'href': self.git.url,
+              'text': self.git.config.options.mount_label},
+             {'text': wh1.secret},
+             {'href': self.url + '/repo-push/delete',
+              'data-id': str(wh1._id)}],
+            [{'href': self.url + '/repo-push/' + str(wh2._id),
+              'text': wh2.hook_url},
+             {'href': self.git2.url,
+              'text': self.git2.config.options.mount_label},
+             {'text': wh2.secret},
+             {'href': self.url + '/repo-push/delete',
+              'data-id': str(wh2._id)}],
+        ])
+        assert_equal(rows, expected_rows)
+
+    def _format_row(self, row):
+        def link(td):
+            a = td.find('a')
+            return {'href': a.get('href'), 'text': a.getText()}
+        def text(td):
+            return {'text': td.getText()}
+        def delete_btn(td):
+            a = td.find('a')
+            return {'href': a.get('href'), 'data-id': a.get('data-id')}
+        tds = row.findAll('td')
+        return [link(tds[0]), link(tds[1]), text(tds[2]), delete_btn(tds[3])]
+
+
+class TestTasks(TestWebhookBase):
+
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send_webhook(self, log, requests):
+        requests.post.return_value = Mock(status_code=200)
+        payload = {'some': ['data']}
+        json_payload = json.dumps(payload, cls=DateJSONEncoder)
+        send_webhook(self.wh._id, payload)
+        signature = hmac.new(
+            self.wh.secret.encode('utf-8'),
+            json_payload.encode('utf-8'),
+            hashlib.sha1)
+        signature = 'sha1=' + signature.hexdigest()
+        headers = {'content-type': 'application/json',
+                   'User-Agent': 'Allura Webhook (https://allura.apache.org/)',
+                   'X-Allura-Signature': signature}
+        requests.post.assert_called_once_with(
+            self.wh.hook_url,
+            data=json_payload,
+            headers=headers,
+            timeout=30)
+        log.info.assert_called_once_with(
+            'Webhook successfully sent: %s %s %s',
+            self.wh.type, self.wh.hook_url, self.wh.app_config.url())
+
+    @patch('allura.webhooks.requests', autospec=True)
+    @patch('allura.webhooks.log', autospec=True)
+    def test_send_webhook_error(self, log, requests):
+        requests.post.return_value = Mock(status_code=500)
+        send_webhook(self.wh._id, {})
+        assert_equal(requests.post.call_count, 1)
+        assert_equal(log.info.call_count, 0)
+        log.error.assert_called_once_with(
+            'Webhook send error: %s %s %s %s %s',
+            self.wh.type, self.wh.hook_url,
+            self.wh.app_config.url(),
+            requests.post.return_value.status_code,
+            requests.post.return_value.reason)
+
+class TestRepoPushWebhookSender(TestWebhookBase):
+
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send(self, send_webhook):
+        sender = RepoPushWebhookSender()
+        sender.get_payload = Mock()
+        with h.push_config(c, app=self.git):
+            sender.send(arg1=1, arg2=2)
+        send_webhook.post.assert_called_once_with(
+            self.wh._id,
+            sender.get_payload.return_value)
+
+    @patch('allura.webhooks.send_webhook', autospec=True)
+    def test_send_no_configured_webhooks(self, send_webhook):
+        self.wh.delete()
+        session(self.wh).flush(self.wh)
+        sender = RepoPushWebhookSender()
+        with h.push_config(c, app=self.git):
+            sender.send(arg1=1, arg2=2)
+        assert_equal(send_webhook.post.call_count, 0)
+
+    def test_get_payload(self):
+        sender = RepoPushWebhookSender()
+        _ci = list(range(1, 4))
+        _se = [Mock(info=str(x)) for x in _ci]
+        with patch.object(self.git.repo, 'commit', autospec=True, side_effect=_se):
+            with h.push_config(c, app=self.git):
+                result = sender.get_payload(commit_ids=_ci)
+        expected_result = {
+            'url': 'http://localhost/adobe/adobe-1/src/',
+            'count': 3,
+            'revisions': ['1', '2', '3'],
+        }
+        assert_equal(result, expected_result)
+
+
+class TestModels(TestWebhookBase):
+
+    def test_webhook_find(self):
+        p = M.Project.query.get(shortname='test')
+        assert_equal(M.Webhook.find('smth', p), [])
+        assert_equal(M.Webhook.find('repo-push', p), [])
+        assert_equal(M.Webhook.find('smth', self.project), [])
+        assert_equal(M.Webhook.find('repo-push', self.project), [self.wh])
+
+    def test_webhook_url(self):
+        assert_equal(self.wh.url(),
+            '/adobe/adobe-1/admin/webhooks/repo-push/{}'.format(self.wh._id))

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/Allura/allura/webhooks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/webhooks.py b/Allura/allura/webhooks.py
index 8495786..2393acd 100644
--- a/Allura/allura/webhooks.py
+++ b/Allura/allura/webhooks.py
@@ -236,7 +236,7 @@ def send_webhook(webhook_id, payload):
     # TODO: catch
     # TODO: configurable timeout
     r = requests.post(url, data=json_payload, headers=headers, timeout=30)
-    if r.status_code >= 200 and r.status_code <= 300:
+    if r.status_code >= 200 and r.status_code < 300:
         log.info('Webhook successfully sent: %s %s %s',
                  webhook.type, webhook.hook_url, webhook.app_config.url())
     else:

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 4d7a0c0..5e86ab0 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -39,6 +39,7 @@ from allura.tests import decorators as td
 from allura.tests.model.test_repo import RepoImplTestBase
 from allura import model as M
 from allura.model.repo_refresh import send_notifications
+from allura.webhooks import RepoPushWebhookSender
 from forgegit import model as GM
 from forgegit.tests import with_git
 from forgewiki import model as WM
@@ -519,6 +520,30 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
                 self.repo.clone_url('https', 'user'),
                 'https://user@foo.com/')
 
+    def test_webhook_payload(self):
+        sender = RepoPushWebhookSender()
+        cids = list(self.repo.all_commit_ids())[:2]
+        payload = sender.get_payload(commit_ids=cids)
+        expected_payload = {
+            'url': 'http://localhost/p/test/src-git/',
+            'count': 2,
+            'revisions': [
+                {'author': u'Cory Johns',
+                 'author_email': u'cjohns@slashdotmedia.com',
+                 'author_url': None,
+                 'date': datetime.datetime(2013, 3, 28, 18, 54, 16),
+                 'id': u'5c47243c8e424136fd5cdd18cd94d34c66d1955c',
+                 'shortlink': u'[5c4724]',
+                 'summary': u'Not repo root'},
+                {'author': u'Rick Copeland',
+                 'author_email': u'rcopeland@geek.net',
+                 'author_url': None,
+                 'date': datetime.datetime(2010, 10, 7, 18, 44, 11),
+                 'id': u'1e146e67985dcd71c74de79613719bef7bddca4a',
+                 'shortlink': u'[1e146e]',
+                 'summary': u'Change README'}]}
+        assert_equal(payload, expected_payload)
+
 
 class TestGitImplementation(unittest.TestCase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/e7ace573/ForgeSVN/forgesvn/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/tests/model/test_repository.py b/ForgeSVN/forgesvn/tests/model/test_repository.py
index 70c34ac..5267614 100644
--- a/ForgeSVN/forgesvn/tests/model/test_repository.py
+++ b/ForgeSVN/forgesvn/tests/model/test_repository.py
@@ -38,6 +38,7 @@ from alluratest.controller import setup_basic_test, setup_global_objects
 from allura import model as M
 from allura.model.repo_refresh import send_notifications
 from allura.lib import helpers as h
+from allura.webhooks import RepoPushWebhookSender
 from allura.tests.model.test_repo import RepoImplTestBase
 
 from forgesvn import model as SM
@@ -569,6 +570,30 @@ class TestSVNRepo(unittest.TestCase, RepoImplTestBase):
             ThreadLocalORMSession.flush_all()
             assert repo2.is_empty()
 
+    def test_webhook_payload(self):
+        sender = RepoPushWebhookSender()
+        cids = list(self.repo.all_commit_ids())[:2]
+        payload = sender.get_payload(commit_ids=cids)
+        expected_payload = {
+            'url': 'http://localhost/p/test/src/',
+            'count': 2,
+            'revisions': [
+                {'author': u'coldmind',
+                 'author_email': u'',
+                 'author_url': None,
+                 'date': datetime(2013, 11, 8, 13, 38, 11, 152000),
+                 'id': u'{}:6'.format(self.repo._id),
+                 'shortlink': '[r6]',
+                 'summary': ''},
+                {'author': u'rick446',
+                 'author_email': u'',
+                 'author_url': None,
+                 'date': datetime(2010, 11, 18, 20, 14, 21, 515000),
+                 'id': u'{}:5'.format(self.repo._id),
+                 'shortlink': '[r5]',
+                 'summary': u'Copied a => b'}]}
+        assert_equal(payload, expected_payload)
+
 
 class TestSVNRev(unittest.TestCase):