US #1678: Add webhooks to the backend
parent
97fa6bca56
commit
47107eb079
|
@ -197,6 +197,7 @@ INSTALLED_APPS = [
|
||||||
"taiga.hooks.github",
|
"taiga.hooks.github",
|
||||||
"taiga.hooks.gitlab",
|
"taiga.hooks.gitlab",
|
||||||
"taiga.hooks.bitbucket",
|
"taiga.hooks.bitbucket",
|
||||||
|
"taiga.webhooks",
|
||||||
|
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"djmail",
|
"djmail",
|
||||||
|
@ -366,6 +367,7 @@ GITLAB_VALID_ORIGIN_IPS = []
|
||||||
|
|
||||||
EXPORTS_TTL = 60 * 60 * 24 # 24 hours
|
EXPORTS_TTL = 60 * 60 * 24 # 24 hours
|
||||||
CELERY_ENABLED = False
|
CELERY_ENABLED = False
|
||||||
|
WEBHOOKS_ENABLED = False
|
||||||
|
|
||||||
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
|
||||||
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
TEST_RUNNER="django.test.runner.DiscoverRunner"
|
||||||
|
|
|
@ -19,6 +19,7 @@ from .development import *
|
||||||
SKIP_SOUTH_TESTS = True
|
SKIP_SOUTH_TESTS = True
|
||||||
SOUTH_TESTS_MIGRATE = False
|
SOUTH_TESTS_MIGRATE = False
|
||||||
CELERY_ALWAYS_EAGER = True
|
CELERY_ALWAYS_EAGER = True
|
||||||
|
CELERY_ENABLED = False
|
||||||
|
|
||||||
MEDIA_ROOT = "/tmp"
|
MEDIA_ROOT = "/tmp"
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,52 @@ class IsProjectMemberFilterBackend(FilterBackend):
|
||||||
|
|
||||||
return super().filter_queryset(request, queryset.distinct(), view)
|
return super().filter_queryset(request, queryset.distinct(), view)
|
||||||
|
|
||||||
|
class BaseIsProjectAdminFilterBackend(object):
|
||||||
|
def get_project_ids(self, request, view):
|
||||||
|
project_id = None
|
||||||
|
if hasattr(view, "filter_fields") and "project" in view.filter_fields:
|
||||||
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
|
|
||||||
|
if request.user.is_authenticated() and request.user.is_superuser:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not request.user.is_authenticated():
|
||||||
|
return []
|
||||||
|
|
||||||
|
memberships_qs = Membership.objects.filter(user=request.user, is_owner=True)
|
||||||
|
if project_id:
|
||||||
|
memberships_qs = memberships_qs.filter(project_id=project_id)
|
||||||
|
|
||||||
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
|
||||||
|
return projects_list
|
||||||
|
|
||||||
|
|
||||||
|
class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
project_ids = self.get_project_ids(request, view)
|
||||||
|
if project_ids is None:
|
||||||
|
queryset = queryset
|
||||||
|
elif project_ids == []:
|
||||||
|
queryset = queryset.none()
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(project_id__in=project_ids)
|
||||||
|
|
||||||
|
return super().filter_queryset(request, queryset.distinct(), view)
|
||||||
|
|
||||||
|
|
||||||
|
class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
project_ids = self.get_project_ids(request, view)
|
||||||
|
if project_ids is None:
|
||||||
|
queryset = queryset
|
||||||
|
elif project_ids == []:
|
||||||
|
queryset = queryset.none()
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(webhook__project_id__in=project_ids)
|
||||||
|
|
||||||
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
class TagsFilter(FilterBackend):
|
class TagsFilter(FilterBackend):
|
||||||
def __init__(self, filter_name='tags'):
|
def __init__(self, filter_name='tags'):
|
||||||
|
|
|
@ -31,6 +31,13 @@ def get_typename_for_model_class(model:object, for_concrete_model=True) -> str:
|
||||||
|
|
||||||
return "{0}.{1}".format(model._meta.app_label, model._meta.model_name)
|
return "{0}.{1}".format(model._meta.app_label, model._meta.model_name)
|
||||||
|
|
||||||
|
def get_typename_for_model_instance(model_instance):
|
||||||
|
"""
|
||||||
|
Get content type tuple from model instance.
|
||||||
|
"""
|
||||||
|
ct = ContentType.objects.get_for_model(model_instance)
|
||||||
|
return ".".join([ct.app_label, ct.model])
|
||||||
|
|
||||||
|
|
||||||
def reload_attribute(model_instance, attr_name):
|
def reload_attribute(model_instance, attr_name):
|
||||||
"""Fetch the stored value of a model instance attribute.
|
"""Fetch the stored value of a model instance attribute.
|
||||||
|
|
|
@ -18,6 +18,7 @@ import collections
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
from . import middleware as mw
|
from . import middleware as mw
|
||||||
from . import backends
|
from . import backends
|
||||||
|
|
||||||
|
@ -32,14 +33,6 @@ watched_types = set([
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def _get_type_for_model(model_instance):
|
|
||||||
"""
|
|
||||||
Get content type tuple from model instance.
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(model_instance)
|
|
||||||
return ".".join([ct.app_label, ct.model])
|
|
||||||
|
|
||||||
|
|
||||||
def emit_event(data:dict, routing_key:str, *,
|
def emit_event(data:dict, routing_key:str, *,
|
||||||
sessionid:str=None, channel:str="events"):
|
sessionid:str=None, channel:str="events"):
|
||||||
if not sessionid:
|
if not sessionid:
|
||||||
|
@ -64,7 +57,7 @@ def emit_event_for_model(obj, *, type:str="change", channel:str="events",
|
||||||
assert hasattr(obj, "project_id")
|
assert hasattr(obj, "project_id")
|
||||||
|
|
||||||
if not content_type:
|
if not content_type:
|
||||||
content_type = _get_type_for_model(obj)
|
content_type = get_typename_for_model_instance(obj)
|
||||||
|
|
||||||
projectid = getattr(obj, "project_id")
|
projectid = getattr(obj, "project_id")
|
||||||
pk = getattr(obj, "pk", None)
|
pk = getattr(obj, "pk", None)
|
||||||
|
|
|
@ -17,13 +17,15 @@
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
|
|
||||||
from . import middleware as mw
|
from . import middleware as mw
|
||||||
from . import events
|
from . import events
|
||||||
|
|
||||||
|
|
||||||
def on_save_any_model(sender, instance, created, **kwargs):
|
def on_save_any_model(sender, instance, created, **kwargs):
|
||||||
# Ignore any object that can not have project_id
|
# Ignore any object that can not have project_id
|
||||||
content_type = events._get_type_for_model(instance)
|
content_type = get_typename_for_model_instance(instance)
|
||||||
|
|
||||||
# Ignore any other events
|
# Ignore any other events
|
||||||
if content_type not in events.watched_types:
|
if content_type not in events.watched_types:
|
||||||
|
@ -39,7 +41,7 @@ def on_save_any_model(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
def on_delete_any_model(sender, instance, **kwargs):
|
def on_delete_any_model(sender, instance, **kwargs):
|
||||||
# Ignore any object that can not have project_id
|
# Ignore any object that can not have project_id
|
||||||
content_type = events._get_type_for_model(instance)
|
content_type = get_typename_for_model_instance(instance)
|
||||||
|
|
||||||
# Ignore any other changes
|
# Ignore any other changes
|
||||||
if content_type not in events.watched_types:
|
if content_type not in events.watched_types:
|
||||||
|
|
|
@ -90,6 +90,10 @@ router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-att
|
||||||
router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments")
|
router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments")
|
||||||
router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
|
router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
from taiga.webhooks.api import WebhookViewSet, WebhookLogViewSet
|
||||||
|
router.register(r"webhooks", WebhookViewSet, base_name="webhooks")
|
||||||
|
router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs")
|
||||||
|
|
||||||
# History & Components
|
# History & Components
|
||||||
from taiga.projects.history.api import UserStoryHistory
|
from taiga.projects.history.api import UserStoryHistory
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
default_app_config = "taiga.webhooks.apps.WebhooksAppConfig"
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from taiga.base import filters
|
||||||
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
|
from taiga.base.decorators import detail_route
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import serializers
|
||||||
|
from . import permissions
|
||||||
|
from . import tasks
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookViewSet(ModelCrudViewSet):
|
||||||
|
model = models.Webhook
|
||||||
|
serializer_class = serializers.WebhookSerializer
|
||||||
|
permission_classes = (permissions.WebhookPermission,)
|
||||||
|
filter_backends = (filters.IsProjectAdminFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def test(self, request, pk=None):
|
||||||
|
webhook = self.get_object()
|
||||||
|
self.check_permissions(request, 'test', webhook)
|
||||||
|
|
||||||
|
tasks.test_webhook(webhook.id, webhook.url, webhook.key)
|
||||||
|
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
class WebhookLogViewSet(ModelListViewSet):
|
||||||
|
model = models.WebhookLog
|
||||||
|
serializer_class = serializers.WebhookLogSerializer
|
||||||
|
permission_classes = (permissions.WebhookLogPermission,)
|
||||||
|
filter_backends = (filters.IsProjectAdminFromWebhookLogFilterBackend,)
|
||||||
|
filter_fields = ("webhook",)
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def resend(self, request, pk=None):
|
||||||
|
webhooklog = self.get_object()
|
||||||
|
self.check_permissions(request, 'resend', webhooklog)
|
||||||
|
|
||||||
|
webhook = webhooklog.webhook
|
||||||
|
|
||||||
|
tasks.resend_webhook(webhook.id, webhook.url, webhook.key, webhooklog.request_data)
|
||||||
|
|
||||||
|
return Response()
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.db.models import signals
|
||||||
|
|
||||||
|
from . import signal_handlers as handlers
|
||||||
|
from taiga.projects.history.models import HistoryEntry
|
||||||
|
|
||||||
|
|
||||||
|
def connect_webhooks_signals():
|
||||||
|
signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="webhooks")
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect_webhooks_signals():
|
||||||
|
signals.post_save.disconnect(dispatch_uid="webhooks")
|
||||||
|
|
||||||
|
|
||||||
|
class WebhooksAppConfig(AppConfig):
|
||||||
|
name = "taiga.webhooks"
|
||||||
|
verbose_name = "Webhooks App Config"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
connect_webhooks_signals()
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import django_pgjson.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0015_auto_20141230_1212'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Webhook',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
|
||||||
|
('url', models.URLField(verbose_name='URL')),
|
||||||
|
('key', models.TextField(verbose_name='secret key')),
|
||||||
|
('project', models.ForeignKey(related_name='webhooks', to='projects.Project')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebhookLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
|
||||||
|
('url', models.URLField(verbose_name='URL')),
|
||||||
|
('status', models.IntegerField(verbose_name='Status code')),
|
||||||
|
('request_data', django_pgjson.fields.JsonField(verbose_name='Request data')),
|
||||||
|
('response_data', models.TextField(verbose_name='Response data')),
|
||||||
|
('webhook', models.ForeignKey(related_name='logs', to='webhooks.Webhook')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django_pgjson.fields import JsonField
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook(models.Model):
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="webhooks")
|
||||||
|
url = models.URLField(null=False, blank=False, verbose_name=_("URL"))
|
||||||
|
key = models.TextField(null=False, blank=False, verbose_name=_("secret key"))
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLog(models.Model):
|
||||||
|
webhook = models.ForeignKey(Webhook, null=False, blank=False,
|
||||||
|
related_name="logs")
|
||||||
|
url = models.URLField(null=False, blank=False, verbose_name=_("URL"))
|
||||||
|
status = models.IntegerField(null=False, blank=False, verbose_name=_("Status code"))
|
||||||
|
request_data = JsonField(null=False, blank=False, verbose_name=_("Request data"))
|
||||||
|
response_data = models.TextField(null=False, blank=False, verbose_name=_("Response data"))
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectOwner,
|
||||||
|
AllowAny, PermissionComponent)
|
||||||
|
|
||||||
|
from taiga.permissions.service import is_project_owner
|
||||||
|
|
||||||
|
|
||||||
|
class IsWebhookProjectOwner(PermissionComponent):
|
||||||
|
def check_permissions(self, request, view, obj=None):
|
||||||
|
return is_project_owner(request.user, obj.webhook.project)
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookPermission(TaigaResourcePermission):
|
||||||
|
retrieve_perms = IsProjectOwner()
|
||||||
|
create_perms = IsProjectOwner()
|
||||||
|
update_perms = IsProjectOwner()
|
||||||
|
destroy_perms = IsProjectOwner()
|
||||||
|
list_perms = AllowAny()
|
||||||
|
test_perms = IsProjectOwner()
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLogPermission(TaigaResourcePermission):
|
||||||
|
retrieve_perms = IsWebhookProjectOwner()
|
||||||
|
list_perms = AllowAny()
|
||||||
|
resend_perms = IsWebhookProjectOwner()
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from taiga.base.serializers import TagsField, PgArrayField, JsonField
|
||||||
|
|
||||||
|
from taiga.projects.userstories import models as us_models
|
||||||
|
from taiga.projects.tasks import models as task_models
|
||||||
|
from taiga.projects.issues import models as issue_models
|
||||||
|
from taiga.projects.milestones import models as milestone_models
|
||||||
|
from taiga.projects.history import models as history_models
|
||||||
|
from taiga.projects.wiki import models as wiki_models
|
||||||
|
|
||||||
|
from .models import Webhook, WebhookLog
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryDiffField(serializers.Field):
|
||||||
|
def to_native(self, obj):
|
||||||
|
return {key: {"from": value[0], "to": value[1]} for key, value in obj.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
||||||
|
|
||||||
|
class WebhookLogSerializer(serializers.ModelSerializer):
|
||||||
|
request_data = JsonField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WebhookLog
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.Serializer):
|
||||||
|
id = serializers.SerializerMethodField("get_pk")
|
||||||
|
name = serializers.SerializerMethodField("get_name")
|
||||||
|
|
||||||
|
def get_pk(self, obj):
|
||||||
|
return obj.pk
|
||||||
|
|
||||||
|
def get_name(self, obj):
|
||||||
|
return obj.full_name
|
||||||
|
|
||||||
|
class PointSerializer(serializers.Serializer):
|
||||||
|
id = serializers.SerializerMethodField("get_pk")
|
||||||
|
name = serializers.SerializerMethodField("get_name")
|
||||||
|
value = serializers.SerializerMethodField("get_value")
|
||||||
|
|
||||||
|
def get_pk(self, obj):
|
||||||
|
return obj.pk
|
||||||
|
|
||||||
|
def get_name(self, obj):
|
||||||
|
return obj.name
|
||||||
|
|
||||||
|
def get_value(self, obj):
|
||||||
|
return obj.value
|
||||||
|
|
||||||
|
|
||||||
|
class UserStorySerializer(serializers.ModelSerializer):
|
||||||
|
tags = TagsField(default=[], required=False)
|
||||||
|
external_reference = PgArrayField(required=False)
|
||||||
|
owner = UserSerializer()
|
||||||
|
assigned_to = UserSerializer()
|
||||||
|
watchers = UserSerializer(many=True)
|
||||||
|
points = PointSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = us_models.UserStory
|
||||||
|
exclude = ("backlog_order", "sprint_order", "kanban_order", "version")
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TagsField(default=[], required=False)
|
||||||
|
owner = UserSerializer()
|
||||||
|
assigned_to = UserSerializer()
|
||||||
|
watchers = UserSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = task_models.Task
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TagsField(default=[], required=False)
|
||||||
|
owner = UserSerializer()
|
||||||
|
assigned_to = UserSerializer()
|
||||||
|
watchers = UserSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = issue_models.Issue
|
||||||
|
|
||||||
|
|
||||||
|
class WikiPageSerializer(serializers.ModelSerializer):
|
||||||
|
owner = UserSerializer()
|
||||||
|
last_modifier = UserSerializer()
|
||||||
|
watchers = UserSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = wiki_models.WikiPage
|
||||||
|
exclude = ("watchers", "version")
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneSerializer(serializers.ModelSerializer):
|
||||||
|
owner = UserSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = milestone_models.Milestone
|
||||||
|
exclude = ("order", "watchers")
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryEntrySerializer(serializers.ModelSerializer):
|
||||||
|
diff = HistoryDiffField()
|
||||||
|
snapshot = JsonField()
|
||||||
|
values = JsonField()
|
||||||
|
user = JsonField()
|
||||||
|
delete_comment_user = JsonField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = history_models.HistoryEntry
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from taiga.projects.history import services as history_service
|
||||||
|
from taiga.projects.history.choices import HistoryType
|
||||||
|
|
||||||
|
from . import tasks
|
||||||
|
|
||||||
|
|
||||||
|
def _get_project_webhooks(project):
|
||||||
|
webhooks = []
|
||||||
|
for webhook in project.webhooks.all():
|
||||||
|
webhooks.append({
|
||||||
|
"id": webhook.pk,
|
||||||
|
"url": webhook.url,
|
||||||
|
"key": webhook.key,
|
||||||
|
})
|
||||||
|
return webhooks
|
||||||
|
|
||||||
|
|
||||||
|
def on_new_history_entry(sender, instance, created, **kwargs):
|
||||||
|
if not settings.WEBHOOKS_ENABLED:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if instance.is_hidden:
|
||||||
|
return None
|
||||||
|
|
||||||
|
model = history_service.get_model_from_key(instance.key)
|
||||||
|
pk = history_service.get_pk_from_key(instance.key)
|
||||||
|
obj = model.objects.get(pk=pk)
|
||||||
|
|
||||||
|
webhooks = _get_project_webhooks(obj.project)
|
||||||
|
|
||||||
|
if instance.type == HistoryType.create:
|
||||||
|
task = tasks.create_webhook
|
||||||
|
extra_args = []
|
||||||
|
elif instance.type == HistoryType.change:
|
||||||
|
task = tasks.change_webhook
|
||||||
|
extra_args = [instance]
|
||||||
|
elif instance.type == HistoryType.delete:
|
||||||
|
task = tasks.delete_webhook
|
||||||
|
extra_args = []
|
||||||
|
|
||||||
|
for webhook in webhooks:
|
||||||
|
args = [webhook["id"], webhook["url"], webhook["key"], obj] + extra_args
|
||||||
|
|
||||||
|
if settings.CELERY_ENABLED:
|
||||||
|
task.delay(*args)
|
||||||
|
else:
|
||||||
|
task(*args)
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Copyright (C) 2013 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
from rest_framework.renderers import UnicodeJSONRenderer
|
||||||
|
|
||||||
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
|
from taiga.celery import app
|
||||||
|
|
||||||
|
from .serializers import (UserStorySerializer, IssueSerializer, TaskSerializer,
|
||||||
|
WikiPageSerializer, MilestoneSerializer,
|
||||||
|
HistoryEntrySerializer)
|
||||||
|
from .models import WebhookLog
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize(obj):
|
||||||
|
content_type = get_typename_for_model_instance(obj)
|
||||||
|
|
||||||
|
if content_type == "userstories.userstory":
|
||||||
|
return UserStorySerializer(obj).data
|
||||||
|
elif content_type == "issues.issue":
|
||||||
|
return IssueSerializer(obj).data
|
||||||
|
elif content_type == "tasks.task":
|
||||||
|
return TaskSerializer(obj).data
|
||||||
|
elif content_type == "wiki.wikipage":
|
||||||
|
return WikiPageSerializer(obj).data
|
||||||
|
elif content_type == "milestones.milestone":
|
||||||
|
return MilestoneSerializer(obj).data
|
||||||
|
elif content_type == "history.historyentry":
|
||||||
|
return HistoryEntrySerializer(obj).data
|
||||||
|
|
||||||
|
|
||||||
|
def _get_type(obj):
|
||||||
|
content_type = get_typename_for_model_instance(obj)
|
||||||
|
return content_type.split(".")[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_signature(data, key):
|
||||||
|
mac = hmac.new(key.encode("utf-8"), msg=data, digestmod=hashlib.sha1)
|
||||||
|
return mac.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _send_request(webhook_id, url, key, data):
|
||||||
|
serialized_data = UnicodeJSONRenderer().render(data)
|
||||||
|
signature = _generate_signature(serialized_data, key)
|
||||||
|
headers = {
|
||||||
|
"X-TAIGA-WEBHOOK-SIGNATURE": signature,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, data=serialized_data, headers=headers)
|
||||||
|
WebhookLog.objects.create(webhook_id=webhook_id, url=url,
|
||||||
|
status=response.status_code,
|
||||||
|
request_data=data,
|
||||||
|
response_data=response.content)
|
||||||
|
except RequestException:
|
||||||
|
WebhookLog.objects.create(webhook_id=webhook_id, url=url, status=0,
|
||||||
|
request_data=data,
|
||||||
|
response_data="error-in-request")
|
||||||
|
|
||||||
|
ids = [webhook_log.id for webhook_log in WebhookLog.objects.filter(webhook_id=webhook_id).order_by("-id")[10:]]
|
||||||
|
WebhookLog.objects.filter(id__in=ids).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def change_webhook(webhook_id, url, key, obj, change):
|
||||||
|
data = {}
|
||||||
|
data['data'] = _serialize(obj)
|
||||||
|
data['action'] = "change"
|
||||||
|
data['type'] = _get_type(obj)
|
||||||
|
data['change'] = _serialize(change)
|
||||||
|
|
||||||
|
_send_request(webhook_id, url, key, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def create_webhook(webhook_id, url, key, obj):
|
||||||
|
data = {}
|
||||||
|
data['data'] = _serialize(obj)
|
||||||
|
data['action'] = "create"
|
||||||
|
data['type'] = _get_type(obj)
|
||||||
|
|
||||||
|
_send_request(webhook_id, url, key, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def delete_webhook(webhook_id, url, key, obj):
|
||||||
|
data = {}
|
||||||
|
data['data'] = _serialize(obj)
|
||||||
|
data['action'] = "delete"
|
||||||
|
data['type'] = _get_type(obj)
|
||||||
|
|
||||||
|
_send_request(webhook_id, url, key, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def resend_webhook(webhook_id, url, key, data):
|
||||||
|
_send_request(webhook_id, url, key, data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def test_webhook(webhook_id, url, key):
|
||||||
|
data = {}
|
||||||
|
data['data'] = {"test": "test"}
|
||||||
|
data['action'] = "test"
|
||||||
|
data['type'] = "test"
|
||||||
|
|
||||||
|
_send_request(webhook_id, url, key, data)
|
||||||
|
|
|
@ -197,6 +197,28 @@ class InvitationFactory(Factory):
|
||||||
email = factory.Sequence(lambda n: "user{}@email.com".format(n))
|
email = factory.Sequence(lambda n: "user{}@email.com".format(n))
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookFactory(Factory):
|
||||||
|
class Meta:
|
||||||
|
model = "webhooks.Webhook"
|
||||||
|
strategy = factory.CREATE_STRATEGY
|
||||||
|
|
||||||
|
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||||
|
url = "http://localhost:8080/test"
|
||||||
|
key = "factory-key"
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookLogFactory(Factory):
|
||||||
|
class Meta:
|
||||||
|
model = "webhooks.WebhookLog"
|
||||||
|
strategy = factory.CREATE_STRATEGY
|
||||||
|
|
||||||
|
webhook = factory.SubFactory("tests.factories.WebhookFactory")
|
||||||
|
url = "http://localhost:8080/test"
|
||||||
|
status = "200"
|
||||||
|
request_data = "test-request"
|
||||||
|
response_data = "test-response"
|
||||||
|
|
||||||
|
|
||||||
class StorageEntryFactory(Factory):
|
class StorageEntryFactory(Factory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "userstorage.StorageEntry"
|
model = "userstorage.StorageEntry"
|
||||||
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
|
from taiga.webhooks.serializers import WebhookSerializer
|
||||||
|
from taiga.webhooks.models import Webhook
|
||||||
|
from taiga.webhooks import tasks
|
||||||
|
|
||||||
|
from tests import factories as f
|
||||||
|
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module(module):
|
||||||
|
disconnect_signals()
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_module(module):
|
||||||
|
reconnect_signals()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data():
|
||||||
|
m = type("Models", (object,), {})
|
||||||
|
|
||||||
|
m.registered_user = f.UserFactory.create()
|
||||||
|
m.project_owner = f.UserFactory.create()
|
||||||
|
|
||||||
|
m.project1 = f.ProjectFactory(is_private=True,
|
||||||
|
anon_permissions=[],
|
||||||
|
public_permissions=[],
|
||||||
|
owner=m.project_owner)
|
||||||
|
m.project2 = f.ProjectFactory(is_private=True,
|
||||||
|
anon_permissions=[],
|
||||||
|
public_permissions=[],
|
||||||
|
owner=m.project_owner)
|
||||||
|
|
||||||
|
f.MembershipFactory(project=m.project1,
|
||||||
|
user=m.project_owner,
|
||||||
|
is_owner=True)
|
||||||
|
|
||||||
|
m.webhook1 = f.WebhookFactory(project=m.project1)
|
||||||
|
m.webhooklog1 = f.WebhookLogFactory(webhook=m.webhook1)
|
||||||
|
m.webhook2 = f.WebhookFactory(project=m.project2)
|
||||||
|
m.webhooklog2 = f.WebhookLogFactory(webhook=m.webhook2)
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_retrieve(client, data):
|
||||||
|
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
|
||||||
|
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'get', url1, None, users)
|
||||||
|
assert results == [401, 403, 200]
|
||||||
|
results = helper_test_http_method(client, 'get', url2, None, users)
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_update(client, data):
|
||||||
|
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
|
||||||
|
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
webhook_data = WebhookSerializer(data.webhook1).data
|
||||||
|
webhook_data["key"] = "test"
|
||||||
|
webhook_data = json.dumps(webhook_data)
|
||||||
|
results = helper_test_http_method(client, 'put', url1, webhook_data, users)
|
||||||
|
assert results == [401, 403, 200]
|
||||||
|
|
||||||
|
webhook_data = WebhookSerializer(data.webhook2).data
|
||||||
|
webhook_data["key"] = "test"
|
||||||
|
webhook_data = json.dumps(webhook_data)
|
||||||
|
results = helper_test_http_method(client, 'put', url2, webhook_data, users)
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_delete(client, data):
|
||||||
|
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
|
||||||
|
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
results = helper_test_http_method(client, 'delete', url1, None, users)
|
||||||
|
assert results == [401, 403, 204]
|
||||||
|
results = helper_test_http_method(client, 'delete', url2, None, users)
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_list(client, data):
|
||||||
|
url = reverse('webhooks-list')
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooks_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooks_data) == 0
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
client.login(data.registered_user)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooks_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooks_data) == 0
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
client.login(data.project_owner)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooks_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooks_data) == 1
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_create(client, data):
|
||||||
|
url = reverse('webhooks-list')
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
create_data = json.dumps({
|
||||||
|
"url": "http://test.com",
|
||||||
|
"key": "test",
|
||||||
|
"project": data.project1.pk,
|
||||||
|
})
|
||||||
|
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete())
|
||||||
|
assert results == [401, 403, 201]
|
||||||
|
|
||||||
|
create_data = json.dumps({
|
||||||
|
"url": "http://test.com",
|
||||||
|
"key": "test",
|
||||||
|
"project": data.project2.pk,
|
||||||
|
})
|
||||||
|
results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete())
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_patch(client, data):
|
||||||
|
url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk})
|
||||||
|
url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
patch_data = json.dumps({"key": "test"})
|
||||||
|
results = helper_test_http_method(client, 'patch', url1, patch_data, users)
|
||||||
|
assert results == [401, 403, 200]
|
||||||
|
|
||||||
|
patch_data = json.dumps({"key": "test"})
|
||||||
|
results = helper_test_http_method(client, 'patch', url2, patch_data, users)
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_action_test(client, data):
|
||||||
|
url1 = reverse('webhooks-test', kwargs={"pk": data.webhook1.pk})
|
||||||
|
url2 = reverse('webhooks-test', kwargs={"pk": data.webhook2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
|
||||||
|
results = helper_test_http_method(client, 'post', url1, None, users)
|
||||||
|
assert results == [404, 404, 200]
|
||||||
|
assert _send_request_mock.called == True
|
||||||
|
|
||||||
|
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
|
||||||
|
results = helper_test_http_method(client, 'post', url2, None, users)
|
||||||
|
assert results == [404, 404, 404]
|
||||||
|
assert _send_request_mock.called == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_list(client, data):
|
||||||
|
url = reverse('webhooklogs-list')
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooklogs_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooklogs_data) == 0
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
client.login(data.registered_user)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooklogs_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooklogs_data) == 0
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
client.login(data.project_owner)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
webhooklogs_data = json.loads(response.content.decode('utf-8'))
|
||||||
|
assert len(webhooklogs_data) == 1
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_retrieve(client, data):
|
||||||
|
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
|
||||||
|
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'get', url1, None, users)
|
||||||
|
assert results == [401, 403, 200]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'get', url2, None, users)
|
||||||
|
assert results == [401, 403, 403]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_create(client, data):
|
||||||
|
url1 = reverse('webhooklogs-list')
|
||||||
|
url2 = reverse('webhooklogs-list')
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', url1, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', url2, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_delete(client, data):
|
||||||
|
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
|
||||||
|
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'delete', url1, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'delete', url2, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_update(client, data):
|
||||||
|
url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk})
|
||||||
|
url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'put', url1, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'put', url2, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'patch', url1, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'patch', url2, None, users)
|
||||||
|
assert results == [405, 405, 405]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhooklogs_action_resend(client, data):
|
||||||
|
url1 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog1.pk})
|
||||||
|
url2 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog2.pk})
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', url1, None, users)
|
||||||
|
assert results == [404, 404, 200]
|
||||||
|
|
||||||
|
results = helper_test_http_method(client, 'post', url2, None, users)
|
||||||
|
assert results == [404, 404, 404]
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
from taiga.projects.history import services
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_object_with_one_webhook(settings):
|
||||||
|
settings.WEBHOOKS_ENABLED = True
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.WebhookFactory.create(project=project)
|
||||||
|
|
||||||
|
objects = [
|
||||||
|
f.IssueFactory.create(project=project),
|
||||||
|
f.TaskFactory.create(project=project),
|
||||||
|
f.UserStoryFactory.create(project=project),
|
||||||
|
f.WikiPageFactory.create(project=project)
|
||||||
|
]
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.create_webhook') as create_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test")
|
||||||
|
assert create_webhook_mock.call_count == 1
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner)
|
||||||
|
assert change_webhook_mock.call_count == 0
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test")
|
||||||
|
assert change_webhook_mock.call_count == 1
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
|
||||||
|
assert delete_webhook_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_object_with_two_webhook(settings):
|
||||||
|
settings.WEBHOOKS_ENABLED = True
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.WebhookFactory.create(project=project)
|
||||||
|
f.WebhookFactory.create(project=project)
|
||||||
|
|
||||||
|
objects = [
|
||||||
|
f.IssueFactory.create(project=project),
|
||||||
|
f.TaskFactory.create(project=project),
|
||||||
|
f.UserStoryFactory.create(project=project),
|
||||||
|
f.WikiPageFactory.create(project=project)
|
||||||
|
]
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.create_webhook') as create_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test")
|
||||||
|
assert create_webhook_mock.call_count == 2
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test")
|
||||||
|
assert change_webhook_mock.call_count == 2
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner)
|
||||||
|
assert change_webhook_mock.call_count == 0
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
|
||||||
|
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
|
||||||
|
assert delete_webhook_mock.call_count == 2
|
Loading…
Reference in New Issue