outgoing_webhook: Join build_bot_request and send_data_to_server.

The existing organization, of returning an opaque blob from
`build_bot_request`, which was later consumed by
`send_data_to_server`, is not particularly sensible; the steps become
oddly split between the OutgoingWebhookWorker, `do_rest_call`, and the
`OutgoingWebhookServiceInterface`.

Make the `OutgoingWebhookServiceInterface` in charge of building,
making, and returning the request in one method; another method
handles extracting content from a successful response.  `do_rest_call`
is responsible for calling both halves of this, and doing common error
handling.
This commit is contained in:
Alex Vandiver 2021-03-26 19:11:40 -07:00 committed by Tim Abbott
parent be706ea7a1
commit a280905a89
4 changed files with 82 additions and 44 deletions

View File

@ -32,11 +32,7 @@ class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta):
self.service_name: str = service_name
@abc.abstractmethod
def build_bot_request(self, event: Dict[str, Any]) -> Optional[Any]:
raise NotImplementedError
@abc.abstractmethod
def send_data_to_server(self, base_url: str, request_data: Any) -> Response:
def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]:
raise NotImplementedError
@abc.abstractmethod
@ -45,7 +41,7 @@ class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta):
class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface):
def build_bot_request(self, event: Dict[str, Any]) -> Optional[Any]:
def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]:
"""
We send a simple version of the message to outgoing
webhooks, since most of them really only need
@ -69,9 +65,7 @@ class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface):
"token": self.token,
"trigger": event["trigger"],
}
return json.dumps(request_data)
def send_data_to_server(self, base_url: str, request_data: Any) -> Response:
user_agent = "ZulipOutgoingWebhook/" + ZULIP_VERSION
headers = {
"User-Agent": user_agent,
@ -99,7 +93,7 @@ class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface):
class SlackOutgoingWebhookService(OutgoingWebhookServiceInterface):
def build_bot_request(self, event: Dict[str, Any]) -> Optional[Any]:
def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]:
if event["message"]["type"] == "private":
failure_message = "Slack outgoing webhooks don't support private messages."
fail_with_message(event, failure_message)
@ -118,12 +112,7 @@ class SlackOutgoingWebhookService(OutgoingWebhookServiceInterface):
("trigger_word", event["trigger"]),
("service_id", event["user_profile_id"]),
]
return request_data
def send_data_to_server(self, base_url: str, request_data: Any) -> Response:
response = requests.request("POST", base_url, data=request_data)
return response
return requests.request("POST", base_url, data=request_data)
def process_success(self, response_json: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if "text" in response_json:
@ -311,14 +300,16 @@ def process_success_response(
def do_rest_call(
base_url: str, request_data: Any, event: Dict[str, Any], service_handler: Any
base_url: str, event: Dict[str, Any], service_handler: Any
) -> Optional[Response]:
"""Returns response of call if no exception occurs."""
try:
response = service_handler.send_data_to_server(
base_url=base_url,
request_data=request_data,
response = service_handler.make_request(
base_url,
event,
)
if not response:
return None
if str(response.status_code).startswith("2"):
process_success_response(event, service_handler, response)
else:

View File

@ -55,7 +55,7 @@ class TestGenericOutgoingWebhookService(ZulipTestCase):
)
self.assertTrue(m.called)
def test_build_bot_request(self) -> None:
def test_make_request(self) -> None:
othello = self.example_user("othello")
stream = get_stream("Denmark", othello.realm)
message_id = self.send_stream_message(
@ -102,8 +102,25 @@ class TestGenericOutgoingWebhookService(ZulipTestCase):
"trigger": "mention",
}
request_data = self.handler.build_bot_request(event)
request_data = json.loads(request_data)
test_url = "https://example.com/example"
response = mock.Mock(spec=requests.Response)
response.status_code = 200
expect_200 = mock.patch("requests.request", return_value=response)
with expect_200 as mock_request:
self.handler.make_request(
test_url,
event,
)
mock_request.assert_called_once()
self.assertEqual(
mock_request.call_args[0],
(
"POST",
test_url,
),
)
request_data = mock_request.call_args[1]["json"]
validate_against_openapi_schema(request_data, "/zulip-outgoing-webhook", "post", "200")
self.assertEqual(request_data["data"], "@**test**")
self.assertEqual(request_data["token"], "abcdef")
@ -180,8 +197,22 @@ class TestSlackOutgoingWebhookService(ZulipTestCase):
service_class = get_service_interface_class(SLACK_INTERFACE)
self.handler = service_class(token="abcdef", user_profile=None, service_name="test-service")
def test_build_bot_request_stream_message(self) -> None:
request_data = self.handler.build_bot_request(self.stream_message_event)
def test_make_request_stream_message(self) -> None:
test_url = "https://example.com/example"
with mock.patch("requests.request") as mock_request:
self.handler.make_request(
test_url,
self.stream_message_event,
)
mock_request.assert_called_once()
self.assertEqual(
mock_request.call_args[0],
(
"POST",
test_url,
),
)
request_data = mock_request.call_args[1]["data"]
self.assertEqual(request_data[0][1], "abcdef") # token
self.assertEqual(request_data[1][1], "zulip") # team_id
@ -196,10 +227,15 @@ class TestSlackOutgoingWebhookService(ZulipTestCase):
self.assertEqual(request_data[10][1], 12) # user_profile_id
@mock.patch("zerver.lib.outgoing_webhook.fail_with_message")
def test_build_bot_request_private_message(self, mock_fail_with_message: mock.Mock) -> None:
request_data = self.handler.build_bot_request(self.private_message_event)
self.assertIsNone(request_data)
def test_make_request_private_message(self, mock_fail_with_message: mock.Mock) -> None:
test_url = "https://example.com/example"
with mock.patch("requests.request") as mock_request:
response = self.handler.make_request(
test_url,
self.private_message_event,
)
mock_request.assert_not_called()
self.assertIsNone(response)
self.assertTrue(mock_fail_with_message.called)
def test_process_success(self) -> None:

View File

@ -47,10 +47,23 @@ class DoRestCallTests(ZulipTestCase):
"message": {
"display_recipient": "Verona",
"stream_id": 999,
"sender_id": bot_user.id,
"sender_email": bot_user.email,
"sender_realm_id": bot_user.realm.id,
"sender_realm_str": bot_user.realm.string_id,
"sender_delivery_email": bot_user.delivery_email,
"sender_full_name": bot_user.full_name,
"sender_avatar_source": UserProfile.AVATAR_FROM_GRAVATAR,
"sender_avatar_version": 1,
"recipient_type": "stream",
"recipient_type_id": 999,
"sender_is_mirror_dummy": False,
TOPIC_NAME: "Foo",
"id": "",
"type": "stream",
"timestamp": 1,
},
"trigger": "mention",
"user_profile_id": bot_user.id,
"command": "",
"service_name": "",
@ -66,14 +79,14 @@ class DoRestCallTests(ZulipTestCase):
expect_send_response = mock.patch("zerver.lib.outgoing_webhook.send_response_message")
with expect_200, expect_send_response as mock_send:
do_rest_call("", None, mock_event, service_handler)
do_rest_call("", mock_event, service_handler)
self.assertTrue(mock_send.called)
for service_class in [GenericOutgoingWebhookService, SlackOutgoingWebhookService]:
handler = service_class("token", bot_user, "service")
with expect_200:
do_rest_call("", None, mock_event, handler)
do_rest_call("", mock_event, handler)
# TODO: assert something interesting here?
@ -86,7 +99,7 @@ class DoRestCallTests(ZulipTestCase):
with mock.patch("requests.request", return_value=response), self.assertLogs(
level="WARNING"
) as m:
final_response = do_rest_call("", None, mock_event, service_handler)
final_response = do_rest_call("", mock_event, service_handler)
assert final_response is not None
self.assertEqual(
@ -115,7 +128,7 @@ The webhook got a response with status code *500*.""",
expect_fail = mock.patch("zerver.lib.outgoing_webhook.fail_with_message")
with expect_400, expect_fail as mock_fail, self.assertLogs(level="WARNING") as m:
final_response = do_rest_call("", None, mock_event, service_handler)
final_response = do_rest_call("", mock_event, service_handler)
assert final_response is not None
self.assertEqual(
@ -144,9 +157,7 @@ The webhook got a response with status code *400*.""",
with mock.patch("requests.sessions.Session.send") as mock_send:
mock_send.return_value = ResponseMock(200)
final_response = do_rest_call(
"https://example.com/", "payload-stub", mock_event, service_handler
)
final_response = do_rest_call("https://example.com/", mock_event, service_handler)
assert final_response is not None
mock_send.assert_called_once()
@ -167,7 +178,7 @@ The webhook got a response with status code *400*.""",
def helper(side_effect: Any, error_text: str) -> None:
with mock.patch("requests.request", side_effect=side_effect):
do_rest_call("", None, mock_event, service_handler)
do_rest_call("", mock_event, service_handler)
bot_owner_notification = self.get_last_message()
self.assertIn(error_text, bot_owner_notification.content)
@ -202,7 +213,7 @@ The webhook got a response with status code *400*.""",
# Don't think that we should catch and assert whole log output(which is actually a very big error traceback).
# We are already asserting bot_owner_notification.content which verifies exception did occur.
with expect_request_exception, expect_logging_exception, expect_fail as mock_fail:
do_rest_call("", None, mock_event, service_handler)
do_rest_call("", mock_event, service_handler)
self.assertTrue(mock_fail.called)
@ -260,7 +271,8 @@ class TestOutgoingWebhookMessaging(ZulipTestCase):
sender = self.example_user("hamlet")
with mock.patch("zerver.worker.queue_processors.do_rest_call") as m:
with mock.patch("requests.request") as mock_request:
mock_request.return_value = ResponseMock(200)
self.send_personal_message(
sender,
bot,
@ -268,10 +280,11 @@ class TestOutgoingWebhookMessaging(ZulipTestCase):
)
url_token_tups = set()
for item in m.call_args_list:
for item in mock_request.call_args_list:
args = item[0]
base_url = args[0]
request_data = orjson.loads(args[1])
base_url = args[1]
kwargs = item[1]
request_data = kwargs["json"]
tup = (base_url, request_data["token"])
url_token_tups.add(tup)
message_data = request_data["message"]

View File

@ -742,9 +742,7 @@ class OutgoingWebhookWorker(QueueProcessingWorker):
for service in services:
event["service_name"] = str(service.name)
service_handler = get_outgoing_webhook_service_handler(service)
request_data = service_handler.build_bot_request(event)
if request_data:
do_rest_call(service.base_url, request_data, event, service_handler)
do_rest_call(service.base_url, event, service_handler)
@assign_queue("embedded_bots")