From 5e1fb838cb62bbf31b0808a5d209c3f147890ba6 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Wed, 29 Nov 2023 22:03:09 +0000 Subject: [PATCH 1/4] Do the full update ticket flow when followups are posted from the API --- docs/integration.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/integration.rst b/docs/integration.rst index 138306ec..ab0484ed 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -4,6 +4,9 @@ Integrating django-helpdesk into your application Ticket submission with embeded iframe ------------------------------------- +Ticket submission with embeded iframe +------------------------------------- + Django-helpdesk associates an email address with each submitted ticket. If you integrate django-helpdesk directly into your django application, logged in users will automatically have their email address set when they visit the `/tickets/submit/` form. If you wish to pre-fill fields in this form, you can do so simply by setting the following query parameters: - `queue` From e708281dcd03d3517b1c343c5e040363c234738c Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Sat, 2 Dec 2023 18:09:34 +0000 Subject: [PATCH 2/4] Implement webhooks. Fixes #264 --- ...{integration.rst => iframe_submission.rst} | 6 - docs/index.rst | 3 +- docs/webhooks.rst | 10 + helpdesk/apps.py | 3 + helpdesk/email.py | 3 + helpdesk/settings.py | 16 ++ helpdesk/tests/test_webhooks.py | 200 ++++++++++++++++++ helpdesk/update_ticket.py | 3 + helpdesk/webhooks.py | 56 +++++ requirements.txt | 1 + 10 files changed, 294 insertions(+), 7 deletions(-) rename docs/{integration.rst => iframe_submission.rst} (87%) create mode 100644 docs/webhooks.rst create mode 100644 helpdesk/tests/test_webhooks.py create mode 100644 helpdesk/webhooks.py diff --git a/docs/integration.rst b/docs/iframe_submission.rst similarity index 87% rename from docs/integration.rst rename to docs/iframe_submission.rst index ab0484ed..367716d7 100644 --- a/docs/integration.rst +++ b/docs/iframe_submission.rst @@ -1,9 +1,3 @@ -Integrating django-helpdesk into your application -================================================= - -Ticket submission with embeded iframe -------------------------------------- - Ticket submission with embeded iframe ------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 6ed44704..4b2c3898 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,7 +17,8 @@ Contents spam custom_fields api - integration + webhooks + iframe_submission teams license diff --git a/docs/webhooks.rst b/docs/webhooks.rst new file mode 100644 index 00000000..29e69bc8 --- /dev/null +++ b/docs/webhooks.rst @@ -0,0 +1,10 @@ +Registering webhooks to be notified of helpesk events +----------------------------------------------------- + +You can register webhooks to allow third party apps to be notified of helpdesk events. Webhooks can be registered in one of two ways: + +1. Setting comma separated environement variables; ``HELPDESK_NEW_TICKET_WEBHOOK_URLS``& ``HELPDESK_FOLLOWUP_WEBHOOK_URLS``. + +2. Adding getter functions to your ``settings.py``. These should return a list of strings (urls); ``HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS`` & ``HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS``. + +Once these URLs are configured, a serialized copy of the ticket object will be posted to each of these URLs each time a ticket is created or followed up on respectively. diff --git a/helpdesk/apps.py b/helpdesk/apps.py index a3ed19d2..d316d314 100644 --- a/helpdesk/apps.py +++ b/helpdesk/apps.py @@ -8,3 +8,6 @@ class HelpdeskConfig(AppConfig): # see: # https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.default_auto_field default_auto_field = 'django.db.models.AutoField' + + def ready(self): + from . import webhooks # noqa: F401 diff --git a/helpdesk/email.py b/helpdesk/email.py index fb63823d..c454d5a2 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,6 +21,7 @@ from email.message import EmailMessage, MIMEPart from email.utils import getaddresses from email_reply_parser import EmailReplyParser from helpdesk import settings +from helpdesk import webhooks from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.lib import process_attachments, safe_template_context from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket @@ -616,6 +617,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger) "Message seems to be auto-reply, not sending any emails back to the sender") else: send_info_email(message_id, f, ticket, context, queue, new) + if not new: + webhooks.notify_followup_webhooks(f) return ticket diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 1fc8a134..9115b03f 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -277,3 +277,19 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) # Attachment directories should be created with permission 755 (rwxr-xr-x) # Override it in your own Django settings.py HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8) + +def get_followup_webhook_urls(): + urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None) + if urls: + return urls.split(',') + +HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls) + +def get_new_ticket_webhook_urls(): + urls = os.environ.get('HELPDESK_NEW_TICKET_WEBHOOK_URLS', None) + if urls: + return urls.split(',') + +HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS', get_new_ticket_webhook_urls) + +HELPDESK_WEBHOOK_TIMEOUT = getattr(settings, 'HELPDESK_WEBHOOK_TIMEOUT', 3) diff --git a/helpdesk/tests/test_webhooks.py b/helpdesk/tests/test_webhooks.py new file mode 100644 index 00000000..c81dc7ae --- /dev/null +++ b/helpdesk/tests/test_webhooks.py @@ -0,0 +1,200 @@ +from django.contrib.auth.models import User +from helpdesk.models import Queue +from rest_framework.status import ( + HTTP_201_CREATED +) +from rest_framework.test import APITestCase +import json +import os +import requests +import logging + +# Set up a test weberver listeining on localhost:8123 for webhooks +import http.server +import threading +from http import HTTPStatus + +class WebhookRequestHandler(http.server.BaseHTTPRequestHandler): + server: "WebhookServer" + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + self.server.requests.append({ + 'path': self.path, + 'headers': self.headers, + 'body': body + }) + if self.path == '/new-ticket': + self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8'))) + elif self.path == '/followup': + self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8'))) + self.send_response(HTTPStatus.OK) + self.end_headers() + + def do_GET(self): + if not self.path == '/get-past-requests': + self.send_response(HTTPStatus.NOT_FOUND) + self.end_headers() + return + self.send_response(HTTPStatus.OK) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({ + 'new_ticket_requests': self.server.handled_new_ticket_requests, + 'follow_up_requests': self.server.handled_follow_up_requests + }).encode('utf-8')) + + +class WebhookServer(http.server.HTTPServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.requests = [] + self.handled_new_ticket_requests = [] + self.handled_follow_up_requests = [] + + def start(self): + self.thread = threading.Thread(target=self.serve_forever) + self.thread.daemon = True # Set as a daemon so it will be killed once the main thread is dead + self.thread.start() + + def stop(self): + self.shutdown() + self.server_close() + self.thread.join() + + +class WebhookTest(APITestCase): + @classmethod + def setUpTestData(cls): + cls.queue = Queue.objects.create( + title='Test Queue', + slug='test-queue', + ) + + def setUp(self): + staff_user = User.objects.create_user(username='test', is_staff=True) + self.client.force_authenticate(staff_user) + + def test_test_server(self): + server = WebhookServer(('localhost', 8123), WebhookRequestHandler) + os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8123/new-ticket' + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8123/followup' + server.start() + + requests.post('http://localhost:8123/new-ticket', json={ + "foo": "bar"}) + handled_webhook_requests = requests.get('http://localhost:8123/get-past-requests').json() + self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["foo"], "bar") + server.stop() + + def test_create_ticket_and_followup_via_api(self): + server = WebhookServer(('localhost', 8124), WebhookRequestHandler) + os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket' + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup' + server.start() + + response = self.client.post('/api/tickets/', { + 'queue': self.queue.id, + 'title': 'Test title', + 'description': 'Test description\nMulti lines', + 'submitter_email': 'test@mail.com', + 'priority': 4 + }) + self.assertEqual(response.status_code, HTTP_201_CREATED) + handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') + handled_webhook_requests = handled_webhook_requests.json() + self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) >= 1) + self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) + self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title") + self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines") + response = self.client.post('/api/followups/', { + 'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"], + "comment": "Test comment", + }) + self.assertEqual(response.status_code, HTTP_201_CREATED) + handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') + handled_webhook_requests = handled_webhook_requests.json() + self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1) + self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") + server.stop() + + def test_create_ticket_and_followup_via_email(self): + from .. import email + + server = WebhookServer(('localhost', 8125), WebhookRequestHandler) + os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8125/new-ticket' + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8125/followup' + server.start() + class MockMessage(dict): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def get_all(self, key, default=None): + return self.__dict__.get(key, default) + + payload = { + 'body': "hello", + 'full_body': "hello", + 'subject': "Test subject", + 'queue': self.queue, + 'sender_email': "user@example.com", + 'priority': "1", + 'files': [], + } + + message = { + "To": ["info@example.com"], + "Cc": [], + "Message-Id": "random1", + "In-Reply-To": "", + } + email.create_object_from_email_message( + message=MockMessage(**message), + ticket_id=None, + payload=payload, + files=[], + logger=logging.getLogger('helpdesk'), + ) + + handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests') + handled_webhook_requests = handled_webhook_requests.json() + self.assertEqual(len(handled_webhook_requests['new_ticket_requests']), 1) + self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) + + ticket_id = handled_webhook_requests['new_ticket_requests'][-1]["ticket"]['id'] + from .. import models + ticket = models.Ticket.objects.get(id=ticket_id) + + payload = { + 'body': "hello", + 'full_body': "hello", + 'subject': f"[test-queue-{ticket_id}] Test subject", + 'queue': self.queue, + 'sender_email': "user@example.com", + 'priority': "1", + 'files': [], + } + + message = { + "To": ["info@example.com"], + "Cc": [], + "Message-Id": "random", + "In-Reply-To": "123", + } + email.create_object_from_email_message( + message=MockMessage(**message), + ticket_id=ticket_id, + payload=payload, + files=[], + logger=logging.getLogger('helpdesk'), + ) + handled_webhook_requests = requests.get('http://localhost:8125/get-past-requests') + handled_webhook_requests = handled_webhook_requests.json() + self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1) + self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "hello") + self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["id"], ticket_id) + + server.stop() + + diff --git a/helpdesk/update_ticket.py b/helpdesk/update_ticket.py index 658c4b17..711566ba 100644 --- a/helpdesk/update_ticket.py +++ b/helpdesk/update_ticket.py @@ -386,6 +386,9 @@ def update_ticket( )) ticket.save() + from helpdesk.webhooks import notify_followup_webhooks + notify_followup_webhooks(f) + # auto subscribe user if enabled add_staff_subscription(user, ticket) return f diff --git a/helpdesk/webhooks.py b/helpdesk/webhooks.py new file mode 100644 index 00000000..c7902691 --- /dev/null +++ b/helpdesk/webhooks.py @@ -0,0 +1,56 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from . import settings + +import requests +import requests.exceptions +import logging + +from .models import Ticket +from .serializers import TicketSerializer + +logger = logging.getLogger(__name__) + +def notify_followup_webhooks(followup): + urls = settings.HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS() + if not urls: + return + # Serialize the ticket associated with the followup + ticket = followup.ticket + serialized_ticket = TicketSerializer(ticket).data + + # Prepare the data to send + data = { + 'ticket': serialized_ticket, + 'queue_slug': ticket.queue.slug, + 'followup_id': followup.id + } + + for url in urls: + try: + requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) + except requests.exceptions.Timeout: + logger.error('Timeout while sending followup webhook to %s', url) + + +@receiver(post_save, sender=Ticket) +def ticket_post_save(sender, instance, created, **kwargs): + if not created: + return + urls = settings.HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS() + if not urls: + return + # Serialize the ticket + serialized_ticket = TicketSerializer(instance).data + + # Prepare the data to send + data = { + 'ticket': serialized_ticket, + 'queue_slug': instance.queue.slug + } + + for url in urls: + try: + requests.post(url, json=data, timeout=settings.HELPDESK_WEBHOOK_TIMEOUT) + except requests.exceptions.Timeout: + logger.error('Timeout while sending new ticket webhook to %s', url) diff --git a/requirements.txt b/requirements.txt index 453bef8f..322c9d03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ django-model-utils django-cleanup oauthlib requests_oauthlib +requests From 04bbb27b249cd518f86669bc5dc20f0bc6b52c44 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Sun, 3 Dec 2023 00:34:29 +0000 Subject: [PATCH 3/4] Fix mistake in if chaining --- helpdesk/views/public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/views/public.py b/helpdesk/views/public.py index 2ee626cb..41d7da2d 100644 --- a/helpdesk/views/public.py +++ b/helpdesk/views/public.py @@ -210,7 +210,7 @@ class ViewTicket(TemplateView): queue, ticket_id = Ticket.queue_and_id_from_query(ticket_req) if request.user.is_authenticated and request.user.email == email: ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) - if hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: + elif hasattr(settings, 'HELPDESK_VIEW_A_TICKET_PUBLIC') and settings.HELPDESK_VIEW_A_TICKET_PUBLIC: ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email) else: ticket = Ticket.objects.get(id=ticket_id, submitter_email__iexact=email, secret_key__iexact=key) From 27f295098c494094da1381480ab3954ee816969b Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Mon, 4 Dec 2023 23:36:20 +0000 Subject: [PATCH 4/4] Improve webhook env var handling & docs --- docs/webhooks.rst | 4 +++- helpdesk/settings.py | 3 ++- helpdesk/tests/test_webhooks.py | 22 ++++++++++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/webhooks.rst b/docs/webhooks.rst index 29e69bc8..257dbe3f 100644 --- a/docs/webhooks.rst +++ b/docs/webhooks.rst @@ -3,8 +3,10 @@ Registering webhooks to be notified of helpesk events You can register webhooks to allow third party apps to be notified of helpdesk events. Webhooks can be registered in one of two ways: -1. Setting comma separated environement variables; ``HELPDESK_NEW_TICKET_WEBHOOK_URLS``& ``HELPDESK_FOLLOWUP_WEBHOOK_URLS``. +1. Setting the following environement variables to a comma separated list of URLs; ``HELPDESK_NEW_TICKET_WEBHOOK_URLS``& ``HELPDESK_FOLLOWUP_WEBHOOK_URLS``. 2. Adding getter functions to your ``settings.py``. These should return a list of strings (urls); ``HELPDESK_GET_NEW_TICKET_WEBHOOK_URLS`` & ``HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS``. +3. You can optionally set ``HELPDESK_WEBHOOK_TIMEOUT`` which defaults to 3 seconds. Warning, however, webhook requests are sent out sychronously on ticket update. If your webhook handling server is too slow, you should fix this rather than causing helpdesk freezes by messing with this variable. + Once these URLs are configured, a serialized copy of the ticket object will be posted to each of these URLs each time a ticket is created or followed up on respectively. diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 9115b03f..ee82ffcd 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -7,6 +7,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ImproperlyConfigured import os +import re import warnings @@ -281,7 +282,7 @@ HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_P def get_followup_webhook_urls(): urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None) if urls: - return urls.split(',') + return re.split(r'[\s],[\s]', urls) HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS = getattr(settings, 'HELPDESK_GET_FOLLOWUP_WEBHOOK_URLS', get_followup_webhook_urls) diff --git a/helpdesk/tests/test_webhooks.py b/helpdesk/tests/test_webhooks.py index c81dc7ae..7aa23cde 100644 --- a/helpdesk/tests/test_webhooks.py +++ b/helpdesk/tests/test_webhooks.py @@ -27,8 +27,12 @@ class WebhookRequestHandler(http.server.BaseHTTPRequestHandler): }) if self.path == '/new-ticket': self.server.handled_new_ticket_requests.append(json.loads(body.decode('utf-8'))) + if self.path == '/new-ticket-1': + self.server.handled_new_ticket_requests_1.append(json.loads(body.decode('utf-8'))) elif self.path == '/followup': self.server.handled_follow_up_requests.append(json.loads(body.decode('utf-8'))) + elif self.path == '/followup-1': + self.server.handled_follow_up_requests_1.append(json.loads(body.decode('utf-8'))) self.send_response(HTTPStatus.OK) self.end_headers() @@ -42,7 +46,9 @@ class WebhookRequestHandler(http.server.BaseHTTPRequestHandler): self.end_headers() self.wfile.write(json.dumps({ 'new_ticket_requests': self.server.handled_new_ticket_requests, - 'follow_up_requests': self.server.handled_follow_up_requests + 'new_ticket_requests_1': self.server.handled_new_ticket_requests_1, + 'follow_up_requests': self.server.handled_follow_up_requests, + 'follow_up_requests_1': self.server.handled_follow_up_requests_1 }).encode('utf-8')) @@ -51,7 +57,9 @@ class WebhookServer(http.server.HTTPServer): super().__init__(*args, **kwargs) self.requests = [] self.handled_new_ticket_requests = [] + self.handled_new_ticket_requests_1 = [] self.handled_follow_up_requests = [] + self.handled_follow_up_requests_1 = [] def start(self): self.thread = threading.Thread(target=self.serve_forever) @@ -78,8 +86,6 @@ class WebhookTest(APITestCase): def test_test_server(self): server = WebhookServer(('localhost', 8123), WebhookRequestHandler) - os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8123/new-ticket' - os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8123/followup' server.start() requests.post('http://localhost:8123/new-ticket', json={ @@ -90,8 +96,8 @@ class WebhookTest(APITestCase): def test_create_ticket_and_followup_via_api(self): server = WebhookServer(('localhost', 8124), WebhookRequestHandler) - os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket' - os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup' + os.environ['HELPDESK_NEW_TICKET_WEBHOOK_URLS'] = 'http://localhost:8124/new-ticket, http://localhost:8124/new-ticket-1' + os.environ["HELPDESK_FOLLOWUP_WEBHOOK_URLS"] = 'http://localhost:8124/followup , http://localhost:8124/followup-1' server.start() response = self.client.post('/api/tickets/', { @@ -104,9 +110,11 @@ class WebhookTest(APITestCase): self.assertEqual(response.status_code, HTTP_201_CREATED) handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') handled_webhook_requests = handled_webhook_requests.json() - self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) >= 1) + self.assertTrue(len(handled_webhook_requests['new_ticket_requests']) == 1) + self.assertTrue(len(handled_webhook_requests['new_ticket_requests_1']) == 1) self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 0) self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["title"], "Test title") + self.assertEqual(handled_webhook_requests['new_ticket_requests_1'][-1]["ticket"]["title"], "Test title") self.assertEqual(handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["description"], "Test description\nMulti lines") response = self.client.post('/api/followups/', { 'ticket': handled_webhook_requests['new_ticket_requests'][-1]["ticket"]["id"], @@ -116,7 +124,9 @@ class WebhookTest(APITestCase): handled_webhook_requests = requests.get('http://localhost:8124/get-past-requests') handled_webhook_requests = handled_webhook_requests.json() self.assertEqual(len(handled_webhook_requests['follow_up_requests']), 1) + self.assertEqual(len(handled_webhook_requests['follow_up_requests_1']), 1) self.assertEqual(handled_webhook_requests['follow_up_requests'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") + self.assertEqual(handled_webhook_requests['follow_up_requests_1'][-1]["ticket"]["followup_set"][-1]["comment"], "Test comment") server.stop() def test_create_ticket_and_followup_via_email(self):