2020-08-07 01:09:47 +02:00
from typing import Any , Dict
2020-06-11 00:54:34 +02:00
from unittest import mock
2016-07-23 07:51:30 +02:00
2020-08-07 01:09:47 +02:00
import orjson
2020-06-11 00:54:34 +02:00
import requests
2020-03-28 13:22:19 +01:00
2020-06-11 00:54:34 +02:00
from version import ZULIP_VERSION
from zerver . lib . actions import do_create_user
2018-10-11 14:11:52 +02:00
from zerver . lib . outgoing_webhook import (
GenericOutgoingWebhookService ,
SlackOutgoingWebhookService ,
2020-06-11 00:54:34 +02:00
do_rest_call ,
2018-10-11 14:11:52 +02:00
)
2017-05-25 19:48:36 +02:00
from zerver . lib . test_classes import ZulipTestCase
2018-11-10 16:43:59 +01:00
from zerver . lib . topic import TOPIC_NAME
2020-03-28 13:22:19 +01:00
from zerver . lib . users import add_service
2020-07-24 19:21:42 +02:00
from zerver . models import Recipient , Service , UserProfile , get_display_recipient
2016-07-23 08:13:33 +02:00
2019-01-09 15:29:17 +01:00
2017-11-05 11:49:43 +01:00
class ResponseMock :
2020-08-07 01:09:47 +02:00
def __init__ ( self , status_code : int , content : bytes = b " " ) - > None :
2016-07-23 08:13:33 +02:00
self . status_code = status_code
self . content = content
2020-08-07 01:09:47 +02:00
self . text = content . decode ( )
2016-07-23 08:13:33 +02:00
2017-11-05 10:51:25 +01:00
def request_exception_error ( http_method : Any , final_url : Any , data : Any , * * request_kwargs : Any ) - > Any :
2017-08-29 15:21:25 +02:00
raise requests . exceptions . RequestException ( " I ' m a generic exception :( " )
2016-07-23 08:13:33 +02:00
2017-11-05 10:51:25 +01:00
def timeout_error ( http_method : Any , final_url : Any , data : Any , * * request_kwargs : Any ) - > Any :
2017-08-16 13:30:47 +02:00
raise requests . exceptions . Timeout ( " Time is up! " )
2016-07-23 08:13:33 +02:00
2018-10-10 19:52:32 +02:00
def connection_error ( http_method : Any , final_url : Any , data : Any , * * request_kwargs : Any ) - > Any :
raise requests . exceptions . ConnectionError ( )
2016-07-23 08:13:33 +02:00
class DoRestCallTests ( ZulipTestCase ) :
2020-07-24 19:21:42 +02:00
def mock_event ( self , bot_user : UserProfile ) - > Dict [ str , Any ] :
return {
2017-08-29 15:21:25 +02:00
# In the tests there is no active queue processor, so retries don't get processed.
# Therefore, we need to emulate `retry_event` in the last stage when the maximum
# retries have been exceeded.
' failed_tries ' : 3 ,
' message ' : { ' display_recipient ' : ' Verona ' ,
2018-02-15 21:02:47 +01:00
' stream_id ' : 999 ,
2018-11-10 16:43:59 +01:00
TOPIC_NAME : ' Foo ' ,
2017-08-29 15:21:25 +02:00
' id ' : ' ' ,
' type ' : ' stream ' } ,
2020-07-24 19:21:42 +02:00
' user_profile_id ' : bot_user . id ,
2017-08-29 15:21:25 +02:00
' command ' : ' ' ,
' service_name ' : ' ' }
2017-09-25 16:03:35 +02:00
2020-07-24 18:46:14 +02:00
def test_successful_request ( self ) - > None :
2020-07-24 19:21:42 +02:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2020-08-07 01:09:47 +02:00
response = ResponseMock ( 200 , orjson . dumps ( dict ( content = ' whatever ' ) ) )
2020-07-24 18:46:14 +02:00
expect_200 = mock . patch ( ' requests.request ' , return_value = response )
expect_send_response = mock . patch ( ' zerver.lib.outgoing_webhook.send_response_message ' )
with expect_200 , expect_send_response as mock_send :
2020-07-24 19:21:42 +02:00
do_rest_call ( ' ' , None , mock_event , service_handler )
2020-07-24 18:24:28 +02:00
self . assertTrue ( mock_send . called )
2016-07-23 08:13:33 +02:00
2018-10-11 14:11:52 +02:00
for service_class in [ GenericOutgoingWebhookService , SlackOutgoingWebhookService ] :
2020-07-24 19:21:42 +02:00
handler = service_class ( " token " , bot_user , " service " )
2020-07-24 18:46:14 +02:00
with expect_200 :
2020-07-24 19:21:42 +02:00
do_rest_call ( ' ' , None , mock_event , handler )
2020-07-24 18:46:14 +02:00
# TODO: assert something interesting here?
2018-10-11 14:11:52 +02:00
2020-07-24 19:00:11 +02:00
def test_retry_request ( self ) - > None :
2020-07-24 19:21:42 +02:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2017-11-03 13:12:59 +01:00
response = ResponseMock ( 500 )
2020-10-29 20:21:18 +01:00
with mock . patch ( ' requests.request ' , return_value = response ) , self . assertLogs ( level = " WARNING " ) as m :
final_response = do_rest_call ( ' ' , None , mock_event , service_handler )
assert final_response is not None
2017-08-29 15:21:25 +02:00
2020-10-30 00:13:37 +01:00
self . assertEqual ( m . output , [ f " WARNING:root:Message http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/ triggered an outgoing webhook, returning status code 500. \n Content of response (in quotes): \" { final_response . text } \" " ] )
2020-07-24 18:24:28 +02:00
bot_owner_notification = self . get_last_message ( )
self . assertEqual ( bot_owner_notification . content ,
''' [A message](http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/) triggered an outgoing webhook.
2017-08-16 13:30:47 +02:00
The webhook got a response with status code * 500 * . ''' )
2020-07-24 19:00:11 +02:00
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . id )
2016-07-23 08:13:33 +02:00
2020-07-24 18:46:14 +02:00
def test_fail_request ( self ) - > None :
2020-07-24 19:21:42 +02:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2017-11-03 13:12:59 +01:00
response = ResponseMock ( 400 )
2020-07-24 18:46:14 +02:00
expect_400 = mock . patch ( " requests.request " , return_value = response )
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
2020-10-29 20:21:18 +01:00
with expect_400 , expect_fail as mock_fail , self . assertLogs ( level = " WARNING " ) as m :
final_response = do_rest_call ( ' ' , None , mock_event , service_handler )
assert final_response is not None
2020-10-30 00:13:37 +01:00
self . assertEqual ( m . output , [ f " WARNING:root:Message http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/ triggered an outgoing webhook, returning status code 400. \n Content of response (in quotes): \" { final_response . text } \" " ] )
2020-07-24 18:24:28 +02:00
2020-07-24 18:46:14 +02:00
self . assertTrue ( mock_fail . called )
2020-07-24 18:24:28 +02:00
bot_owner_notification = self . get_last_message ( )
self . assertEqual ( bot_owner_notification . content ,
''' [A message](http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/) triggered an outgoing webhook.
2017-08-29 15:21:25 +02:00
The webhook got a response with status code * 400 * . ''' )
2020-07-24 19:00:11 +02:00
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . id )
2016-07-23 08:13:33 +02:00
2019-01-09 15:29:17 +01:00
def test_headers ( self ) - > None :
2020-07-24 19:21:42 +02:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2020-10-29 20:21:18 +01:00
with mock . patch ( ' requests.request ' ) as mock_request , self . assertLogs ( level = " WARNING " ) as m :
final_response = do_rest_call ( ' ' , ' payload-stub ' , mock_event , service_handler )
assert final_response is not None
2020-10-30 00:13:37 +01:00
self . assertEqual ( m . output , [ f " WARNING:root:Message http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/ triggered an outgoing webhook, returning status code { final_response . status_code } . \n Content of response (in quotes): \" { final_response . text } \" " ] )
2019-01-09 15:29:17 +01:00
2020-07-24 18:24:28 +02:00
kwargs = mock_request . call_args [ 1 ]
self . assertEqual ( kwargs [ ' data ' ] , ' payload-stub ' )
user_agent = ' ZulipOutgoingWebhook/ ' + ZULIP_VERSION
headers = {
' content-type ' : ' application/json ' ,
' User-Agent ' : user_agent ,
}
self . assertEqual ( kwargs [ ' headers ' ] , headers )
2019-01-09 15:29:17 +01:00
2018-10-10 19:52:32 +02:00
def test_error_handling ( self ) - > None :
2020-10-29 20:21:18 +01:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
bot_user_email = self . example_user_map [ ' outgoing_webhook_bot ' ]
2018-10-10 19:52:32 +02:00
def helper ( side_effect : Any , error_text : str ) - > None :
2020-07-24 19:21:42 +02:00
2020-10-29 20:21:18 +01:00
with mock . patch ( ' requests.request ' , side_effect = side_effect ) :
do_rest_call ( ' ' , None , mock_event , service_handler )
2020-07-24 18:24:28 +02:00
bot_owner_notification = self . get_last_message ( )
self . assertIn ( error_text , bot_owner_notification . content )
self . assertIn ( ' triggered ' , bot_owner_notification . content )
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . id )
2018-10-10 19:52:32 +02:00
2020-10-29 20:21:18 +01:00
with self . assertLogs ( level = " INFO " ) as i :
helper ( side_effect = timeout_error , error_text = ' A timeout occurred. ' )
helper ( side_effect = connection_error , error_text = ' A connection error occurred. ' )
log_output = [
f " INFO:root:Trigger event { mock_event [ ' command ' ] } on { mock_event [ ' service_name ' ] } timed out. Retrying " ,
f " WARNING:root:Maximum retries exceeded for trigger: { bot_user_email } event: { mock_event [ ' command ' ] } " ,
f " INFO:root:Trigger event { mock_event [ ' command ' ] } on { mock_event [ ' service_name ' ] } resulted in a connection error. Retrying " ,
f " WARNING:root:Maximum retries exceeded for trigger: { bot_user_email } event: { mock_event [ ' command ' ] } "
]
self . assertEqual ( i . output , log_output )
2016-07-23 08:13:33 +02:00
2020-07-24 18:46:14 +02:00
def test_request_exception ( self ) - > None :
2020-07-24 19:21:42 +02:00
bot_user = self . example_user ( ' outgoing_webhook_bot ' )
mock_event = self . mock_event ( bot_user )
service_handler = GenericOutgoingWebhookService ( " token " , bot_user , " service " )
2020-07-24 18:46:14 +02:00
expect_request_exception = mock . patch ( " requests.request " , side_effect = request_exception_error )
2020-10-29 20:21:18 +01:00
expect_logging_exception = self . assertLogs ( level = " ERROR " )
2020-07-24 18:46:14 +02:00
expect_fail = mock . patch ( " zerver.lib.outgoing_webhook.fail_with_message " )
2020-10-29 20:21:18 +01:00
# 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.
2020-07-24 18:46:14 +02:00
with expect_request_exception , expect_logging_exception , expect_fail as mock_fail :
2020-07-24 19:21:42 +02:00
do_rest_call ( ' ' , None , mock_event , service_handler )
2020-07-24 18:46:14 +02:00
self . assertTrue ( mock_fail . called )
2017-08-29 15:21:25 +02:00
bot_owner_notification = self . get_last_message ( )
self . assertEqual ( bot_owner_notification . content ,
2018-10-27 16:37:29 +02:00
''' [A message](http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/) triggered an outgoing webhook.
2017-11-09 16:26:38 +01:00
When trying to send a request to the webhook service , an exception of type RequestException occurred :
2017-08-29 15:21:25 +02:00
` ` `
I ' m a generic exception :(
` ` ` ''' )
2020-07-24 19:21:42 +02:00
assert bot_user . bot_owner is not None
self . assertEqual ( bot_owner_notification . recipient_id , bot_user . bot_owner . id )
2017-11-03 13:38:49 +01:00
class TestOutgoingWebhookMessaging ( ZulipTestCase ) :
2020-03-28 12:25:37 +01:00
def create_outgoing_bot ( self , bot_owner : UserProfile ) - > UserProfile :
return self . create_test_bot (
' outgoing-webhook ' ,
bot_owner ,
full_name = ' Outgoing Webhook bot ' ,
bot_type = UserProfile . OUTGOING_WEBHOOK_BOT ,
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
service_name = ' foo-service ' ,
2020-03-28 12:25:37 +01:00
)
2017-11-03 13:38:49 +01:00
2020-03-28 13:22:19 +01:00
def test_multiple_services ( self ) - > None :
bot_owner = self . example_user ( " othello " )
bot = do_create_user (
bot_owner = bot_owner ,
bot_type = UserProfile . OUTGOING_WEBHOOK_BOT ,
full_name = ' Outgoing Webhook Bot ' ,
email = ' whatever ' ,
realm = bot_owner . realm ,
password = None ,
)
add_service (
' weather ' ,
user_profile = bot ,
interface = Service . GENERIC ,
base_url = ' weather_url ' ,
token = ' weather_token ' ,
)
add_service (
' qotd ' ,
user_profile = bot ,
interface = Service . GENERIC ,
base_url = ' qotd_url ' ,
token = ' qotd_token ' ,
)
sender = self . example_user ( " hamlet " )
with mock . patch ( ' zerver.worker.queue_processors.do_rest_call ' ) as m :
self . send_personal_message (
sender ,
bot ,
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
content = " some content " ,
2020-03-28 13:22:19 +01:00
)
url_token_tups = set ( )
for item in m . call_args_list :
args = item [ 0 ]
base_url = args [ 0 ]
2020-08-07 01:09:47 +02:00
request_data = orjson . loads ( args [ 1 ] )
2020-03-28 13:22:19 +01:00
tup = ( base_url , request_data [ ' token ' ] )
url_token_tups . add ( tup )
message_data = request_data [ ' message ' ]
self . assertEqual ( message_data [ ' content ' ] , ' some content ' )
self . assertEqual ( message_data [ ' sender_id ' ] , sender . id )
self . assertEqual (
url_token_tups ,
{
( ' weather_url ' , ' weather_token ' ) ,
( ' qotd_url ' , ' qotd_token ' ) ,
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
} ,
2020-03-28 13:22:19 +01:00
)
2020-08-07 01:09:47 +02:00
@mock.patch ( ' requests.request ' , return_value = ResponseMock ( 200 , orjson . dumps ( { " response_string " : " Hidley ho, I ' m a webhook responding! " } ) ) )
2017-11-05 10:51:25 +01:00
def test_pm_to_outgoing_webhook_bot ( self , mock_requests_request : mock . Mock ) - > None :
2020-03-28 12:25:37 +01:00
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
2020-03-28 12:37:36 +01:00
sender = self . example_user ( " hamlet " )
2020-03-28 12:25:37 +01:00
2020-03-28 12:37:36 +01:00
self . send_personal_message ( sender , bot ,
2017-11-03 13:38:49 +01:00
content = " foo " )
last_message = self . get_last_message ( )
2018-10-10 01:36:31 +02:00
self . assertEqual ( last_message . content , " Hidley ho, I ' m a webhook responding! " )
2020-03-28 12:25:37 +01:00
self . assertEqual ( last_message . sender_id , bot . id )
2020-03-28 12:37:36 +01:00
self . assertEqual (
last_message . recipient . type_id ,
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
sender . id ,
2020-03-28 12:37:36 +01:00
)
self . assertEqual (
last_message . recipient . type ,
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
Recipient . PERSONAL ,
2020-03-28 12:37:36 +01:00
)
2017-11-03 13:38:49 +01:00
2020-08-07 01:09:47 +02:00
@mock.patch ( ' requests.request ' , return_value = ResponseMock ( 200 , orjson . dumps ( { " response_string " : " Hidley ho, I ' m a webhook responding! " } ) ) )
2017-11-05 10:51:25 +01:00
def test_stream_message_to_outgoing_webhook_bot ( self , mock_requests_request : mock . Mock ) - > None :
2020-03-28 12:25:37 +01:00
bot_owner = self . example_user ( " othello " )
bot = self . create_outgoing_bot ( bot_owner )
self . send_stream_message ( bot_owner , " Denmark " ,
2020-06-09 00:25:09 +02:00
content = f " @** { bot . full_name } ** foo " ,
2017-11-03 13:38:49 +01:00
topic_name = " bar " )
last_message = self . get_last_message ( )
2018-10-10 01:36:31 +02:00
self . assertEqual ( last_message . content , " Hidley ho, I ' m a webhook responding! " )
2020-03-28 12:25:37 +01:00
self . assertEqual ( last_message . sender_id , bot . id )
2018-11-10 16:11:12 +01:00
self . assertEqual ( last_message . topic_name ( ) , " bar " )
2017-11-03 13:38:49 +01:00
display_recipient = get_display_recipient ( last_message . recipient )
self . assertEqual ( display_recipient , " Denmark " )