Tests for Webhook sender (#5017)

* tests/rest_api/assets: update webhooks in testdb

* tests/rest_api/test_webhook_sender: add tests for project and ping events

* tests/rest_api/test_webhook_sender: add tests for webhook intersection case

* tests/rest_api/test_webhook_sender: add tests for task update events

* tests/rest_api/test_webhook_sender: add tests for task create and job update events

* tests/rest_api/test_webhook_sender: add tests for issue events

* tests/rest_api/test_webhook_sender: add tests for membership events

* tests/rest_api/test_webhook_sender: add tests for organization event

* Fix Pylint warnings

* apps/engine: get rid of sending `create` signals in views

* apps/organizations: get rid of sending `create` signals in views

* tests/rest_api/test_webhooks: fix typo

* apps/engine: get rid of sending  signals in task view

* tests/rest_api: remove debug prints

* apps/engine: fix pylint errors

* tests/rest_api/test_webhooks_sender: add tests for `redelivery` method

* tests: define tag for minio image

* apps/webhooks: remove owner_id from write serializer

* tests/rest_api/test_webhooks: fix code style

* tests/rest_api: added tests for webhook comment events

* tests/rest_api: fix typo

Co-authored-by: Anastasia Yasakova <yasakova_anastasiya@mail.ru>

* tests: fix warnings from black

Co-authored-by: Anastasia Yasakova <yasakova_anastasiya@mail.ru>
main
Kirill Sizov 3 years ago committed by GitHub
parent 7cc05e80de
commit bcb3f9bda8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -325,8 +325,8 @@ class SerializeMixin:
class CreateModelMixin(mixins.CreateModelMixin):
def perform_create(self, serializer):
super().perform_create(serializer)
def perform_create(self, serializer, **kwargs):
serializer.save(**kwargs)
signal_create.send(self, instance=serializer.instance)
class PartialUpdateModelMixin:
@ -337,8 +337,10 @@ class PartialUpdateModelMixin:
"""
def perform_update(self, serializer):
instance = serializer.instance
data = serializer.to_representation(instance)
old_values = {
attr: serializer.to_representation(serializer.instance).get(attr, None)
attr: data[attr] if attr in data else getattr(instance, attr, None)
for attr in self.request.data.keys()
}

@ -41,7 +41,6 @@ from rest_framework.response import Response
from rest_framework.exceptions import PermissionDenied
from django_sendfile import sendfile
from cvat.apps.webhooks.signals import signal_create, signal_update
import cvat.apps.dataset_manager as dm
import cvat.apps.dataset_manager.views # pylint: disable=unused-import
from cvat.apps.engine.cloud_provider import (
@ -73,7 +72,7 @@ from cvat.apps.engine.serializers import (
from utils.dataset_manifest import ImageManifestManager
from cvat.apps.engine.utils import av_scan_paths, process_failed_job, configure_dependent_job
from cvat.apps.engine import backup
from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin, DestroyModelMixin
from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin, DestroyModelMixin, CreateModelMixin
from cvat.apps.engine.location import get_location_configuration, StorageType
from . import models, task
@ -273,7 +272,7 @@ class ServerViewSet(viewsets.ViewSet):
})
)
class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin,
mixins.RetrieveModelMixin, CreateModelMixin, DestroyModelMixin,
PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin
):
queryset = models.Project.objects.prefetch_related(Prefetch('label_set',
@ -308,9 +307,11 @@ class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return queryset
def perform_create(self, serializer):
serializer.save(owner=self.request.user,
organization=self.request.iam_context['organization'])
signal_create.send(self, instance=serializer.instance)
super().perform_create(
serializer,
owner=self.request.user,
organization=self.request.iam_context['organization']
)
@extend_schema(
summary='Method returns information of the tasks of the project with the selected id',
@ -714,7 +715,7 @@ class DataChunkGetter:
})
)
class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin,
mixins.RetrieveModelMixin, CreateModelMixin, DestroyModelMixin,
PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin
):
queryset = Task.objects.prefetch_related(
@ -803,32 +804,25 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
def perform_update(self, serializer):
instance = serializer.instance
old_values = {}
old_repr = serializer.to_representation(instance)
for attr in self.request.data.keys():
old_values[attr] = old_repr[attr] if attr in old_repr \
else getattr(instance, attr, None)
super().perform_update(serializer)
updated_instance = serializer.save()
updated_instance = serializer.instance
if instance.project:
instance.project.save()
if updated_instance.project:
updated_instance.project.save()
if getattr(instance, '_prefetched_objects_cache', None):
instance._prefetched_objects_cache = {}
signal_update.send(self, instance=serializer.instance, old_values=old_values)
def perform_create(self, serializer):
instance = serializer.save(owner=self.request.user,
organization=self.request.iam_context['organization'])
if instance.project:
db_project = instance.project
super().perform_create(
serializer,
owner=self.request.user,
organization=self.request.iam_context['organization']
)
if serializer.instance.project:
db_project = serializer.instance.project
db_project.save()
assert instance.organization == db_project.organization
signal_create.send(self, instance=serializer.instance)
assert serializer.instance.organization == db_project.organization
def perform_destroy(self, instance):
task_dirname = instance.get_dirname()
@ -1708,7 +1702,7 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
})
)
class IssueViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin,
mixins.RetrieveModelMixin, CreateModelMixin, DestroyModelMixin,
PartialUpdateModelMixin
):
queryset = Issue.objects.all().order_by('-id')
@ -1739,8 +1733,7 @@ class IssueViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return IssueWriteSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
signal_create.send(self, instance=serializer.instance)
super().perform_create(serializer, owner=self.request.user)
@extend_schema(summary='The action returns all comments of a specific issue',
responses=CommentReadSerializer(many=True)) # Duplicate to still get 'list' op. name
@ -1789,7 +1782,7 @@ class IssueViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
})
)
class CommentViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.CreateModelMixin, DestroyModelMixin,
mixins.RetrieveModelMixin, CreateModelMixin, DestroyModelMixin,
PartialUpdateModelMixin
):
queryset = Comment.objects.all().order_by('-id')
@ -1815,8 +1808,7 @@ class CommentViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return CommentWriteSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
signal_create.send(self, instance=serializer.instance)
super().perform_create(serializer, owner=self.request.user)
@extend_schema(tags=['users'])
@extend_schema_view(

@ -8,8 +8,7 @@ from rest_framework.permissions import SAFE_METHODS
from django.utils.crypto import get_random_string
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
from cvat.apps.engine.mixins import PartialUpdateModelMixin, DestroyModelMixin
from cvat.apps.webhooks.signals import signal_create
from cvat.apps.engine.mixins import PartialUpdateModelMixin, DestroyModelMixin, CreateModelMixin
from cvat.apps.iam.permissions import (
InvitationPermission, MembershipPermission, OrganizationPermission)
@ -177,8 +176,8 @@ class MembershipViewSet(mixins.RetrieveModelMixin, DestroyModelMixin,
class InvitationViewSet(viewsets.GenericViewSet,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
CreateModelMixin,
DestroyModelMixin,
):
queryset = Invitation.objects.all()
@ -208,8 +207,7 @@ class InvitationViewSet(viewsets.GenericViewSet,
'key': get_random_string(length=64),
'organization': self.request.iam_context['organization']
}
serializer.save(**extra_kwargs)
signal_create.send(self, instance=serializer.instance)
super().perform_create(serializer, **extra_kwargs)
def perform_update(self, serializer):
if 'accepted' in self.request.query_params:

@ -54,7 +54,6 @@ class Webhook(models.Model):
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
# questionable: should we keep webhook if owner has been deleted?
owner = models.ForeignKey(
User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+"
)

@ -97,11 +97,6 @@ class WebhookReadSerializer(serializers.ModelSerializer):
class WebhookWriteSerializer(WriteOnceMixin, serializers.ModelSerializer):
events = EventTypesSerializer(write_only=True)
# Q: should be owner_id required or not?
owner_id = serializers.IntegerField(
write_only=True, allow_null=True, required=False
)
project_id = serializers.IntegerField(
write_only=True, allow_null=True, required=False
)
@ -120,11 +115,10 @@ class WebhookWriteSerializer(WriteOnceMixin, serializers.ModelSerializer):
"secret",
"is_active",
"enable_ssl",
"owner_id",
"project_id",
"events",
)
write_once_fields = ("type", "owner_id", "project_id")
write_once_fields = ("type", "project_id")
validators = [EventTypeValidator()]
def create(self, validated_data):

@ -2,7 +2,7 @@ version: '3.3'
services:
minio:
image: quay.io/minio/minio
image: quay.io/minio/minio:RELEASE.2022-09-17T00-09-45Z
hostname: minio
restart: always
command: server /data --console-address ":9001"
@ -25,7 +25,7 @@ services:
aliases:
- minio
mc:
image: minio/mc
image: minio/mc:RELEASE.2022-09-16T09-16-47Z
depends_on:
- minio
environment:

@ -305,7 +305,7 @@ class TestPostWebhooks:
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
def test_cannot_create_non_unique_webhook(self):
pytest.skip("Not implemeted yet")
pytest.skip("Not implemented yet")
response = post_method("admin2", "webhooks", self.proj_webhook)
response = post_method("admin2", "webhooks", self.proj_webhook)
@ -769,9 +769,7 @@ class TestPatchWebhooks:
)
def test_cannot_update_with_nonexistent_contenttype(self):
patch_data = {
"content_type": "application/x-www-form-urlencoded",
}
patch_data = {"content_type": "application/x-www-form-urlencoded"}
response = patch_method("admin2", f"webhooks/{self.WID}", patch_data)
assert response.status_code == HTTPStatus.BAD_REQUEST

@ -10,11 +10,11 @@ import pytest
from deepdiff import DeepDiff
from shared.fixtures.init import CVAT_ROOT_DIR, _run
from shared.utils.config import get_method, patch_method, post_method
from shared.utils.config import delete_method, get_method, patch_method, post_method
# Testing webhook functionality:
# - webhook_receiver container receive post request and return responses with the same body
# - cvat save response body for each delivery
# - CVAT save response body for each delivery
#
# So idea of this testing system is quite simple:
# 1) trigger some webhook
@ -49,39 +49,50 @@ def webhook_spec(events, project_id=None, webhook_type="organization"):
}
def create_webhook(events, webhook_type, project_id=None, org_id=""):
assert (webhook_type == "project" and project_id is not None) or (
webhook_type == "organization" and org_id
)
response = post_method(
"admin1", "webhooks", webhook_spec(events, project_id, webhook_type), org_id=org_id
)
assert response.status_code == HTTPStatus.CREATED
return response.json()
def get_deliveries(webhook_id):
response = get_method("admin1", f"webhooks/{webhook_id}/deliveries")
assert response.status_code == HTTPStatus.OK
deliveries = response.json()
last_payload = json.loads(deliveries["results"][0]["response"])
return deliveries, last_payload
@pytest.mark.usefixtures("changedb")
class TestWebhookProjectEvents:
def test_webhook_project_update(self):
events = ["update:project"]
patch_data = {"name": "new_project_name"}
# create project
def test_webhook_update_project_name(self):
response = post_method("admin1", "projects", {"name": "project"})
assert response.status_code == HTTPStatus.CREATED
project = response.json()
# create webhook
response = post_method(
"admin1", "webhooks", webhook_spec(events, project["id"], webhook_type="project")
)
assert response.status_code == HTTPStatus.CREATED
webhook = response.json()
events = ["update:project"]
webhook = create_webhook(events, "project", project_id=project["id"])
# update project
patch_data = {"name": "new_project_name"}
response = patch_method("admin1", f"projects/{project['id']}", patch_data)
assert response.status_code == HTTPStatus.OK
# get list of deliveries of webhook
response = get_method("admin1", f"webhooks/{webhook['id']}/deliveries")
assert response.status_code == HTTPStatus.OK
response_data = response.json()
deliveries, payload = get_deliveries(webhook["id"])
# check that we sent only one webhook
assert response_data["count"] == 1
assert deliveries["count"] == 1
# check value of payload that CVAT sent
payload = json.loads(response_data["results"][0]["response"])
assert payload["event"] == events[0]
assert payload["sender"]["username"] == "admin1"
assert payload["before_update"]["name"] == project["name"]
@ -96,3 +107,651 @@ class TestWebhookProjectEvents:
)
== {}
)
def test_webhook_update_project_labels(self):
response = post_method("admin1", "projects", {"name": "project"})
assert response.status_code == HTTPStatus.CREATED
project = response.json()
events = ["update:project"]
webhook = create_webhook(events, "project", project["id"])
patch_data = {"labels": [{"name": "label_0", "color": "#aabbcc"}]}
response = patch_method("admin1", f"projects/{project['id']}", patch_data)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 1
assert payload["event"] == events[0]
assert len(payload["before_update"]["labels"]) == 0
assert len(payload["project"]["labels"]) == 1
assert payload["project"]["labels"][0]["name"] == patch_data["labels"][0]["name"]
assert payload["project"]["labels"][0]["color"] == patch_data["labels"][0]["color"]
def test_webhook_create_and_delete_project(self, organizations):
org_id = list(organizations)[0]["id"]
events = ["create:project", "delete:project"]
webhook = create_webhook(events, "organization", org_id=org_id)
response = post_method("admin1", "projects", {"name": "project_name"}, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
project = response.json()
deliveries, create_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 1
response = delete_method("admin1", f"projects/{project['id']}", org_id=org_id)
assert response.status_code == HTTPStatus.NO_CONTENT
deliveries, delete_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 2
assert create_payload["event"] == "create:project"
assert delete_payload["event"] == "delete:project"
assert (
DeepDiff(
create_payload["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
assert (
DeepDiff(
delete_payload["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookIntersection:
# Test case description:
# few webhooks are triggered by the same event
# In this case we need to check that CVAT will sent
# the right number of payloads to the target url
def test_project_and_organization_webhooks_intersection(self, organizations):
org_id = list(organizations)[0]["id"]
post_data = {"name": "project_name"}
response = post_method("admin1", "projects", post_data, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
events = ["update:project"]
project_id = response.json()["id"]
webhook_id_1 = create_webhook(events, "organization", org_id=org_id)["id"]
webhook_id_2 = create_webhook(events, "project", project_id=project_id, org_id=org_id)["id"]
patch_data = {"name": "new_project_name"}
response = patch_method("admin1", f"projects/{project_id}", patch_data)
assert response.status_code == HTTPStatus.OK
deliveries_1, payload_1 = get_deliveries(webhook_id_1)
deliveries_2, payload_2 = get_deliveries(webhook_id_2)
assert deliveries_1["count"] == deliveries_2["count"] == 1
assert payload_1["project"]["name"] == payload_2["project"]["name"] == patch_data["name"]
assert (
payload_1["before_update"]["name"]
== payload_2["before_update"]["name"]
== post_data["name"]
)
assert payload_1["webhook_id"] == webhook_id_1
assert payload_2["webhook_id"] == webhook_id_2
assert deliveries_1["results"][0]["webhook_id"] == webhook_id_1
assert deliveries_2["results"][0]["webhook_id"] == webhook_id_2
def test_two_project_webhooks_intersection(self):
post_data = {"name": "project_name"}
response = post_method("admin1", "projects", post_data)
assert response.status_code == HTTPStatus.CREATED
project_id = response.json()["id"]
events_1 = ["create:task", "update:project"]
events_2 = ["create:task", "create:issue"]
webhook_id_1 = create_webhook(events_1, "project", project_id=project_id)["id"]
webhook_id_2 = create_webhook(events_2, "project", project_id=project_id)["id"]
post_data = {"name": "project_name", "project_id": project_id}
response = post_method("admin1", "tasks", post_data)
assert response.status_code == HTTPStatus.CREATED
deliveries_1, payload_1 = get_deliveries(webhook_id_1)
deliveries_2, payload_2 = get_deliveries(webhook_id_2)
assert deliveries_1["count"] == deliveries_2["count"] == 1
assert payload_1["event"] == payload_2["event"] == "create:task"
assert payload_1["task"]["name"] == payload_2["task"]["name"] == post_data["name"]
assert payload_1["webhook_id"] == webhook_id_1
assert payload_2["webhook_id"] == webhook_id_2
def test_two_organization_webhook_intersection(self, organizations):
org_id = list(organizations)[0]["id"]
events_1 = ["create:project", "update:membership"]
events_2 = ["create:project", "update:job"]
webhook_id_1 = create_webhook(events_1, "organization", org_id=org_id)["id"]
webhook_id_2 = create_webhook(events_2, "organization", org_id=org_id)["id"]
post_data = {"name": "project_name"}
response = post_method("admin1", "projects", post_data, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
project = response.json()
deliveries_1, payload_1 = get_deliveries(webhook_id_1)
deliveries_2, payload_2 = get_deliveries(webhook_id_2)
assert deliveries_1["count"] == deliveries_2["count"] == 1
assert payload_1["webhook_id"] == webhook_id_1
assert payload_2["webhook_id"] == webhook_id_2
assert (
DeepDiff(
payload_1["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
assert (
DeepDiff(
payload_2["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookTaskEvents:
def test_webhook_update_task_assignee(self, users, tasks):
task_id, project_id = next(
(
(task["id"], task["project_id"])
for task in tasks
if task["project_id"] is not None
and task["organization"] is None
and task["assignee"] is not None
)
)
assignee_id = next(
(user["id"] for user in users if user["id"] != tasks[task_id]["assignee"]["id"])
)
webhook_id = create_webhook(["update:task"], "project", project_id=project_id)["id"]
patch_data = {"assignee_id": assignee_id}
response = patch_method("admin1", f"tasks/{task_id}", patch_data)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id=webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["assignee_id"] == tasks[task_id]["assignee"]["id"]
assert payload["task"]["assignee"]["id"] == assignee_id
def test_webhook_update_task_label(self, tasks):
task_id, org_id = next(
(
(task["id"], task["organization"])
for task in tasks
if task["project_id"] is None and task["organization"] is not None
)
)
webhook_id = create_webhook(["update:task"], "organization", org_id=org_id)["id"]
patch_data = {"labels": [{"name": "new_label"}]}
response = patch_method("admin1", f"tasks/{task_id}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id=webhook_id)
assert deliveries["count"] == 1
assert (
len(payload["before_update"]["labels"])
== len(tasks[task_id]["labels"])
== len(payload["task"]["labels"]) - 1
)
def test_webhook_create_and_delete_task(self, organizations):
org_id = list(organizations)[0]["id"]
events = ["create:task", "delete:task"]
webhook = create_webhook(events, "organization", org_id=org_id)
post_data = {"name": "task_name", "labels": [{"name": "label_0"}]}
response = post_method("admin1", "tasks", post_data, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
task = response.json()
deliveries, create_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 1
response = delete_method("admin1", f"tasks/{task['id']}", org_id=org_id)
assert response.status_code == HTTPStatus.NO_CONTENT
deliveries, delete_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 2
assert create_payload["event"] == "create:task"
assert delete_payload["event"] == "delete:task"
assert (
DeepDiff(
create_payload["task"],
task,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
assert (
DeepDiff(
delete_payload["task"],
task,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookJobEvents:
def test_webhook_update_job_assignee(self, jobs, tasks, users):
job = next(
(
job
for job in jobs
if job["assignee"] is None and tasks[job["task_id"]]["organization"] is not None
)
)
org_id = tasks[job["task_id"]]["organization"]
webhook_id = create_webhook(["update:job"], "organization", org_id=org_id)["id"]
patch_data = {"assignee": list(users)[0]["id"]}
response = patch_method("admin1", f"jobs/{job['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["assignee"] is None
assert payload["job"]["assignee"]["id"] == patch_data["assignee"]
def test_webhook_update_job_stage(self, jobs, tasks):
stages = {"annotation", "validation", "acceptance"}
job = next((job for job in jobs if tasks[job["task_id"]]["organization"] is not None))
org_id = tasks[job["task_id"]]["organization"]
webhook_id = create_webhook(["update:job"], "organization", org_id=org_id)["id"]
patch_data = {"stage": (stages - {job["stage"]}).pop()}
response = patch_method("admin1", f"jobs/{job['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["stage"] == job["stage"]
assert payload["job"]["stage"] == patch_data["stage"]
def test_webhook_update_job_state(self, jobs, tasks):
states = {"new", "in progress", "rejected", "completed"}
job = next(
(
job
for job in jobs
if tasks[job["task_id"]]["organization"] is not None
and job["state"] == "in progress"
)
)
org_id = tasks[job["task_id"]]["organization"]
webhook_id = create_webhook(["update:job"], "organization", org_id=org_id)["id"]
patch_data = {"state": (states - {job["state"]}).pop()}
response = patch_method("admin1", f"jobs/{job['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["state"] == job["state"]
assert payload["job"]["state"] == patch_data["state"]
@pytest.mark.usefixtures("changedb")
class TestWebhookIssueEvents:
def test_webhook_update_issue_resolved(self, issues, jobs, tasks):
issue = next(
(
issue
for issue in issues
if tasks[jobs[issue["job"]]["task_id"]]["organization"] is not None
)
)
org_id = tasks[jobs[issue["job"]]["task_id"]]["organization"]
webhook_id = create_webhook(["update:issue"], "organization", org_id=org_id)["id"]
patch_data = {"resolved": not issue["resolved"]}
response = patch_method("admin1", f"issues/{issue['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["resolved"] == issue["resolved"]
assert payload["issue"]["resolved"] == patch_data["resolved"]
def test_webhook_update_issue_position(self, issues, jobs, tasks):
issue = next(
(
issue
for issue in issues
if tasks[jobs[issue["job"]]["task_id"]]["organization"] is not None
)
)
org_id = tasks[jobs[issue["job"]]["task_id"]]["organization"]
webhook_id = create_webhook(["update:issue"], "organization", org_id=org_id)["id"]
patch_data = {"position": [0, 1, 2, 3]}
response = patch_method("admin1", f"issues/{issue['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["position"] == issue["position"]
assert payload["issue"]["position"] == patch_data["position"]
def test_webhook_create_and_delete_issue(self, organizations, jobs, tasks):
org_id = list(organizations)[0]["id"]
job_id = next(
(job["id"] for job in jobs if tasks[job["task_id"]]["organization"] == org_id)
)
events = ["create:issue", "delete:issue"]
webhook = create_webhook(events, "organization", org_id=org_id)
post_data = {"frame": 0, "position": [0, 1, 2, 3], "job": job_id, "message": "issue_msg"}
response = post_method("admin1", "issues", post_data, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
issue = response.json()
deliveries, create_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 1
response = delete_method("admin1", f"issues/{issue['id']}", org_id=org_id)
assert response.status_code == HTTPStatus.NO_CONTENT
deliveries, delete_payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 2
assert create_payload["event"] == "create:issue"
assert delete_payload["event"] == "delete:issue"
assert (
DeepDiff(
create_payload["issue"],
issue,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
assert (
DeepDiff(
delete_payload["issue"],
issue,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookMembershipEvents:
def test_webhook_update_membership_role(self, memberships):
roles = {"worker", "supervisor", "maintainer"}
membership = next(
(membership for membership in memberships if membership["role"] != "owner")
)
org_id = membership["organization"]
webhook_id = create_webhook(["update:membership"], "organization", org_id=org_id)["id"]
patch_data = {"role": (roles - {membership["role"]}).pop()}
response = patch_method(
"admin1", f"memberships/{membership['id']}", patch_data, org_id=org_id
)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["role"] == membership["role"]
assert payload["membership"]["role"] == patch_data["role"]
def test_webhook_delete_membership(self, memberships):
membership = next(
(membership for membership in memberships if membership["role"] != "owner")
)
org_id = membership["organization"]
webhook_id = create_webhook(["delete:membership"], "organization", org_id=org_id)["id"]
response = delete_method("admin1", f"memberships/{membership['id']}", org_id=org_id)
assert response.status_code == HTTPStatus.NO_CONTENT
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert (
DeepDiff(
payload["membership"],
membership,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookOrganizationEvents:
def test_webhook_update_organization_name(self, organizations):
org_id = list(organizations)[0]["id"]
webhook_id = create_webhook(["update:organization"], "organization", org_id=org_id)["id"]
patch_data = {"name": "new_org_name"}
patch_method("admin1", f"organizations/{org_id}", patch_data, org_id=org_id)
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["name"] == organizations[org_id]["name"]
assert payload["organization"]["name"] == patch_data["name"]
@pytest.mark.usefixtures("changedb")
class TestWebhookCommentEvents:
def test_webhook_update_comment_message(self, comments, issues, jobs, tasks):
org_comments = list(
(comment, tasks[jobs[issues[comment["issue"]]["job"]]["task_id"]]["organization"])
for comment in comments
)
comment, org_id = next(
((comment, org_id) for comment, org_id in org_comments if org_id is not None)
)
webhook_id = create_webhook(["update:comment"], "organization", org_id=org_id)["id"]
patch_data = {"message": "new comment message"}
response = patch_method("admin1", f"comments/{comment['id']}", patch_data, org_id=org_id)
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook_id)
assert deliveries["count"] == 1
assert payload["before_update"]["message"] == comment["message"]
comment.update(patch_data)
assert (
DeepDiff(
payload["comment"],
comment,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
def test_webhook_create_and_delete_comment(self, issues, jobs, tasks):
issue = next(
(
issue
for issue in issues
if tasks[jobs[issue["job"]]["task_id"]]["organization"] is not None
)
)
org_id = tasks[jobs[issue["job"]]["task_id"]]["organization"]
events = ["create:comment", "delete:comment"]
webhook_id = create_webhook(events, "organization", org_id=org_id)["id"]
post_data = {"issue": issue["id"], "message": "new comment message"}
response = post_method("admin1", f"comments", post_data, org_id=org_id)
assert response.status_code == HTTPStatus.CREATED
create_deliveries, create_payload = get_deliveries(webhook_id)
comment_id = response.json()["id"]
response = delete_method("admin1", f"comments/{comment_id}", org_id=org_id)
assert response.status_code == HTTPStatus.NO_CONTENT
delete_deliveries, delete_payload = get_deliveries(webhook_id)
assert create_deliveries["count"] == 1
assert delete_deliveries["count"] == 2
assert create_payload["event"] == "create:comment"
assert delete_payload["event"] == "delete:comment"
assert (
create_payload["comment"]["message"]
== delete_payload["comment"]["message"]
== post_data["message"]
)
@pytest.mark.usefixtures("changedb")
class TestWebhookPing:
def test_ping_webhook(self, projects):
project_id = list(projects)[0]["id"]
webhook = create_webhook(["create:task"], "project", project_id=project_id)
response = post_method("admin1", f"webhooks/{webhook['id']}/ping", {})
assert response.status_code == HTTPStatus.OK
deliveries, payload = get_deliveries(webhook["id"])
assert deliveries["count"] == 1
assert (
DeepDiff(
payload["webhook"],
webhook,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
@pytest.mark.usefixtures("changedb")
class TestWebhookRedelivery:
def test_webhook_redelivery(self, projects):
project = list(projects)[0]
webhook_id = create_webhook(["update:project"], "project", project_id=project["id"])["id"]
patch_data = {"name": "new_project_name"}
response = patch_method("admin1", f"projects/{project['id']}", patch_data)
assert response.status_code == HTTPStatus.OK
deliveries_1, payload_1 = get_deliveries(webhook_id)
delivery_id = deliveries_1["results"][0]["id"]
response = post_method(
"admin1", f"webhooks/{webhook_id}/deliveries/{delivery_id}/redelivery", {}
)
assert response.status_code == HTTPStatus.OK
deliveries_2, payload_2 = get_deliveries(webhook_id)
assert deliveries_1["count"] == 1
assert deliveries_2["count"] == 2
assert deliveries_1["results"][0]["redelivery"] is False
assert deliveries_2["results"][0]["redelivery"] is True
project.update(patch_data)
assert (
DeepDiff(
payload_1["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)
assert (
DeepDiff(
payload_2["project"],
project,
ignore_order=True,
exclude_paths=["root['updated_date']"],
)
== {}
)

@ -0,0 +1,91 @@
{
"count": 6,
"next": null,
"previous": null,
"results": [
{
"created_date": "2022-03-16T12:49:29.372000Z",
"id": 6,
"issue": 5,
"message": "Wrong position",
"owner": {
"first_name": "User",
"id": 20,
"last_name": "Sixth",
"url": "http://localhost:8080/api/users/20",
"username": "user6"
},
"updated_date": "2022-03-16T12:49:29.372000Z"
},
{
"created_date": "2022-03-16T12:40:00.767000Z",
"id": 5,
"issue": 4,
"message": "Issue with empty frame",
"owner": {
"first_name": "User",
"id": 2,
"last_name": "First",
"url": "http://localhost:8080/api/users/2",
"username": "user1"
},
"updated_date": "2022-03-16T12:40:00.767000Z"
},
{
"created_date": "2022-03-16T11:08:18.370000Z",
"id": 4,
"issue": 3,
"message": "Another one issue",
"owner": {
"first_name": "Business",
"id": 11,
"last_name": "Second",
"url": "http://localhost:8080/api/users/11",
"username": "business2"
},
"updated_date": "2022-03-16T11:08:18.370000Z"
},
{
"created_date": "2022-03-16T11:07:22.173000Z",
"id": 3,
"issue": 2,
"message": "Something should be here",
"owner": {
"first_name": "Business",
"id": 11,
"last_name": "Second",
"url": "http://localhost:8080/api/users/11",
"username": "business2"
},
"updated_date": "2022-03-16T11:07:22.173000Z"
},
{
"created_date": "2022-03-16T11:04:49.821000Z",
"id": 2,
"issue": 1,
"message": "Just to suffer?",
"owner": {
"first_name": "User",
"id": 2,
"last_name": "First",
"url": "http://localhost:8080/api/users/2",
"username": "user1"
},
"updated_date": "2022-03-16T11:04:49.821000Z"
},
{
"created_date": "2022-03-16T11:04:39.447000Z",
"id": 1,
"issue": 1,
"message": "Why are we still here?",
"owner": {
"first_name": "User",
"id": 2,
"last_name": "First",
"url": "http://localhost:8080/api/users/2",
"username": "user1"
},
"updated_date": "2022-03-16T11:04:39.447000Z"
}
]
}

@ -36,7 +36,7 @@
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$260000$DevmxlmLwciP1P6sZs2Qag$U9DFtjTWx96Sk95qY6UXVcvpdQEP2LcoFBftk5D2RKY=",
"last_login": "2022-09-28T12:20:48.633Z",
"last_login": "2022-09-29T08:00:09.733Z",
"is_superuser": true,
"username": "admin1",
"first_name": "Admin",
@ -463,6 +463,14 @@
"expire_date": "2022-03-07T10:37:08.963Z"
}
},
{
"model": "sessions.session",
"pk": "8r8hzr05yskg13ugvyysz5mq67yu0q09",
"fields": {
"session_data": ".eJxVjEEOwiAQRe_C2pCh1AIu3fcMZIYZbNVAUtqV8e7apAvd_vfef6mI2zrFrckSZ1YXZdTpdyNMDyk74DuWW9WplnWZSe-KPmjTY2V5Xg_372DCNn3rkIZEASSjiAUx3RkkEHXOsA_kDLAAos0eemvYosvBu5xC1_NAnEC9P_-NOIo:1odoSn:hXr9ZRCpPPwIFiVyFia14QHX4dwZigtH7yGUNgU2Gfg",
"expire_date": "2022-10-13T08:00:09.738Z"
}
},
{
"model": "sessions.session",
"pk": "9432vwcpkukpdrme8vipuk9rmt4jv6c8",
@ -5846,18 +5854,18 @@
},
{
"model": "webhooks.webhook",
"pk": 5,
"pk": 6,
"fields": {
"target_url": "http://example.com",
"target_url": "http://example.com/",
"description": "",
"events": "delete:invitation,delete:project,create:project,delete:comment,update:organization,update:issue,update:task,update:comment,update:job,update:project,delete:task,create:comment,delete:issue,delete:membership,create:invitation,create:task,create:issue,update:membership",
"events": "delete:task,create:task,update:issue,update:membership,create:comment,delete:comment,update:job,update:comment,create:project,delete:invitation,create:invitation,update:project,update:task,delete:membership,delete:issue,delete:project,update:organization,create:issue",
"type": "organization",
"content_type": "application/json",
"secret": "",
"is_active": true,
"enable_ssl": true,
"created_date": "2022-09-28T12:51:06.703Z",
"updated_date": "2022-09-28T12:51:06.703Z",
"created_date": "2022-09-29T08:00:48.440Z",
"updated_date": "2022-09-29T08:00:48.441Z",
"owner": [
"admin1"
],
@ -5865,51 +5873,6 @@
"organization": 1
}
},
{
"model": "webhooks.webhookdelivery",
"pk": 8,
"fields": {
"webhook": 5,
"event": "create:invitation",
"status_code": 200,
"redelivery": false,
"created_date": "2022-09-28T13:11:37.850Z",
"updated_date": "2022-09-28T13:11:38.311Z",
"changed_fields": "",
"request": {
"event": "create:invitation",
"sender": {
"id": 1,
"url": "http://localhost:8080/api/users/1",
"username": "admin1",
"last_name": "First",
"first_name": "Admin"
},
"invitation": {
"key": "hIH9RB3QqZLFwdUDmufaSPc2H8uS5cNjcG6pk8gfAIQ4jg6nJZZWDIQHMN1gFMk9",
"role": "supervisor",
"user": {
"id": 19,
"url": "http://localhost:8080/api/users/19",
"username": "user5",
"last_name": "Fifth",
"first_name": "User"
},
"owner": {
"id": 1,
"url": "http://localhost:8080/api/users/1",
"username": "admin1",
"last_name": "First",
"first_name": "Admin"
},
"created_date": "2022-09-28T13:11:37.839853Z",
"organization": 1
},
"webhook_id": 5
},
"response": "<!doctype html>\n<html>\n<head>\n <title>Example Domain</title>\n\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n body {\n background-color: #f0f0f2;\n margin: 0;\n padding: 0;\n font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n \n }\n div {\n width: 600px;\n margin: 5em auto;\n padding: 2em;\n background-color: #fdfdff;\n border-radius: 0.5em;\n box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);\n }\n a:link, a:visited {\n color: #38488f;\n text-decoration: none;\n }\n @media (max-width: 700px) {\n div {\n margin: 0 auto;\n width: auto;\n }\n }\n </style> \n</head>\n\n<body>\n<div>\n <h1>Example Domain</h1>\n <p>This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.</p>\n <p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>\n</div>\n</body>\n</html>\n"
}
},
{
"model": "admin.logentry",
"pk": 1,

@ -310,7 +310,7 @@
"is_active": true,
"is_staff": true,
"is_superuser": true,
"last_login": "2022-09-28T12:20:48.633000Z",
"last_login": "2022-09-29T08:00:09.733000Z",
"last_name": "First",
"url": "http://localhost:8080/api/users/1",
"username": "admin1"

@ -5,7 +5,7 @@
"results": [
{
"content_type": "application/json",
"created_date": "2022-09-28T12:51:06.703000Z",
"created_date": "2022-09-29T08:00:48.440000Z",
"description": "",
"enable_ssl": true,
"events": [
@ -28,10 +28,8 @@
"update:project",
"update:task"
],
"id": 5,
"id": 6,
"is_active": true,
"last_delivery_date": "2022-09-28T13:11:38.311000Z",
"last_status": 200,
"organization": 1,
"owner": {
"first_name": "Admin",
@ -41,10 +39,10 @@
"username": "admin1"
},
"project": null,
"target_url": "http://example.com",
"target_url": "http://example.com/",
"type": "organization",
"updated_date": "2022-09-28T12:51:06.703000Z",
"url": "http://localhost:8080/api/webhooks/5"
"updated_date": "2022-09-29T08:00:48.441000Z",
"url": "http://localhost:8080/api/webhooks/6"
},
{
"content_type": "application/json",

@ -97,6 +97,12 @@ def issues():
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def comments():
with open(osp.join(ASSETS_DIR, "comments.json")) as f:
return Container(json.load(f)["results"])
@pytest.fixture(scope="session")
def webhooks():
with open(osp.join(ASSETS_DIR, "webhooks.json")) as f:
@ -328,8 +334,9 @@ def find_issue_staff_user(is_issue_staff, is_issue_admin):
def find(issues, users, is_staff, is_admin):
for issue in issues:
for user in users:
i_admin, i_staff = is_issue_admin(user["id"], issue["id"]), is_issue_staff(
user["id"], issue["id"]
i_admin, i_staff = (
is_issue_admin(user["id"], issue["id"]),
is_issue_staff(user["id"], issue["id"]),
)
if (is_admin is None and (i_staff or i_admin) == is_staff) or (
is_admin == i_admin and is_staff == i_staff

@ -18,6 +18,7 @@ if __name__ == "__main__":
"membership",
"invitation",
"cloudstorage",
"comment",
"issue",
"webhook",
]:

Loading…
Cancel
Save