2019-06-05 15:12:34 +02:00
|
|
|
from types import SimpleNamespace
|
2020-05-26 07:16:25 +02:00
|
|
|
from unittest.mock import MagicMock, patch
|
2019-06-05 15:12:34 +02:00
|
|
|
from typing import Dict
|
|
|
|
|
2018-11-15 05:31:34 +01:00
|
|
|
from django.http import HttpRequest
|
|
|
|
|
|
|
|
from zerver.decorator import api_key_only_webhook_view
|
|
|
|
from zerver.lib.exceptions import InvalidJSONError, JsonableError
|
2018-04-24 20:22:38 +02:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase, WebhookTestCase
|
|
|
|
from zerver.lib.webhooks.common import \
|
|
|
|
validate_extract_webhook_http_header, \
|
2018-11-15 05:31:34 +01:00
|
|
|
MISSING_EVENT_HEADER_MESSAGE, MissingHTTPEventHeader, \
|
2019-06-21 04:41:30 +02:00
|
|
|
INVALID_JSON_MESSAGE, get_fixture_http_headers, standardize_headers
|
2018-11-15 05:31:34 +01:00
|
|
|
from zerver.models import get_user, get_realm, UserProfile
|
|
|
|
from zerver.lib.users import get_api_key
|
2018-04-24 20:22:38 +02:00
|
|
|
from zerver.lib.send_email import FromAddress
|
|
|
|
from zerver.lib.test_helpers import HostRequestMock
|
|
|
|
|
|
|
|
|
|
|
|
class WebhooksCommonTestCase(ZulipTestCase):
|
|
|
|
def test_webhook_http_header_header_exists(self) -> None:
|
|
|
|
webhook_bot = get_user('webhook-bot@zulip.com', get_realm('zulip'))
|
|
|
|
request = HostRequestMock()
|
|
|
|
request.META['HTTP_X_CUSTOM_HEADER'] = 'custom_value'
|
|
|
|
request.user = webhook_bot
|
|
|
|
|
|
|
|
header_value = validate_extract_webhook_http_header(request, 'X_CUSTOM_HEADER',
|
|
|
|
'test_webhook')
|
|
|
|
|
|
|
|
self.assertEqual(header_value, 'custom_value')
|
|
|
|
|
|
|
|
def test_webhook_http_header_header_does_not_exist(self) -> None:
|
|
|
|
webhook_bot = get_user('webhook-bot@zulip.com', get_realm('zulip'))
|
|
|
|
webhook_bot.last_reminder = None
|
|
|
|
notification_bot = self.notification_bot()
|
|
|
|
request = HostRequestMock()
|
|
|
|
request.user = webhook_bot
|
|
|
|
request.path = 'some/random/path'
|
|
|
|
|
|
|
|
exception_msg = "Missing the HTTP event header 'X_CUSTOM_HEADER'"
|
|
|
|
with self.assertRaisesRegex(MissingHTTPEventHeader, exception_msg):
|
|
|
|
validate_extract_webhook_http_header(request, 'X_CUSTOM_HEADER',
|
|
|
|
'test_webhook')
|
|
|
|
|
|
|
|
msg = self.get_last_message()
|
|
|
|
expected_message = MISSING_EVENT_HEADER_MESSAGE.format(
|
|
|
|
bot_name=webhook_bot.full_name,
|
|
|
|
request_path=request.path,
|
|
|
|
header_name='X_CUSTOM_HEADER',
|
|
|
|
integration_name='test_webhook',
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
support_email=FromAddress.SUPPORT,
|
2018-04-24 20:22:38 +02:00
|
|
|
).rstrip()
|
|
|
|
self.assertEqual(msg.sender.email, notification_bot.email)
|
|
|
|
self.assertEqual(msg.content, expected_message)
|
|
|
|
|
2019-01-31 14:32:37 +01:00
|
|
|
def test_notify_bot_owner_on_invalid_json(self) -> None:
|
2018-11-20 18:52:25 +01:00
|
|
|
@api_key_only_webhook_view('ClientName', notify_bot_owner_on_invalid_json=False)
|
2018-12-06 00:12:19 +01:00
|
|
|
def my_webhook_no_notify(request: HttpRequest, user_profile: UserProfile) -> None:
|
2018-11-15 05:31:34 +01:00
|
|
|
raise InvalidJSONError("Malformed JSON")
|
|
|
|
|
|
|
|
@api_key_only_webhook_view('ClientName', notify_bot_owner_on_invalid_json=True)
|
2018-12-06 00:12:19 +01:00
|
|
|
def my_webhook_notify(request: HttpRequest, user_profile: UserProfile) -> None:
|
2018-11-15 05:31:34 +01:00
|
|
|
raise InvalidJSONError("Malformed JSON")
|
|
|
|
|
|
|
|
webhook_bot_email = 'webhook-bot@zulip.com'
|
|
|
|
webhook_bot_realm = get_realm('zulip')
|
|
|
|
webhook_bot = get_user(webhook_bot_email, webhook_bot_realm)
|
|
|
|
webhook_bot_api_key = get_api_key(webhook_bot)
|
|
|
|
request = HostRequestMock()
|
|
|
|
request.POST['api_key'] = webhook_bot_api_key
|
|
|
|
request.host = "zulip.testserver"
|
|
|
|
expected_msg = INVALID_JSON_MESSAGE.format(webhook_name='ClientName')
|
|
|
|
|
|
|
|
last_message_id = self.get_last_message().id
|
|
|
|
with self.assertRaisesRegex(JsonableError, "Malformed JSON"):
|
2020-04-22 04:13:37 +02:00
|
|
|
my_webhook_no_notify(request) # type: ignore[call-arg] # mypy doesn't seem to apply the decorator
|
2018-11-15 05:31:34 +01:00
|
|
|
|
|
|
|
# First verify that without the setting, it doesn't send a PM to bot owner.
|
|
|
|
msg = self.get_last_message()
|
|
|
|
self.assertEqual(msg.id, last_message_id)
|
|
|
|
self.assertNotEqual(msg.content, expected_msg.strip())
|
|
|
|
|
|
|
|
# Then verify that with the setting, it does send such a message.
|
2018-12-06 00:12:19 +01:00
|
|
|
with self.assertRaisesRegex(JsonableError, "Malformed JSON"):
|
2020-04-22 04:13:37 +02:00
|
|
|
my_webhook_notify(request) # type: ignore[call-arg] # mypy doesn't seem to apply the decorator
|
2018-11-15 05:31:34 +01:00
|
|
|
msg = self.get_last_message()
|
|
|
|
self.assertNotEqual(msg.id, last_message_id)
|
|
|
|
self.assertEqual(msg.sender.email, self.notification_bot().email)
|
|
|
|
self.assertEqual(msg.content, expected_msg.strip())
|
|
|
|
|
2019-06-05 15:12:34 +02:00
|
|
|
@patch("zerver.lib.webhooks.common.importlib.import_module")
|
|
|
|
def test_get_fixture_http_headers_for_success(self, import_module_mock: MagicMock) -> None:
|
|
|
|
def fixture_to_headers(fixture_name: str) -> Dict[str, str]:
|
|
|
|
# A sample function which would normally perform some
|
|
|
|
# extra operations before returning a dictionary
|
|
|
|
# corresponding to the fixture name passed. For this test,
|
|
|
|
# we just return a fixed dictionary.
|
|
|
|
return {"key": "value"}
|
|
|
|
|
|
|
|
fake_module = SimpleNamespace(fixture_to_headers=fixture_to_headers)
|
|
|
|
import_module_mock.return_value = fake_module
|
|
|
|
|
|
|
|
headers = get_fixture_http_headers("some_integration", "complex_fixture")
|
|
|
|
self.assertEqual(headers, {"key": "value"})
|
|
|
|
|
|
|
|
def test_get_fixture_http_headers_for_non_existant_integration(self) -> None:
|
|
|
|
headers = get_fixture_http_headers("some_random_nonexistant_integration", "fixture_name")
|
|
|
|
self.assertEqual(headers, {})
|
|
|
|
|
|
|
|
@patch("zerver.lib.webhooks.common.importlib.import_module")
|
2019-06-22 06:57:40 +02:00
|
|
|
def test_get_fixture_http_headers_with_no_fixtures_to_headers_function(
|
|
|
|
self,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
import_module_mock: MagicMock,
|
2019-06-22 06:57:40 +02:00
|
|
|
) -> None:
|
|
|
|
|
2019-06-05 15:12:34 +02:00
|
|
|
fake_module = SimpleNamespace()
|
|
|
|
import_module_mock.return_value = fake_module
|
|
|
|
|
2019-06-22 06:57:40 +02:00
|
|
|
self.assertEqual(
|
|
|
|
get_fixture_http_headers("some_integration", "simple_fixture"),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
{},
|
2019-06-22 06:57:40 +02:00
|
|
|
)
|
2019-06-05 15:12:34 +02:00
|
|
|
|
2019-06-21 04:41:30 +02:00
|
|
|
def test_standardize_headers(self) -> None:
|
|
|
|
self.assertEqual(standardize_headers({}), {})
|
2019-06-05 15:12:34 +02:00
|
|
|
|
|
|
|
raw_headers = {"Content-Type": "text/plain", "X-Event-Type": "ping"}
|
2019-06-21 04:41:30 +02:00
|
|
|
djangoified_headers = standardize_headers(raw_headers)
|
2019-06-05 15:12:34 +02:00
|
|
|
expected_djangoified_headers = {"CONTENT_TYPE": "text/plain", "HTTP_X_EVENT_TYPE": "ping"}
|
|
|
|
self.assertEqual(djangoified_headers, expected_djangoified_headers)
|
|
|
|
|
2018-04-24 20:22:38 +02:00
|
|
|
class MissingEventHeaderTestCase(WebhookTestCase):
|
|
|
|
STREAM_NAME = 'groove'
|
|
|
|
URL_TEMPLATE = '/api/v1/external/groove?stream={stream}&api_key={api_key}'
|
|
|
|
|
|
|
|
# This tests the validate_extract_webhook_http_header function with
|
|
|
|
# an actual webhook, instead of just making a mock
|
|
|
|
def test_missing_event_header(self) -> None:
|
|
|
|
self.subscribe(self.test_user, self.STREAM_NAME)
|
|
|
|
result = self.client_post(self.url, self.get_body('ticket_state_changed'),
|
|
|
|
content_type="application/x-www-form-urlencoded")
|
|
|
|
self.assert_json_error(result, "Missing the HTTP event header 'X_GROOVE_EVENT'")
|
|
|
|
|
|
|
|
webhook_bot = get_user('webhook-bot@zulip.com', get_realm('zulip'))
|
|
|
|
webhook_bot.last_reminder = None
|
|
|
|
notification_bot = self.notification_bot()
|
|
|
|
msg = self.get_last_message()
|
|
|
|
expected_message = MISSING_EVENT_HEADER_MESSAGE.format(
|
|
|
|
bot_name=webhook_bot.full_name,
|
|
|
|
request_path='/api/v1/external/groove',
|
|
|
|
header_name='X_GROOVE_EVENT',
|
|
|
|
integration_name='Groove',
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
support_email=FromAddress.SUPPORT,
|
2018-04-24 20:22:38 +02:00
|
|
|
).rstrip()
|
2018-05-17 18:13:57 +02:00
|
|
|
if msg.sender.email != notification_bot.email: # nocoverage
|
|
|
|
# This block seems to fire occasionally; debug output:
|
|
|
|
print(msg)
|
|
|
|
print(msg.content)
|
2018-04-24 20:22:38 +02:00
|
|
|
self.assertEqual(msg.sender.email, notification_bot.email)
|
|
|
|
self.assertEqual(msg.content, expected_message)
|
|
|
|
|
2018-05-10 19:00:29 +02:00
|
|
|
def get_body(self, fixture_name: str) -> str:
|
2018-04-24 20:22:38 +02:00
|
|
|
return self.webhook_fixture_data("groove", fixture_name, file_type="json")
|