diff --git a/static/images/integrations/integrations_dev_panel.png b/static/images/integrations/integrations_dev_panel.png index 462757ae5e..99d2878dc4 100644 Binary files a/static/images/integrations/integrations_dev_panel.png and b/static/images/integrations/integrations_dev_panel.png differ diff --git a/static/js/integrations_dev_panel.js b/static/js/integrations_dev_panel.js index 10014753b9..f8220fa0d9 100644 --- a/static/js/integrations_dev_panel.js +++ b/static/js/integrations_dev_panel.js @@ -17,6 +17,7 @@ var clear_handlers = { integration_name: function () { $('#integration_name').children()[0].selected = true; }, fixture_name: function () { $('#fixture_name').empty(); }, fixture_body: function () { $("#fixture_body")[0].value = ""; }, + custom_http_headers: function () { $("#custom_http_headers")[0].value = ""; }, }; function clear_elements(elements) { @@ -141,7 +142,7 @@ function get_fixtures(integration_name) { /* Request fixtures from the backend for any integrations that we don't already have fixtures for (which would be stored in the JS variable called "loaded_fixtures"). */ if (integration_name === "") { - clear_elements(["fixture_body", "fixture_name", "URL", "message"]); + clear_elements(["custom_http_headers", "fixture_body", "fixture_name", "URL", "message"]); return; } @@ -190,9 +191,21 @@ function send_webhook_fixture_message() { return; } + var custom_headers = $("#custom_http_headers").val(); + if (custom_headers !== "") { + // JSON.parse("") would trigger an error, as empty strings do not qualify as JSON. + try { + // Let JavaScript validate the JSON for us. + custom_headers = JSON.stringify(JSON.parse(custom_headers)); + } catch (err) { + set_message("Custom HTTP headers are not in a valid JSON format.", "warning"); + return; + } + } + channel.post({ url: "/devtools/integrations/check_send_webhook_fixture_message", - data: {url: url, body: body}, + data: {url: url, body: body, custom_headers: custom_headers}, beforeSend: function (xhr) {xhr.setRequestHeader('X-CSRFToken', csrftoken);}, success: function () { // If the previous fixture body was sent successfully, then we should change the success @@ -214,7 +227,7 @@ function send_webhook_fixture_message() { // Initialization $(function () { clear_elements(["stream_name", "topic_name", "URL", "bot_name", "integration_name", - "fixture_name", "fixture_body", "message"]); + "fixture_name", "custom_http_headers", "fixture_body", "message"]); var potential_default_bot = $("#bot_name")[0][1]; if (potential_default_bot !== undefined) { @@ -222,7 +235,7 @@ $(function () { } $('#integration_name').change(function () { - clear_elements(["fixture_body", "fixture_name", "message"]); + clear_elements(["custom_http_headers", "fixture_body", "fixture_name", "message"]); var integration_name = $(this).children("option:selected").val(); get_fixtures(integration_name); update_url(); diff --git a/static/styles/integrations_dev_panel.css b/static/styles/integrations_dev_panel.css index e38f68a074..bb36c4cd96 100644 --- a/static/styles/integrations_dev_panel.css +++ b/static/styles/integrations_dev_panel.css @@ -7,6 +7,11 @@ padding: 10px; } +#custom_http_headers { + height: 200px; + width: 700px; +} + #fixture_body { height: 500px; width: 700px; diff --git a/templates/zerver/api/incoming-webhooks-walkthrough.md b/templates/zerver/api/incoming-webhooks-walkthrough.md index f6ff4b1d31..b62b8bc604 100644 --- a/templates/zerver/api/incoming-webhooks-walkthrough.md +++ b/templates/zerver/api/incoming-webhooks-walkthrough.md @@ -237,17 +237,19 @@ This is the GUI tool. 1. Run `./tools/run-dev.py` then go to http://localhost:9991/devtools/integrations/. -2. Set the following mandatory fields: -- **Bot** - Any incoming webhook bot. -- **Integration** - One of the integrations. -- **Fixture** - Though not mandatory, it's recommended that you select one and then tweak it if necessary. +2. Set the following mandatory fields: +**Bot** - Any incoming webhook bot. +**Integration** - One of the integrations. +**Fixture** - Though not mandatory, it's recommended that you select one and then tweak it if necessary. The remaining fields are optional, and the URL will automatically be generated. 3. Click **Send**! -By opening Zulip in one tab and this tool in another, you can quickly tweak +By opening Zulip in one tab and then this tool in another, you can quickly tweak your code and send sample messages for many different test fixtures. +Note: Custom HTTP Headers must be entered as a JSON dictionary, if you want to use any in the first place that is. +Feel free to use 4-spaces as tabs for indentation if you'd like! Your sample notification may look like: diff --git a/templates/zerver/integrations/development/dev_panel.html b/templates/zerver/integrations/development/dev_panel.html index 18084fca7c..0f9e5236c4 100644 --- a/templates/zerver/integrations/development/dev_panel.html +++ b/templates/zerver/integrations/development/dev_panel.html @@ -65,6 +65,15 @@ + + Custom HTTP Headers + + + + + + + JSON Body diff --git a/zerver/tests/test_integrations_dev_panel.py b/zerver/tests/test_integrations_dev_panel.py index 7f1d317932..c4aae28f5f 100644 --- a/zerver/tests/test_integrations_dev_panel.py +++ b/zerver/tests/test_integrations_dev_panel.py @@ -15,7 +15,8 @@ class TestIntegrationsDevPanel(ZulipTestCase): data = { "url": url, - "body": body + "body": body, + "custom_headers": "", } response = self.client_post(target_url, data) @@ -24,7 +25,7 @@ class TestIntegrationsDevPanel(ZulipTestCase): expected_response = {"result": "error", "msg": "Internal server error"} self.assertEqual(ujson.loads(response.content), expected_response) - def test_check_send_webhook_fixture_message_for_success(self) -> None: + def test_check_send_webhook_fixture_message_for_success_without_headers(self) -> None: bot = get_user('webhook-bot@zulip.com', self.zulip_realm) url = "/api/v1/external/airbrake?api_key={key}&stream=Denmark&topic=Airbrake Notifications".format(key=bot.api_key) target_url = "/devtools/integrations/check_send_webhook_fixture_message" @@ -34,6 +35,7 @@ class TestIntegrationsDevPanel(ZulipTestCase): data = { "url": url, "body": body, + "custom_headers": "", } response = self.client_post(target_url, data) @@ -45,6 +47,28 @@ class TestIntegrationsDevPanel(ZulipTestCase): self.assertEqual(Stream.objects.get(id=latest_msg.recipient.type_id).name, "Denmark") self.assertEqual(latest_msg.topic_name(), "Airbrake Notifications") + def test_check_send_webhook_fixture_message_for_success_with_headers(self) -> None: + bot = get_user('webhook-bot@zulip.com', self.zulip_realm) + url = "/api/v1/external/github?api_key={key}&stream=Denmark&topic=GitHub Notifications".format(key=bot.api_key) + target_url = "/devtools/integrations/check_send_webhook_fixture_message" + with open("zerver/webhooks/github/fixtures/ping_organization.json", "r") as f: + body = f.read() + + data = { + "url": url, + "body": body, + "custom_headers": ujson.dumps({"X_GITHUB_EVENT": "ping"}), + } + + response = self.client_post(target_url, data) + self.assertEqual(response.status_code, 200) + + latest_msg = Message.objects.latest('id') + expected_message = "GitHub webhook has been successfully configured by eeshangarg." + self.assertEqual(latest_msg.content, expected_message) + self.assertEqual(Stream.objects.get(id=latest_msg.recipient.type_id).name, "Denmark") + self.assertEqual(latest_msg.topic_name(), "GitHub Notifications") + def test_get_fixtures_for_nonexistant_integration(self) -> None: target_url = "/devtools/integrations/somerandomnonexistantintegration/fixtures" response = self.client_get(target_url) diff --git a/zerver/views/development/integrations.py b/zerver/views/development/integrations.py index d0f28e019c..264f2fdb93 100644 --- a/zerver/views/development/integrations.py +++ b/zerver/views/development/integrations.py @@ -10,6 +10,7 @@ from zerver.lib.integrations import WEBHOOK_INTEGRATIONS from zerver.lib.request import has_request_variables, REQ from zerver.lib.response import json_success, json_error from zerver.models import UserProfile, get_realm +from zerver.management.commands.send_webhook_fixture_message import parse_headers ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../') @@ -58,10 +59,19 @@ def get_fixtures(request: HttpResponse, @has_request_variables def check_send_webhook_fixture_message(request: HttpRequest, url: str=REQ(), - body: str=REQ()) -> HttpResponse: + body: str=REQ(), + custom_headers: str=REQ()) -> HttpResponse: client = Client() realm = get_realm("zulip") - response = client.post(url, body, content_type="application/json", HTTP_HOST=realm.host) + try: + headers = parse_headers(custom_headers) + except ValueError as ve: + return json_error("Custom HTTP headers are not in a valid JSON format. {}".format(ve)) # nolint + if not headers: + headers = {} + http_host = headers.pop("HTTP_HOST", realm.host) + content_type = headers.pop("HTTP_CONTENT_TYPE", "application/json") + response = client.post(url, body, content_type=content_type, HTTP_HOST=http_host, **headers) if response.status_code == 200: return json_success() else: