diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fb6116..c18e289e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ ## 2.1.0 ??? (unreleased) ### Features -- ... +- Webhooks: Improve webhook data: + - add permalinks + - owner, assigned_to, status, type, priority, severity, user_story, milestone, project are objects + - add role to 'points' object + - add the owner to every notification ('by' field) + - add the date of the notification ('date' field) + - show human diffs in 'changes' + - remove unnecessary data ### Misc - Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut))) diff --git a/taiga/base/utils/json.py b/taiga/base/utils/json.py index 5cb8f6b6..a5e0a477 100644 --- a/taiga/base/utils/json.py +++ b/taiga/base/utils/json.py @@ -22,8 +22,8 @@ from taiga.base.api.utils import encoders import json -def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder): - return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii) +def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder, indent=None): + return json.dumps(data, cls=encoder_class, ensure_ascii=ensure_ascii, indent=indent) def loads(data): diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index a9b8545e..426537dc 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils import timezone from django.utils.translation import ugettext as _ from taiga.base import filters @@ -45,7 +46,7 @@ class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet): self.check_permissions(request, 'test', webhook) self.pre_conditions_blocked(webhook) - webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key) + webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key, request.user, timezone.now()) log = serializers.WebhookLogSerializer(webhooklog) return response.Ok(log.data) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index a2714a05..a2553b69 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -20,21 +20,26 @@ from django.core.exceptions import ObjectDoesNotExist from taiga.base.api import serializers from taiga.base.fields import TagsField, PgArrayField, JsonField -from taiga.projects.userstories import models as us_models -from taiga.projects.tasks import models as task_models +from taiga.front.templatetags.functions import resolve as resolve_front_url + +from taiga.projects.history import models as history_models from taiga.projects.issues import models as issue_models from taiga.projects.milestones import models as milestone_models -from taiga.projects.wiki import models as wiki_models -from taiga.projects.history import models as history_models from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.services import get_logo_big_thumbnail_url +from taiga.projects.tasks import models as task_models +from taiga.projects.userstories import models as us_models +from taiga.projects.wiki import models as wiki_models + +from taiga.users.gravatar import get_gravatar_url +from taiga.users.services import get_photo_or_gravatar_url 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()} - +######################################################################## +## WebHooks +######################################################################## class WebhookSerializer(serializers.ModelSerializer): logs_counter = serializers.SerializerMethodField("get_logs_counter") @@ -55,16 +60,93 @@ class WebhookLogSerializer(serializers.ModelSerializer): model = WebhookLog +######################################################################## +## User +######################################################################## + class UserSerializer(serializers.Serializer): id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") + permalink = serializers.SerializerMethodField("get_permalink") + gravatar_url = serializers.SerializerMethodField("get_gravatar_url") + username = serializers.SerializerMethodField("get_username") + full_name = serializers.SerializerMethodField("get_full_name") + photo = serializers.SerializerMethodField("get_photo") def get_pk(self, obj): return obj.pk - def get_name(self, obj): - return obj.full_name + def get_permalink(self, obj): + return resolve_front_url("user", obj.username) + def get_gravatar_url(self, obj): + return get_gravatar_url(obj.email) + + def get_username(self, obj): + return obj.get_username + + def get_full_name(self, obj): + return obj.get_full_name() + + def get_photo(self, obj): + return get_photo_or_gravatar_url(obj) + +######################################################################## +## Project +######################################################################## + +class ProjectSerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + permalink = serializers.SerializerMethodField("get_permalink") + name = serializers.SerializerMethodField("get_name") + logo_big_url = serializers.SerializerMethodField("get_logo_big_url") + + def get_pk(self, obj): + return obj.pk + + def get_permalink(self, obj): + return resolve_front_url("project", obj.slug) + + def get_name(self, obj): + return obj.name + + def get_logo_big_url(self, obj): + return get_logo_big_thumbnail_url(obj) + + +######################################################################## +## History Serializer +######################################################################## + +class HistoryDiffField(serializers.Field): + def to_native(self, value): + # Tip: 'value' is the object returned by + # taiga.projects.history.models.HistoryEntry.values_diff() + + ret = {} + + for key, val in value.items(): + if key in ["attachments", "custom_attributes"]: + ret[key] = val + elif key == "points": + ret[key] = {k: {"from": v[0], "to": v[1]} for k, v in val.items()} + else: + ret[key] = {"from": val[0], "to": val[1]} + + return ret + + +class HistoryEntrySerializer(serializers.ModelSerializer): + diff = HistoryDiffField(source="values_diff") + + class Meta: + model = history_models.HistoryEntry + exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user", + "values", "created_at") + + +######################################################################## +## _Misc_ +######################################################################## class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") @@ -90,86 +172,251 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): except ObjectDoesNotExist: return None -class PointSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") + +class RolePointsSerializer(serializers.Serializer): + role = serializers.SerializerMethodField("get_role") name = serializers.SerializerMethodField("get_name") value = serializers.SerializerMethodField("get_value") + def get_role(self, obj): + return obj.role.name + + def get_name(self, obj): + return obj.points.name + + def get_value(self, obj): + return obj.points.value + + +class UserStoryStatusSerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + slug = serializers.SerializerMethodField("get_slug") + color = serializers.SerializerMethodField("get_color") + is_closed = serializers.SerializerMethodField("get_is_closed") + is_archived = serializers.SerializerMethodField("get_is_archived") + def get_pk(self, obj): return obj.pk def get_name(self, obj): return obj.name - def get_value(self, obj): - return obj.value + def get_slug(self, obj): + return obj.slug + + def get_color(self, obj): + return obj.color + + def get_is_closed(self, obj): + return obj.is_closed + + def get_is_archived(self, obj): + return obj.is_archived -class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsField(default=[], required=False) - external_reference = PgArrayField(required=False) - owner = UserSerializer() - assigned_to = UserSerializer() - points = PointSerializer(many=True) +class TaskStatusSerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + slug = serializers.SerializerMethodField("get_slug") + color = serializers.SerializerMethodField("get_color") + is_closed = serializers.SerializerMethodField("get_is_closed") - class Meta: - model = us_models.UserStory - exclude = ("backlog_order", "sprint_order", "kanban_order", "version") + def get_pk(self, obj): + return obj.pk - def custom_attributes_queryset(self, project): - return project.userstorycustomattributes.all() + def get_name(self, obj): + return obj.name + + def get_slug(self, obj): + return obj.slug + + def get_color(self, obj): + return obj.color + + def get_is_closed(self, obj): + return obj.is_closed -class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsField(default=[], required=False) - owner = UserSerializer() - assigned_to = UserSerializer() +class IssueStatusSerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + slug = serializers.SerializerMethodField("get_slug") + color = serializers.SerializerMethodField("get_color") + is_closed = serializers.SerializerMethodField("get_is_closed") - class Meta: - model = task_models.Task + def get_pk(self, obj): + return obj.pk - def custom_attributes_queryset(self, project): - return project.taskcustomattributes.all() + def get_name(self, obj): + return obj.name + + def get_slug(self, obj): + return obj.slug + + def get_color(self, obj): + return obj.color + + def get_is_closed(self, obj): + return obj.is_closed -class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsField(default=[], required=False) - owner = UserSerializer() - assigned_to = UserSerializer() +class IssueTypeSerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + color = serializers.SerializerMethodField("get_color") - class Meta: - model = issue_models.Issue + def get_pk(self, obj): + return obj.pk - def custom_attributes_queryset(self, project): - return project.issuecustomattributes.all() + def get_name(self, obj): + return obj.name + + def get_color(self, obj): + return obj.color -class WikiPageSerializer(serializers.ModelSerializer): - owner = UserSerializer() - last_modifier = UserSerializer() +class PrioritySerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + color = serializers.SerializerMethodField("get_color") - class Meta: - model = wiki_models.WikiPage - exclude = ("watchers", "version") + def get_pk(self, obj): + return obj.pk + def get_name(self, obj): + return obj.name + + def get_color(self, obj): + return obj.color + + +class SeveritySerializer(serializers.Serializer): + id = serializers.SerializerMethodField("get_pk") + name = serializers.SerializerMethodField("get_name") + color = serializers.SerializerMethodField("get_color") + + def get_pk(self, obj): + return obj.pk + + def get_name(self, obj): + return obj.name + + def get_color(self, obj): + return obj.color + + +######################################################################## +## Milestone +######################################################################## class MilestoneSerializer(serializers.ModelSerializer): + permalink = serializers.SerializerMethodField("get_permalink") + project = ProjectSerializer() owner = UserSerializer() class Meta: model = milestone_models.Milestone exclude = ("order", "watchers") + def get_permalink(self, obj): + return resolve_front_url("taskboard", obj.project.slug, obj.slug) -class HistoryEntrySerializer(serializers.ModelSerializer): - diff = HistoryDiffField() - snapshot = JsonField() - values = JsonField() - user = JsonField() - delete_comment_user = JsonField() + +######################################################################## +## User Story +######################################################################## + +class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): + permalink = serializers.SerializerMethodField("get_permalink") + tags = TagsField(default=[], required=False) + external_reference = PgArrayField(required=False) + project = ProjectSerializer() + owner = UserSerializer() + assigned_to = UserSerializer() + points = RolePointsSerializer(source="role_points", many=True) + status = UserStoryStatusSerializer() + milestone = MilestoneSerializer() class Meta: - model = history_models.HistoryEntry + model = us_models.UserStory + exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher") + + def get_permalink(self, obj): + return resolve_front_url("userstory", obj.project.slug, obj.ref) + + def custom_attributes_queryset(self, project): + return project.userstorycustomattributes.all() + + +######################################################################## +## Task +######################################################################## + +class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): + permalink = serializers.SerializerMethodField("get_permalink") + tags = TagsField(default=[], required=False) + project = ProjectSerializer() + owner = UserSerializer() + assigned_to = UserSerializer() + status = TaskStatusSerializer() + user_story = UserStorySerializer() + milestone = MilestoneSerializer() + + class Meta: + model = task_models.Task + exclude = ("version", "total_watchers", "is_watcher") + + def get_permalink(self, obj): + return resolve_front_url("task", obj.project.slug, obj.ref) + + def custom_attributes_queryset(self, project): + return project.taskcustomattributes.all() + + +######################################################################## +## Issue +######################################################################## + +class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): + permalink = serializers.SerializerMethodField("get_permalink") + tags = TagsField(default=[], required=False) + project = ProjectSerializer() + milestone = MilestoneSerializer() + owner = UserSerializer() + assigned_to = UserSerializer() + status = IssueStatusSerializer() + type = IssueTypeSerializer() + priority = PrioritySerializer() + severity = SeveritySerializer() + + class Meta: + model = issue_models.Issue + exclude = ("version", "total_watchers", "is_watcher") + + def get_permalink(self, obj): + return resolve_front_url("issue", obj.project.slug, obj.ref) + + def custom_attributes_queryset(self, project): + return project.issuecustomattributes.all() + + +######################################################################## +## Wiki Page +######################################################################## + +class WikiPageSerializer(serializers.ModelSerializer): + permalink = serializers.SerializerMethodField("get_permalink") + project = ProjectSerializer() + owner = UserSerializer() + last_modifier = UserSerializer() + + class Meta: + model = wiki_models.WikiPage + exclude = ("watchers", "total_watchers", "is_watcher", "version") + + def get_permalink(self, obj): + return resolve_front_url("wiki", obj.project.slug, obj.slug) diff --git a/taiga/webhooks/signal_handlers.py b/taiga/webhooks/signal_handlers.py index 42755553..415d4fcf 100644 --- a/taiga/webhooks/signal_handlers.py +++ b/taiga/webhooks/signal_handlers.py @@ -61,10 +61,13 @@ def on_new_history_entry(sender, instance, created, **kwargs): extra_args = [instance] elif instance.type == HistoryType.delete: task = tasks.delete_webhook - extra_args = [timezone.now()] + extra_args = [] + + by = instance.owner + date = timezone.now() for webhook in webhooks: - args = [webhook["id"], webhook["url"], webhook["key"], obj] + extra_args + args = [webhook["id"], webhook["url"], webhook["key"], by, date, obj] + extra_args if settings.CELERY_ENABLED: connection.on_commit(lambda: task.delay(*args)) diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index c9ca6b88..7427fe6e 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -26,7 +26,7 @@ from taiga.celery import app from .serializers import (UserStorySerializer, IssueSerializer, TaskSerializer, WikiPageSerializer, MilestoneSerializer, - HistoryEntrySerializer) + HistoryEntrySerializer, UserSerializer) from .models import WebhookLog @@ -67,58 +67,70 @@ def _send_request(webhook_id, url, key, data): request = requests.Request('POST', url, data=serialized_data, headers=headers) prepared_request = request.prepare() - session = requests.Session() - try: - response = session.send(prepared_request) - webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, - status=response.status_code, - request_data=data, - request_headers=dict(prepared_request.headers), - response_data=response.content, - response_headers=dict(response.headers), - duration=response.elapsed.total_seconds()) - except RequestException as e: - webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, status=0, - request_data=data, - request_headers=dict(prepared_request.headers), - response_data="error-in-request: {}".format(str(e)), - response_headers={}, - duration=0) - session.close() + with requests.Session() as session: + try: + response = session.send(prepared_request) + except RequestException as e: + # Error sending the webhook + webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, status=0, + request_data=data, + request_headers=dict(prepared_request.headers), + response_data="error-in-request: {}".format(str(e)), + response_headers={}, + duration=0) + else: + # Webhook was sent successfully + webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, + status=response.status_code, + request_data=data, + request_headers=dict(prepared_request.headers), + response_data=response.content, + response_headers=dict(response.headers), + duration=response.elapsed.total_seconds()) + finally: + # Only the last ten webhook logs traces are required + # so remove the leftover + ids = (WebhookLog.objects.filter(webhook_id=webhook_id) + .order_by("-id") + .values_list('id', flat=True)[10:]) + WebhookLog.objects.filter(id__in=ids).delete() - ids = [log.id for log in WebhookLog.objects.filter(webhook_id=webhook_id).order_by("-id")[10:]] - WebhookLog.objects.filter(id__in=ids).delete() return webhook_log @app.task -def change_webhook(webhook_id, url, key, obj, change): +def create_webhook(webhook_id, url, key, by, date, obj): data = {} - data['data'] = _serialize(obj) - data['action'] = "change" - data['type'] = _get_type(obj) - data['change'] = _serialize(change) - - return _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) + data['by'] = UserSerializer(by).data + data['date'] = date + data['data'] = _serialize(obj) return _send_request(webhook_id, url, key, data) @app.task -def delete_webhook(webhook_id, url, key, obj, deleted_date): +def delete_webhook(webhook_id, url, key, by, date, obj): data = {} - data['data'] = _serialize(obj) data['action'] = "delete" data['type'] = _get_type(obj) - data['deleted_date'] = deleted_date + data['by'] = UserSerializer(by).data + data['date'] = date + data['data'] = _serialize(obj) + + return _send_request(webhook_id, url, key, data) + + +@app.task +def change_webhook(webhook_id, url, key, by, date, obj, change): + data = {} + data['action'] = "change" + data['type'] = _get_type(obj) + data['by'] = UserSerializer(by).data + data['date'] = date + data['data'] = _serialize(obj) + data['change'] = _serialize(change) return _send_request(webhook_id, url, key, data) @@ -129,10 +141,12 @@ def resend_webhook(webhook_id, url, key, data): @app.task -def test_webhook(webhook_id, url, key): +def test_webhook(webhook_id, url, key, by, date): data = {} - data['data'] = {"test": "test"} data['action'] = "test" data['type'] = "test" + data['by'] = UserSerializer(by).data + data['date'] = date + data['data'] = {"test": "test"} return _send_request(webhook_id, url, key, data) diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py new file mode 100644 index 00000000..95f6a4a2 --- /dev/null +++ b/tests/integration/test_webhooks_issues.py @@ -0,0 +1,248 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch +from unittest.mock import Mock + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +from taiga.base.utils import json + +def test_webhooks_when_create_issue(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.IssueFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_issue(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.IssueFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.subject = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["subject"] == obj.subject + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + + +def test_webhooks_when_delete_issue(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.IssueFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_issue_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.IssueFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.IssueAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.IssueAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1 + + +def test_webhooks_when_update_issue_custom_attributes(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.IssueFactory.create(project=project) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=obj.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=obj.project) + ct2_id = "{}".format(custom_attr_2.id) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create custom attributes + obj.custom_attributes_values.attributes_values = { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + } + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 2 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Update custom attributes + obj.custom_attributes_values.attributes_values[ct1_id] = "test_2_updated" + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 1 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Delete custom attributes + del obj.custom_attributes_values.attributes_values[ct1_id] + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "issue" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 1 diff --git a/tests/integration/test_webhooks_milestones.py b/tests/integration/test_webhooks_milestones.py new file mode 100644 index 00000000..f720df2f --- /dev/null +++ b/tests/integration/test_webhooks_milestones.py @@ -0,0 +1,101 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch +from unittest.mock import Mock + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +from taiga.base.utils import json + +def test_webhooks_when_create_milestone(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.MilestoneFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "milestone" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_milestone(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.MilestoneFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.name = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "milestone" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["name"] == obj.name + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["name"]["to"] == data["data"]["name"] + assert data["change"]["diff"]["name"]["from"] != data["data"]["name"] + + +def test_webhooks_when_delete_milestone(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.MilestoneFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "milestone" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks_signals.py similarity index 59% rename from tests/integration/test_webhooks.py rename to tests/integration/test_webhooks_signals.py index 83ee4b28..cf9996ca 100644 --- a/tests/integration/test_webhooks.py +++ b/tests/integration/test_webhooks_signals.py @@ -18,6 +18,7 @@ import pytest from unittest.mock import patch +from unittest.mock import Mock from .. import factories as f @@ -26,7 +27,7 @@ from taiga.projects.history import services pytestmark = pytest.mark.django_db(transaction=True) -def test_new_object_with_one_webhook(settings): +def test_new_object_with_one_webhook_signal(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() f.WebhookFactory.create(project=project) @@ -38,28 +39,31 @@ def test_new_object_with_one_webhook(settings): 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 + response = Mock(status_code=200, headers={}, content="ok") + response.elapsed.total_seconds.return_value = 100 for obj in objects: - with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: + services.take_snapshot(obj, user=obj.owner, comment="test") + assert session_send_mock.call_count == 1 + + for obj in objects: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner) - assert change_webhook_mock.call_count == 0 + assert session_send_mock.call_count == 0 for obj in objects: - with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner, comment="test") - assert change_webhook_mock.call_count == 1 + assert session_send_mock.call_count == 1 for obj in objects: - with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) - assert delete_webhook_mock.call_count == 1 + assert session_send_mock.call_count == 1 -def test_new_object_with_two_webhook(settings): +def test_new_object_with_two_webhook_signals(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() f.WebhookFactory.create(project=project) @@ -72,28 +76,31 @@ def test_new_object_with_two_webhook(settings): 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 + response = Mock(status_code=200, headers={}, content="ok") + response.elapsed.total_seconds.return_value = 100 for obj in objects: - with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner, comment="test") - assert change_webhook_mock.call_count == 2 + assert session_send_mock.call_count == 2 for obj in objects: - with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: + services.take_snapshot(obj, user=obj.owner, comment="test") + assert session_send_mock.call_count == 2 + + for obj in objects: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner) - assert change_webhook_mock.call_count == 0 + assert session_send_mock.call_count == 0 for obj in objects: - with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) - assert delete_webhook_mock.call_count == 2 + assert session_send_mock.call_count == 2 -def test_send_request_one_webhook(settings): +def test_send_request_one_webhook_signal(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() f.WebhookFactory.create(project=project) @@ -105,12 +112,15 @@ def test_send_request_one_webhook(settings): f.WikiPageFactory.create(project=project) ] - for obj in objects: - with patch('taiga.webhooks.tasks._send_request') as _send_request_mock: - services.take_snapshot(obj, user=obj.owner, comment="test") - assert _send_request_mock.call_count == 1 + response = Mock(status_code=200, headers={}, content="ok") + response.elapsed.total_seconds.return_value = 100 for obj in objects: - with patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: + services.take_snapshot(obj, user=obj.owner, comment="test") + assert session_send_mock.call_count == 1 + + for obj in objects: + with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock: services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) - assert _send_request_mock.call_count == 1 + assert session_send_mock.call_count == 1 diff --git a/tests/integration/test_webhooks_tasks.py b/tests/integration/test_webhooks_tasks.py new file mode 100644 index 00000000..ba5eda8d --- /dev/null +++ b/tests/integration/test_webhooks_tasks.py @@ -0,0 +1,248 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch +from unittest.mock import Mock + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +from taiga.base.utils import json + +def test_webhooks_when_create_task(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.TaskFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_task(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.TaskFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.subject = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["subject"] == obj.subject + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + + +def test_webhooks_when_delete_task(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.TaskFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_task_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.TaskFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.TaskAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.TaskAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1 + + +def test_webhooks_when_update_task_custom_attributes(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.TaskFactory.create(project=project) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=obj.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=obj.project) + ct2_id = "{}".format(custom_attr_2.id) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create custom attributes + obj.custom_attributes_values.attributes_values = { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + } + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 2 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Update custom attributes + obj.custom_attributes_values.attributes_values[ct1_id] = "test_2_updated" + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 1 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Delete custom attributes + del obj.custom_attributes_values.attributes_values[ct1_id] + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "task" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 1 diff --git a/tests/integration/test_webhooks_userstories.py b/tests/integration/test_webhooks_userstories.py new file mode 100644 index 00000000..716697ce --- /dev/null +++ b/tests/integration/test_webhooks_userstories.py @@ -0,0 +1,308 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch +from unittest.mock import Mock + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +from taiga.base.utils import json + +def test_webhooks_when_create_user_story(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.UserStoryFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_user_story(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.UserStoryFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.subject = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["subject"] == obj.subject + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + + +def test_webhooks_when_delete_user_story(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.UserStoryFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_user_story_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.UserStoryFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.UserStoryAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.UserStoryAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1 + + +def test_webhooks_when_update_user_story_custom_attributes(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.UserStoryFactory.create(project=project) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=obj.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=obj.project) + ct2_id = "{}".format(custom_attr_2.id) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create custom attributes + obj.custom_attributes_values.attributes_values = { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + } + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 2 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Update custom attributes + obj.custom_attributes_values.attributes_values[ct1_id] = "test_2_updated" + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 1 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Delete custom attributes + del obj.custom_attributes_values.attributes_values[ct1_id] + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 1 + + +def test_webhooks_when_update_user_story_points(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + role1 = f.RoleFactory.create(project=project) + role2 = f.RoleFactory.create(project=project) + + points1 = f.PointsFactory.create(project=project, value=None) + points2 = f.PointsFactory.create(project=project, value=1) + points3 = f.PointsFactory.create(project=project, value=2) + + obj = f.UserStoryFactory.create(project=project) + obj.role_points.all().delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Set points + f.RolePointsFactory.create(user_story=obj, role=role1, points=points1) + f.RolePointsFactory.create(user_story=obj, role=role2, points=points2) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "" + assert data["change"]["diff"]["points"][role1.name]["from"] == None + assert data["change"]["diff"]["points"][role1.name]["to"] == points1.name + assert data["change"]["diff"]["points"][role2.name]["from"] == None + assert data["change"]["diff"]["points"][role2.name]["to"] == points2.name + + # Change points + obj.role_points.all().update(points=points3) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "userstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "" + assert data["change"]["diff"]["points"][role1.name]["from"] == points1.name + assert data["change"]["diff"]["points"][role1.name]["to"] == points3.name + assert data["change"]["diff"]["points"][role2.name]["from"] == points2.name + assert data["change"]["diff"]["points"][role2.name]["to"] == points3.name diff --git a/tests/integration/test_webhooks_wikipages.py b/tests/integration/test_webhooks_wikipages.py new file mode 100644 index 00000000..5d10f233 --- /dev/null +++ b/tests/integration/test_webhooks_wikipages.py @@ -0,0 +1,170 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch +from unittest.mock import Mock + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +from taiga.base.utils import json + +def test_webhooks_when_create_wiki_page(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.WikiPageFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_wiki_page(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.WikiPageFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.content = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["content"] == obj.content + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["content_html"]["from"] != data["change"]["diff"]["content_html"]["to"] + assert obj.content in data["change"]["diff"]["content_html"]["to"] + + +def test_webhooks_when_delete_wiki_page(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.WikiPageFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_wiki_page_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.WikiPageFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.WikiAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.WikiAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "wikipage" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1